LogoLogo
WebsiteAppWiki
  • 🤙Start here
    • Overview
    • Shell Protocol Basics
    • Quickstart: Deploy a liquidity pool
    • Tutorial: Executing swap with Shell Protocol
    • Reference Library
      • Contract addresses
  • 🤿Deep dive
    • Important concepts
      • Separating accounting logic & business logic
      • Ocean Primitives
    • The Ocean
    • Primitives
      • Adapters
      • Proteus AMM Engine
      • NFT Fractionalizer
  • 📜Smart Contracts Specification
    • Ocean
      • Ocean.sol
      • IOceanPrimitive.sol
      • IOceanToken.sol
      • OceanAdapter.sol
    • Proteus
      • Proteus.sol
      • EvolvingProteus.sol
      • LiquidityPoolProxy.sol
    • Fractionalizer
      • FractionalizerFactory.sol
  • 🧠Tutorials
    • Build an AMM with Proteus
    • Build an NFT Fractionalizer
    • Build an NFT AMM
Powered by GitBook
On this page
  • What is the Ocean?
  • Creating Ocean Primitives
  • Ocean implementation details
  • Deriving token's Ocean IDs
  • Interactions
  • Interaction types
  • Interactions execution
  • Preparing (encoding) interactions for execution
  • Balances
  1. Deep dive

The Ocean

PreviousOcean PrimitivesNextPrimitives

Last updated 9 months ago

This is a developer's explanation of the Ocean. For a broad introduction, consult the . The Shell v2 is highly recommended as well.

The Ocean implementation can be found in Ocean.sol at the .

What is the Ocean?

The Ocean serves as the bedrock of Shell Protocol. Often referred to as the accounting layer, it functions as a unified accounting system.

Designed with adaptability in mind, Ocean can integrate any kind of primitive, be it AMMs, lending pools, algorithmic stablecoins, NFT markets, or innovative primitives yet to be invented. These primitives are called Ocean Primitives.

Creating Ocean Primitives

Functioning as an accounting system, the Ocean manages tasks like transferring, wrapping, and unwrapping tokens. While every Ocean Primitive receives queries from the Ocean for computational assessments based on input-output tokens and the type of interaction, only the Ocean directly manages token handling. In order to create an Ocean primitive smart contract developers must implement IOceanPrimitive interface in addition to primitive's internal logic.

/// @notice Implementing this allows a primitive to be called by the Ocean's
///  defi framework.
interface IOceanPrimitive {
    function computeOutputAmount(
        uint256 inputToken,
        uint256 outputToken,
        uint256 inputAmount,
        address userAddress,
        bytes32 metadata
    ) external returns (uint256 outputAmount);

    function computeInputAmount(
        uint256 inputToken,
        uint256 outputToken,
        uint256 outputAmount,
        address userAddress,
        bytes32 metadata
    ) external returns (uint256 inputAmount);

    function getTokenSupply(uint256 tokenId)
        external
        view
        returns (uint256 totalSupply);
}

Ocean implementation details

Within the Ocean’s ledger, tokens are categorized as two types:

  • Wrapped Tokens: Tokens from external ledgers (ERC-20, ERC-721, etc)

  • Native Tokens: Tokens created by Ocean Primitives.

Each token within the Ocean, regardless of being wrapped or native, has a corresponding Ocean ID, serving as a unique identifier.

Deriving token's Ocean IDs

Wrapped tokens

For ERC-20 tokens, Ocean IDs are sourced by casting the ERC-20 contract's address to a uint256:

uint256 oceanID = uint256(uint160(contractAddress));

For ERC-721 and ERC-1155 tokens, Ocean IDs are derived from a hash of the contract’s address and the token ID:

uint256 oceanID = uint256(keccak256(abi.encodePacked(contractAddress, tokenID)));

Native tokens

A native token’s Ocean ID is determined when an Ocean Primitive invokes the public function registerNewToken(uint256, uint256). The Ocean ID for native native tokens is derived by a hash combination of the primitive's contract address and a nonce: uint256 oceanID = uint256(keccak256(abi.encodePacked(primitiveAddress, nonce)));

/**
 * @title Interface for external contracts that issue tokens on the Ocean's
 *  public multitoken ledger
 * @dev Implemented by OceanERC1155.
 */
interface IOceanToken {
    function registerNewTokens(
        uint256 currentNumberOfTokens,
        uint256 numberOfAdditionalTokens
    ) external returns (uint256[] memory);
}
abstract contract LiquidityPool is IOceanPrimitive {
    /* ... */
    constructor(
        uint256 xToken_,
        uint256 yToken_,
        address ocean_,
        uint256 initialLpTokenSupply_,
        address claimer
    ) {
        claimerOrDeployer = claimer == address(0) ? msg.sender : claimer;
        initialLpTokenSupply = initialLpTokenSupply_;
        ocean = ocean_;
        xToken = xToken_;
        yToken = yToken_;
        uint256[] memory registeredToken = IOceanToken(ocean_)
            .registerNewTokens(0, 1);
        lpTokenId = registeredToken[0];
    }
    /* ... */
}
function registerNewTokens(
    uint256 currentNumberOfTokens,
    uint256 numberOfAdditionalTokens
) external override returns (uint256[] memory oceanIds) {
    oceanIds = new uint256[](numberOfAdditionalTokens);
    uint256[] memory nonces = new uint256[](numberOfAdditionalTokens);

    for (uint256 i = 0; i < numberOfAdditionalTokens; ++i) {
        uint256 tokenNonce = currentNumberOfTokens + i;
        uint256 newToken = _calculateOceanId(msg.sender, tokenNonce);
        nonces[i] = tokenNonce;
        oceanIds[i] = newToken;
        tokensToPrimitives[newToken] = msg.sender;
    }
    emit NewTokensRegistered(msg.sender, oceanIds, nonces);
}

