The Ocean
This is a developer's explanation of the Ocean. For a broad introduction, consult the Ocean wiki page. The Shell v2 Ocean White Paper is highly recommended as well.
The Ocean implementation can be found in Ocean.sol
at the following Shell GitHub link.
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.
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.
A practical demonstration of this function invocation to create an LP token is evident in the LiquidityPool.sol
smart contract constructor here.
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.
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.
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.
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.
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