The Ocean

This page delves into Shell Protocol's accounting component, the Ocean, with a focus tailored for developers. For a broader yet detailed explanation, consult the Ocean page on Shell Protocol's wiki. For a comprehensive understanding, the Ocean White Paper is highly recommended. The Ocean's implementation can be found in our public repository in Ocean.sol file at the following link.

What is the Ocean?

The Ocean serves as the bedrock of the 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 even 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);
}

The full implementation of this interface can be found here: https://github.com/Shell-Protocol/Shell-Protocol/blob/main/src/ocean/IOceanPrimitive.sol

Ocean implementation details

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 Ocean White Paper.

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

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

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)));

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 our repo.

/**
 * @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);
}

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

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];
    }
    /* ... */
}

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

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

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

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

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 "Creating Ocean Primitives" section). Other interactions within InteractionType are intuitive and cater to interactions with external token ledgers.

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);
}

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

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).

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

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 interactionType aids the Ocean in parsing the struct correctly and corresponds to one of the nine potential options highlighted in the 'Interaction Types' section.

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.

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 the Ocean white paper.

Last updated