Interactions

As the accounting component, the Ocean's primary function is to execute instructions — referred to as interactions — specified by users, Ocean Primitives, or other smart contracts.

Ocean-native primitives mustn't communicate with each other directly. They should relay interactions to the Ocean, which subsequently calls the appropriate primitives for computations.

Interaction types

enum InteractionType {
    WrapErc20,
    UnwrapErc20,
    WrapErc721,
    UnwrapErc721,
    WrapErc1155,
    UnwrapErc1155,
    ComputeInputAmount,
    ComputeOutputAmount,
    UnwrapEther
}

Interactions execution

Making a fully modular DeFi system is nothing more than composing Ocean Primitives, and composing these primitives is nothing more than orchestrating multiple interactions.

To execute an interaction or a series of interactions, one should invoke one of two public functions on the Ocean: doInteraction() or doMultipleInteractions(), accompanied by the necessary parameters.

function doInteraction(Interaction calldata interaction)
    external
    override
    nonReentrant
    returns (
        uint256 burnId,
        uint256 burnAmount,
        uint256 mintId,
        uint256 mintAmount
    )
{
    emit OceanTransaction(msg.sender, 1);
    return _doInteraction(interaction, msg.sender);
}

function doMultipleInteractions(
    Interaction[] calldata interactions,
    uint256[] calldata ids
)
    external
    payable
    override
    nonReentrant
    returns (
        uint256[] memory burnIds,
        uint256[] memory burnAmounts,
        uint256[] memory mintIds,
        uint256[] memory mintAmounts
    )
{
    emit OceanTransaction(msg.sender, interactions.length);
    return _doMultipleInteractions(interactions, ids, msg.sender);
}

Note that prior to invoking these methods, interactions need to be correctly formatted (encoded).

Preparing (encoding) interactions for execution

Interactions within the Ocean are uniformly encoded via the Interaction struct, encapsulating information for both Ocean Primitives and external ledgers (wrapping and unwrapping tokens).

struct Interaction {
    bytes32 ;
    uint256 inputToken;
    uint256 outputToken;
    uint256 specifiedAmount;
    bytes32 metadata;
}

Noticed the first field interactionTypeAndAddress in the Interaction struct?

It's a bytes array that combines both the interaction type and the address of the external contract with which the caller of the doInteraction() or doInteractions() functions intends the Ocean to query. This combination, or 'packing', of the interaction type with the external contract address is designed to save on gas consumption.

The final field, metadata, is primarily used for ERC-721 and ERC-1155 wraps and unwraps to specify the token ID. Though passed to the primitive by default, it's at the primitive's discretion to utilize this field or ignore it altogether. For example, Proteus primitives use metadata for enforcing slippage protection.

Balances

Every accountant maintains a balance sheet, and the Ocean is no exception. Functioning as an ERC-1155 contract, the Ocean inherently acts as a ledger. This structure endows the Ocean with potent capabilities, such as tracking state changes in memory, obviating the need for constant storage writing. This feature allows the composing of primitives in the Ocean to be up to three times cheaper than the alternatives.

The full implementation of this interface can be found here:

The Ocean is structured as an ERC-1155 smart contract, allowing it to interface with a multitude of token types, such as ERC-20, ERC-721, and ERC-1155. The architectural approach and execution of the Ocean present a notable advantage over the conventional Sequencer-Adapter and Vault-Router models. These intricacies are elucidated in sections 1.1 to 3.1 of the .

For an exhaustive understanding of Wrapped and Ocean native tokens and Ocean IDs, refer to sections 3.1 and 3.2 of the .

Calling the public function registerNewToken(unit256, unit256) by the primitive is done by passing Ocean contract address to the IOceanToken interface along with the necessary input data. You can review the IOceanToken interface in .

A practical demonstration of this function invocation to create an LP token is evident in the LiquidityPool.sol smart contract constructor .

For an in-depth look at the registerNewToken(uint256, uint256) function's implementation, refer to the OceanERC1155.sol file . The accompanying comments offer added context.

There are nine distinct interaction types that the Ocean can undertake. These are outlined in the InteractionType enum located in the Interactions.sol .

The ComputeInputAmount and ComputeOutputAmount interactions communicate with Ocean Primitives, invoking the respective computeInputAmount and computeOutputAmount methods that these primitives are mandated to implement (as detailed in the "" section). Other interactions within InteractionType are intuitive and cater to interactions with external token ledgers.

Actual implementation of these functions can be reviewed in the Ocean.sol file in our public repo, starting from and respectively.

Interaction struct definition can be found in the Interactions.sol file in our public repo, starting from the .

The interactionType aids the Ocean in parsing the struct correctly and corresponds to one of the nine potential options highlighted in the '' section.

It's not imperative for a smart contract developer to delve deeply into the Ocean's balance-handling mechanisms, so a detailed explanation is omitted here. For those keen on gaining a more comprehensive understanding of how the Ocean manages balances, it is advisable to consult sections 5.1 to 6 of .

🤿
Ocean wiki page
Ocean White Paper
following Shell GitHub link
https://github.com/Shell-Protocol/Shell-Protocol/blob/main/src/ocean/IOceanPrimitive.sol
Ocean White Paper
Ocean White Paper
our repo
here
here
file
lines 246
271
line 31
the Ocean white paper
Creating Ocean Primitives
Interaction Types