Sushi Protocol Adapter

The Sushi adapter enables the Yieldinator Facet to integrate with SushiSwap's liquidity pools and SushiBar (xSUSHI) staking, allowing vaults to earn trading fees, SUSHI rewards, and additional yield from staking SUSHI tokens.

Overview

SushiSwap is a decentralized exchange and yield farming platform that allows users to provide liquidity to trading pairs, earn trading fees, and farm SUSHI tokens. The Sushi adapter facilitates deposits into SushiSwap pools, manages SLP (Sushi Liquidity Provider) token balances, stakes in MasterChef farming contracts, and optionally stakes SUSHI rewards in the SushiBar for additional yield.

Implementation Details

Contract: SushiAdapter.sol

The adapter implements the standard YieldinatorAdapter interface with Sushi-specific functionality:

contract SushiAdapter is YieldinatorAdapter {
    using SafeERC20 for IERC20;
    
    // Sushi contracts
    address public sushiToken;
    address public xSushiToken; // SushiBar token
    IMasterChef public masterChef;
    ISushiBar public sushiBar;
    
    // Pool configuration
    struct PoolConfig {
        address pair; // SushiSwap pair address
        uint256 pid; // Pool ID in MasterChef
        address token0; // First token in the pair
        address token1; // Second token in the pair
        bool stakeLp; // Whether to stake LP tokens in MasterChef
        bool stakeSushi; // Whether to stake SUSHI rewards in SushiBar
        bool active; // Whether the pool is active
    }
    
    // Mapping from token to pool configuration
    mapping(address => PoolConfig) public tokenToPool;
    
    // Deposited amounts per token
    mapping(address => uint256) public depositedAmount;
    
    // Constructor
    constructor(
        address _admin,
        address _sushiToken,
        address _xSushiToken,
        address _masterChef,
        address _sushiBar
    ) YieldinatorAdapter("SushiSwap", _admin) {
        require(_sushiToken != address(0), "SushiAdapter: sushi token cannot be zero address");
        require(_xSushiToken != address(0), "SushiAdapter: xSushi token cannot be zero address");
        require(_masterChef != address(0), "SushiAdapter: masterChef cannot be zero address");
        require(_sushiBar != address(0), "SushiAdapter: sushiBar cannot be zero address");
        
        sushiToken = _sushiToken;
        xSushiToken = _xSushiToken;
        masterChef = IMasterChef(_masterChef);
        sushiBar = ISushiBar(_sushiBar);
    }
}

Key Functions

Pool Registration

Before using the adapter for a specific token, the pool must be registered:

function registerPool(
    address _token,
    address _pair,
    uint256 _pid,
    address _token0,
    address _token1,
    bool _stakeLp,
    bool _stakeSushi
) external onlyRole(ADMIN_ROLE) {
    require(_token != address(0), "SushiAdapter: token cannot be zero address");
    require(_pair != address(0), "SushiAdapter: pair cannot be zero address");
    require(_token0 != address(0), "SushiAdapter: token0 cannot be zero address");
    require(_token1 != address(0), "SushiAdapter: token1 cannot be zero address");
    
    // Verify the token is one of the pair tokens
    require(_token == _token0 || _token == _token1, "SushiAdapter: token must be in pair");
    
    // Create and store pool configuration
    PoolConfig memory config = PoolConfig({
        pair: _pair,
        pid: _pid,
        token0: _token0,
        token1: _token1,
        stakeLp: _stakeLp,
        stakeSushi: _stakeSushi,
        active: true
    });
    
    tokenToPool[_token] = config;
    
    emit PoolRegistered(_token, _pair, _pid);
}

Deposit

The deposit function handles adding liquidity to SushiSwap and optionally staking LP tokens:

