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
SushiAdapter.solThe 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