Protocol Adapters

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

Writing Ocean Adapter was introduced in the Shell v3 which required certain changes in the Ocean contract. Trying to write Ocean Adapters and making them work with older versions won't work.

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

Anyone can write an OceanAdapter creating the synergy and compatibility between Shell Protocol and project of creators liking. It's completely permissionless, there's no allow list or anything similar. You can see a few example of Ocean Adapter written by the at the following links in our repo:

  • Curve2PoolAdapter - coming soon

  • CurveTricryptoAdapter - coming soon

Why integrate with the Ocean?

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

With each integration project and protocol do not only integrate and 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 are getting the opportunity of tapping into the much bigger user base than they would initially have. Every Shell Protocol's user immediately becomes their's as well.

Creating an Ocean Adapter

In order to create Ocean Adapter smart contract developers must inherit abstract contract OceanAdapter.sol passing Ocean contract and Primitive contract addresses (address for the contract we integration is written for) to it's 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