function deposit(address _token, uint256 _amount)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (bool)
{
    require(_amount > 0, "SushiAdapter: amount must be greater than 0");
    PoolConfig memory config = tokenToPool[_token];
    require(config.active, "SushiAdapter: pool not active for token");
    
    // Transfer token from caller
    IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
    
    // Determine other token in pair and amount needed
    address otherToken = (_token == config.token0) ? config.token1 : config.token0;
    uint256 otherAmount = _calculateRequiredAmount(_token, otherToken, _amount);
    
    // Approve router to use tokens
    IERC20(_token).safeApprove(address(router), _amount);
    IERC20(otherToken).safeApprove(address(router), otherAmount);
    
    // Add liquidity to SushiSwap
    (uint256 amountA, uint256 amountB, uint256 liquidity) = router.addLiquidity(
        _token,
        otherToken,
        _amount,
        otherAmount,
        _amount * 95 / 100, // 5% slippage
        otherAmount * 95 / 100, // 5% slippage
        address(this),
        block.timestamp + 1800 // 30 minutes deadline
    );
    
    // If staking LP tokens is enabled, stake in MasterChef
    if (config.stakeLp) {
        IERC20(config.pair).safeApprove(address(masterChef), liquidity);
        masterChef.deposit(config.pid, liquidity);
    }
    
    // Update deposited amount
    depositedAmount[_token] += amountA;
    
    emit Deposited(_token, amountA);
    return true;
}

Withdraw

The withdraw function handles removing liquidity from SushiSwap and unstaking LP tokens if necessary:

function withdraw(address _token, uint256 _amount)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (uint256)
{
    require(_amount > 0, "SushiAdapter: amount must be greater than 0");
    require(_amount <= depositedAmount[_token], "SushiAdapter: insufficient balance");
    
    PoolConfig memory config = tokenToPool[_token];
    require(config.active, "SushiAdapter: pool not active for token");
    
    // Calculate proportion of LP tokens to withdraw
    uint256 lpBalance = _getLpBalance(config);
    uint256 lpToWithdraw = lpBalance * _amount / depositedAmount[_token];
    
    // If LP tokens are staked, unstake from MasterChef
    if (config.stakeLp) {
        masterChef.withdraw(config.pid, lpToWithdraw);
        // Harvest SUSHI rewards
        _harvestSushiRewards(config);
    }
    
    // Remove liquidity from SushiSwap
    address otherToken = (_token == config.token0) ? config.token1 : config.token0;
    IERC20(config.pair).safeApprove(address(router), lpToWithdraw);
    
    (uint256 amountA, uint256 amountB) = router.removeLiquidity(
        _token,
        otherToken,
        lpToWithdraw,
        _amount * 95 / 100, // 5% slippage
        0, // Min amount for other token
        address(this),
        block.timestamp + 1800 // 30 minutes deadline
    );
    
    // Transfer withdrawn token to caller
    IERC20(_token).safeTransfer(msg.sender, amountA);
    
    // Update deposited amount
    depositedAmount[_token] -= amountA;
    
    emit Withdrawn(_token, amountA);
    return amountA;
}

Harvest Yield

The harvestYield function collects SUSHI rewards and optionally stakes them in SushiBar:

function harvestYield(address _token)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (uint256)
{
    PoolConfig memory config = tokenToPool[_token];
    require(config.active, "SushiAdapter: pool not active for token");
    
    // Harvest SUSHI rewards without withdrawing LP tokens
    if (config.stakeLp) {
        masterChef.deposit(config.pid, 0);
    }
    
    return _harvestSushiRewards(config);
}

function _harvestSushiRewards(PoolConfig memory config) internal returns (uint256) {
    uint256 sushiBalance = IERC20(sushiToken).balanceOf(address(this));
    
    // If staking SUSHI rewards is enabled, stake in SushiBar
    if (config.stakeSushi && sushiBalance > 0) {
        IERC20(sushiToken).safeApprove(address(sushiBar), sushiBalance);
        sushiBar.enter(sushiBalance);
        
        // Calculate xSUSHI received
        uint256 xSushiBalance = IERC20(xSushiToken).balanceOf(address(this));
        emit YieldHarvested(sushiToken, sushiBalance, xSushiToken, xSushiBalance);
        return xSushiBalance;
    } else if (sushiBalance > 0) {
        // Transfer SUSHI rewards to caller
        IERC20(sushiToken).safeTransfer(msg.sender, sushiBalance);
        emit YieldHarvested(sushiToken, sushiBalance, address(0), 0);
        return sushiBalance;
    }
    
    return 0;
}

Emergency Withdraw

The emergencyWithdraw function provides a safety mechanism to withdraw all funds:

function emergencyWithdraw(address _token)
    external
    override
    onlyRole(ADMIN_ROLE)
    nonReentrant
    returns (uint256)
{
    PoolConfig memory config = tokenToPool[_token];
    require(config.active, "SushiAdapter: pool not active for token");
    
    // Emergency withdraw from MasterChef if LP tokens are staked
    if (config.stakeLp) {
        uint256 lpBalance = _getLpBalance(config);
        if (lpBalance > 0) {
            masterChef.emergencyWithdraw(config.pid);
        }
    }
    
    // Remove liquidity from SushiSwap
    uint256 lpBalance = IERC20(config.pair).balanceOf(address(this));
    if (lpBalance > 0) {
        address otherToken = (_token == config.token0) ? config.token1 : config.token0;
        IERC20(config.pair).safeApprove(address(router), lpBalance);
        
        router.removeLiquidity(
            _token,
            otherToken,
            lpBalance,
            0, // Min amount for token
            0, // Min amount for other token
            address(this),
            block.timestamp + 1800 // 30 minutes deadline
        );
    }
    
    // Transfer all token balance to caller
    uint256 tokenBalance = IERC20(_token).balanceOf(address(this));
    if (tokenBalance > 0) {
        IERC20(_token).safeTransfer(msg.sender, tokenBalance);
    }
    
    // Transfer all SUSHI and xSUSHI to caller
    uint256 sushiBalance = IERC20(sushiToken).balanceOf(address(this));
    if (sushiBalance > 0) {
        IERC20(sushiToken).safeTransfer(msg.sender, sushiBalance);
    }
    
    uint256 xSushiBalance = IERC20(xSushiToken).balanceOf(address(this));
    if (xSushiBalance > 0) {
        IERC20(xSushiToken).safeTransfer(msg.sender, xSushiBalance);
    }
    
    // Reset deposited amount
    depositedAmount[_token] = 0;
    
    emit EmergencyWithdrawn(_token, tokenBalance);
    return tokenBalance;
}

APY Calculation

The getCurrentAPY function calculates the current APY for a token in the SushiSwap pool:

function getCurrentAPY(address _token)
    external
    view
    override
    returns (uint256)
{
    PoolConfig memory config = tokenToPool[_token];
    if (!config.active) return 0;
    
    // Calculate trading fee APY (0.3% fee per trade)
    uint256 tradingFeeAPY = _calculateTradingFeeAPY(config.pair);
    
    // Calculate SUSHI rewards APY
    uint256 sushiRewardsAPY = _calculateSushiRewardsAPY(config.pid);
    
    // Add additional APY from SushiBar if staking SUSHI
    uint256 sushiBarAPY = config.stakeSushi ? _calculateSushiBarAPY() : 0;
    
    // Combine APYs
    return tradingFeeAPY + sushiRewardsAPY + sushiBarAPY;
}

Usage Example

Here's an example of how to use the Sushi adapter with the Yieldinator Facet:

// Register the Sushi adapter with the Yieldinator Facet
address sushiAdapter = address(new SushiAdapter(
    admin,
    sushiToken,
    xSushiToken,
    masterChef,
    sushiBar
));
yieldinatorFacet.registerAdapter("SushiSwap", sushiAdapter);

// Register a pool for WETH in the WETH-USDC pair
SushiAdapter(sushiAdapter).registerPool(
    WETH,
    WETH_USDC_PAIR,
    1, // Pool ID in MasterChef
    WETH,
    USDC,
    true, // Stake LP tokens
    true  // Stake SUSHI rewards
);

// Add a yield strategy using the Sushi adapter
yieldinatorFacet.addYieldStrategy(
    "SushiSwap WETH-USDC",
    sushiAdapter,
    WETH,
    2, // Medium risk level
    8500 // 85.00% APY
);

Security Considerations

  • Impermanent Loss: Users should be aware of impermanent loss risks when providing liquidity to SushiSwap pairs.

  • Smart Contract Risk: The adapter interacts with multiple external contracts (SushiSwap Router, MasterChef, SushiBar) which may have vulnerabilities.

  • Price Impact: Large deposits or withdrawals may experience significant price impact, especially for less liquid pairs.

  • Slippage Protection: The adapter includes slippage protection (5% by default) to prevent excessive value loss during swaps.

Gas Optimization

  • The adapter minimizes gas usage by batching operations when possible.

  • For harvesting rewards, users can choose whether to stake SUSHI in SushiBar based on gas costs and expected returns.

  • The adapter avoids unnecessary approvals by only approving tokens when needed.

Future Improvements

  • Support for SushiSwap's BentoBox and Kashi lending

  • Integration with SushiSwap's limit order functionality

  • Support for concentrated liquidity pools (Trident)

  • Automated compounding of rewards for maximum yield