Adapters

Ocean adapter primitives

What is an adapter?

In DeFi, generally speaking, an adapter is any smart contract that integrates unrelated DeFi protocols with each other. The purpose of Ocean adapter smart contracts is to connect Shell with external protocols.

Ocean Adapters

The Ocean Adapter is compatible with Shell v3, which introduced certain changes to the Ocean smart contract. An Ocean Adapter won't work with older versions of Shell.

The Ocean Adapter is a special version of a DeFi Adapter smart contract that is written according to the Ocean specification, i.e. it inherits the special abstract contract OceanAdapter in order to integrate external projects and protocols, and make them compatible with the Ocean.

Anyone can write an OceanAdapter, to connect Shell Protocol and any project of the creator's liking. It's completely permissionless! You can see a few example of the Ocean Adapter in the following links in our repo:

  • Curve2PoolAdapter - coming soon

  • CurveTricryptoAdapter - coming soon

Why integrate with the Ocean?

As a fully open source and permissionless protocol, writing an Ocean Adapter is available to anyone. External projects can write their own adapters and integrate with Shell Protocol, getting the best out of its composability and unique architecture.

Integrated projects do not only become compatible with the Shell Protocol, but also with one another.

And last but not least, new protocols that are just starting and may not have big numbers in TVL and other metrics will get to tap into a much bigger user base than they would initially have. Every user on Shell Protocol becomes their user as well.

Creating an Ocean Adapter

In order to create an Ocean Adapter smart contract, developers must inherit the abstract contract OceanAdapter.sol, passing Ocean contract and Primitive contract addresses (the address for the contract the integration is written for) to its constructor like in the example below:

contract Curve2PoolAdapter is OceanAdapter {
    ...
    /**
     * @notice only initializing the immutables, mappings & approves tokens
     */
    constructor(address ocean_, address primitive_) OceanAdapter(ocean_, primitive_) {
        address xTokenAddress = ICurve2Pool(primitive).coins(0);
        xToken = _calculateOceanId(xTokenAddress, 0);
        underlying[xToken] = xTokenAddress;
        decimals[xToken] = IERC20Metadata(xTokenAddress).decimals();
        _approveToken(xTokenAddress);

        address yTokenAddress = ICurve2Pool(primitive).coins(1);
        yToken = _calculateOceanId(yTokenAddress, 0);
        indexOf[yToken] = int128(1);
        underlying[yToken] = yTokenAddress;
        decimals[yToken] = IERC20Metadata(yTokenAddress).decimals();
        _approveToken(yTokenAddress);

        lpTokenId = _calculateOceanId(primitive_, 0);
        underlying[lpTokenId] = primitive_;
        decimals[lpTokenId] = IERC20Metadata(primitive_).decimals();
        _approveToken(primitive_);
    }
    ...
}

One requirement that has to be satisfied is implementation of the three methods that were specified, but not implemented by the inherited abstract contract OceanAdapter.sol, and those methods are: primitiveOutputAmount, wrapToken and unwrapToken.

A reference of the actual implementation (which may be different for each Adapter) can be seen in Curve2PoolAdapter example:

/**
* @dev wraps the underlying token into the Ocean
* @param tokenId Ocean ID of token to wrap
* @param amount wrap amount
*/
function wrapToken(uint256 tokenId, uint256 amount) internal override {
    address tokenAddress = underlying[tokenId];

    Interaction memory interaction = Interaction({
        interactionTypeAndAddress: _fetchInteractionId(tokenAddress, uint256(InteractionType.WrapErc20)),
        inputToken: 0,
        outputToken: 0,
        specifiedAmount: amount,
        metadata: bytes32(0)
    });

    IOceanInteractions(ocean).doInteraction(interaction);
}

/**
* @dev unwraps the underlying token from the Ocean
* @param tokenId Ocean ID of token to unwrap
* @param amount unwrap amount
*/
function unwrapToken(uint256 tokenId, uint256 amount) internal override returns (uint256 unwrappedAmount) {
    address tokenAddress = underlying[tokenId];

    Interaction memory interaction = Interaction({
        interactionTypeAndAddress: _fetchInteractionId(tokenAddress, uint256(InteractionType.UnwrapErc20)),
        inputToken: 0,
        outputToken: 0,
        specifiedAmount: amount,
        metadata: bytes32(0)
    });

    IOceanInteractions(ocean).doInteraction(interaction);

     // handle the unwrap fee scenario
    uint256 unwrapFee = amount / IOceanInteractions(ocean).unwrapFeeDivisor();
    (, uint256 truncated) = _convertDecimals(NORMALIZED_DECIMALS, decimals[tokenId], amount - unwrapFee);
    unwrapFee = unwrapFee + truncated;

    unwrappedAmount = amount - unwrapFee;
}

/**
* @dev swaps/add liquidity/remove liquidity from Curve 2pool
* @param inputToken The user is giving this token to the pool
* @param outputToken The pool is giving this token to the user
* @param inputAmount The amount of the inputToken the user is giving to the pool
* @param minimumOutputAmount The minimum amount of tokens expected back after the exchange
*/
function primitiveOutputAmount(
    uint256 inputToken,
    uint256 outputToken,
    uint256 inputAmount,
    bytes32 minimumOutputAmount
)
    internal
    override
    returns (uint256 outputAmount)
{
    (uint256 rawInputAmount,) = _convertDecimals(NORMALIZED_DECIMALS, decimals[inputToken], inputAmount);

    ComputeType action = _determineComputeType(inputToken, outputToken);

    uint256 rawOutputAmount;

    // avoid multiple SLOADS
    int128 indexOfInputAmount = indexOf[inputToken];
    int128 indexOfOutputAmount = indexOf[outputToken];

    if (action == ComputeType.Swap) {
        rawOutputAmount =
            ICurve2Pool(primitive).exchange(indexOfInputAmount, indexOfOutputAmount, rawInputAmount, 0);
    } else if (action == ComputeType.Deposit) {
        uint256[2] memory inputAmounts;
        inputAmounts[uint256(int256(indexOfInputAmount))] = rawInputAmount;
        rawOutputAmount = ICurve2Pool(primitive).add_liquidity(inputAmounts, 0);
    } else {
        rawOutputAmount = ICurve2Pool(primitive).remove_liquidity_one_coin(rawInputAmount, indexOfOutputAmount, 0);
    }

    (outputAmount,) = _convertDecimals(decimals[outputToken], NORMALIZED_DECIMALS, rawOutputAmount);

    if (uint256(minimumOutputAmount) > outputAmount) revert SLIPPAGE_LIMIT_EXCEEDED();

    if (action == ComputeType.Swap) {
        emit Swap(inputToken, inputAmount, outputAmount, minimumOutputAmount, primitive, true);
    } else if (action == ComputeType.Deposit) {
        emit Deposit(inputToken, inputAmount, outputAmount, minimumOutputAmount, primitive, true);
    } else {
        emit Withdraw(outputToken, inputAmount, outputAmount, minimumOutputAmount, primitive, true);
    }
}

Other than that, smart contract developers are free to add arbitrarily complex logic and custom functionality to their Adapters.

The full implementation of abstract contract OceanAdapter.sol can be found here: [link coming soon].

Last updated