Lido Protocol Adapter

The Lido adapter enables the Yieldinator Facet to integrate with Lido's liquid staking protocol, allowing vaults to earn staking rewards on ETH while maintaining liquidity through stETH tokens.

Overview

Lido is a liquid staking solution for Ethereum that allows users to stake their ETH without locking assets or maintaining staking infrastructure. When users stake ETH with Lido, they receive stETH tokens that represent their staked ETH plus accrued staking rewards. The stETH token balance automatically increases over time as staking rewards are earned, providing a simple way to earn yield on ETH while maintaining liquidity. The Lido adapter facilitates deposits into Lido's staking protocol, manages stETH positions, and tracks yield from staking rewards.

Implementation Details

Contract: LidoAdapter.sol

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

contract LidoAdapter is YieldinatorAdapter {
    using SafeERC20 for IERC20;
    
    // Lido contracts
    address public stETH; // Lido's staked ETH token
    address public wstETH; // Wrapped stETH token
    ILido public lido; // Lido staking contract
    IwstETH public wstETHContract; // Wrapped stETH contract
    
    // Staking configuration
    struct StakingConfig {
        bool useWrapped; // Whether to use wrapped stETH (wstETH)
        bool active; // Whether staking is active
    }
    
    // Configuration for ETH staking
    StakingConfig public ethStakingConfig;
    
    // Deposited amounts
    uint256 public depositedAmount;
    
    // Constructor
    constructor(
        address _admin,
        address _stETH,
        address _wstETH,
        address _lido
    ) YieldinatorAdapter("Lido", _admin) {
        require(_stETH != address(0), "LidoAdapter: stETH cannot be zero address");
        require(_wstETH != address(0), "LidoAdapter: wstETH cannot be zero address");
        require(_lido != address(0), "LidoAdapter: Lido cannot be zero address");
        
        stETH = _stETH;
        wstETH = _wstETH;
        lido = ILido(_lido);
        wstETHContract = IwstETH(_wstETH);
    }
}

Key Functions

Configure Staking

Before using the adapter, the staking configuration must be set:

function configureStaking(bool _useWrapped) external onlyRole(ADMIN_ROLE) {
    ethStakingConfig = StakingConfig({
        useWrapped: _useWrapped,
        active: true
    });
    
    emit StakingConfigured(_useWrapped);
}

Deposit

The deposit function handles staking ETH with Lido and optionally wrapping stETH:

function deposit(address _token, uint256 _amount)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (bool)
{
    require(_amount > 0, "LidoAdapter: amount must be greater than 0");
    require(_token == address(0), "LidoAdapter: only ETH deposits supported");
    require(ethStakingConfig.active, "LidoAdapter: staking not active");
    
    // Ensure we have enough ETH
    require(address(this).balance >= _amount, "LidoAdapter: insufficient ETH balance");
    
    // Stake ETH with Lido
    uint256 stETHBefore = IERC20(stETH).balanceOf(address(this));
    lido.submit{value: _amount}(address(0)); // Submit with no referral
    uint256 stETHAfter = IERC20(stETH).balanceOf(address(this));
    
    // Calculate stETH received
    uint256 stETHReceived = stETHAfter - stETHBefore;
    require(stETHReceived > 0, "LidoAdapter: no stETH received");
    
    // If using wrapped stETH, wrap the stETH
    if (ethStakingConfig.useWrapped) {
        IERC20(stETH).safeApprove(wstETH, stETHReceived);
        wstETHContract.wrap(stETHReceived);
    }
    
    // Update deposited amount
    depositedAmount += _amount;
    
    emit Deposited(address(0), _amount);
    return true;
}

// Special function to receive ETH before deposit
receive() external payable {
    // Only accept ETH from the Yieldinator facet
    require(hasRole(YIELDINATOR_ROLE, msg.sender), "LidoAdapter: unauthorized ETH sender");
}

Withdraw

The withdraw function handles unstaking from Lido by exchanging stETH for ETH:

function withdraw(address _token, uint256 _amount)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (uint256)
{
    require(_amount > 0, "LidoAdapter: amount must be greater than 0");
    require(_token == address(0), "LidoAdapter: only ETH withdrawals supported");
    require(_amount <= depositedAmount, "LidoAdapter: insufficient balance");
    require(ethStakingConfig.active, "LidoAdapter: staking not active");
    
    uint256 ethToReturn;
    
    if (ethStakingConfig.useWrapped) {
        // Calculate proportion of wstETH to unwrap
        uint256 wstETHBalance = IERC20(wstETH).balanceOf(address(this));
        uint256 wstETHToUnwrap = wstETHBalance * _amount / depositedAmount;
        
        // Unwrap wstETH to get stETH
        uint256 stETHReceived = wstETHContract.unwrap(wstETHToUnwrap);
        
        // Exchange stETH for ETH (in real implementation, this would use Curve or another liquidity source)
        ethToReturn = _exchangeStETHForETH(stETHReceived);
    } else {
        // Calculate proportion of stETH to exchange
        uint256 stETHBalance = IERC20(stETH).balanceOf(address(this));
        uint256 stETHToExchange = stETHBalance * _amount / depositedAmount;
        
        // Exchange stETH for ETH (in real implementation, this would use Curve or another liquidity source)
        ethToReturn = _exchangeStETHForETH(stETHToExchange);
    }
    
    require(ethToReturn > 0, "LidoAdapter: no ETH received");
    
    // Transfer ETH to caller
    (bool success, ) = msg.sender.call{value: ethToReturn}("");
    require(success, "LidoAdapter: ETH transfer failed");
    
    // Update deposited amount
    depositedAmount -= _amount;
    
    emit Withdrawn(address(0), ethToReturn);
    return ethToReturn;
}

function _exchangeStETHForETH(uint256 _stETHAmount) internal returns (uint256) {
    // In a real implementation, this would use Curve's stETH/ETH pool or another liquidity source
    // For this example, we'll assume a simplified exchange mechanism
    
    // Approve Curve pool to use stETH
    IERC20(stETH).safeApprove(curvePool, _stETHAmount);
    
    // Exchange stETH for ETH using Curve
    uint256 ethBefore = address(this).balance;
    ICurvePool(curvePool).exchange(0, 1, _stETHAmount, 0); // Simplified Curve call
    uint256 ethAfter = address(this).balance;
    
    return ethAfter - ethBefore;
}

Harvest Yield

The harvestYield function calculates the yield earned from staking rewards:

function harvestYield(address _token)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (uint256)
{
    require(_token == address(0), "LidoAdapter: only ETH supported");
    require(ethStakingConfig.active, "LidoAdapter: staking not active");
    
    // Lido automatically accrues staking rewards by increasing the stETH balance
    // There's no explicit harvest action needed
    
    // Calculate the current value in ETH
    uint256 currentValue = _getCurrentValueInETH();
    
    // Calculate yield as the difference between current value and deposited amount
    if (currentValue > depositedAmount) {
        uint256 yield = currentValue - depositedAmount;
        emit YieldHarvested(address(0), yield, address(0), 0);
        return yield;
    }
    
    return 0;
}

function _getCurrentValueInETH() internal view returns (uint256) {
    if (ethStakingConfig.useWrapped) {
        // Calculate ETH value of wstETH holdings
        uint256 wstETHBalance = IERC20(wstETH).balanceOf(address(this));
        uint256 stETHEquivalent = wstETHContract.unwrap(wstETHBalance);
        return _getETHValueOfStETH(stETHEquivalent);
    } else {
        // Calculate ETH value of stETH holdings
        uint256 stETHBalance = IERC20(stETH).balanceOf(address(this));
        return _getETHValueOfStETH(stETHBalance);
    }
}

function _getETHValueOfStETH(uint256 _stETHAmount) internal view returns (uint256) {
    // In Lido, the stETH/ETH exchange rate is determined by the total staked ETH and total stETH supply
    return _stETHAmount * lido.getTotalPooledEther() / lido.getTotalShares();
}

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)
{
    require(_token == address(0), "LidoAdapter: only ETH supported");
    require(ethStakingConfig.active, "LidoAdapter: staking not active");
    
    uint256 ethReceived = 0;
    
    // Unwrap wstETH if using wrapped stETH
    if (ethStakingConfig.useWrapped) {
        uint256 wstETHBalance = IERC20(wstETH).balanceOf(address(this));
        if (wstETHBalance > 0) {
            uint256 stETHReceived = wstETHContract.unwrap(wstETHBalance);
            
            // Exchange stETH for ETH
            ethReceived += _exchangeStETHForETH(stETHReceived);
        }
    } else {
        // Exchange stETH for ETH
        uint256 stETHBalance = IERC20(stETH).balanceOf(address(this));
        if (stETHBalance > 0) {
            ethReceived += _exchangeStETHForETH(stETHBalance);
        }
    }
    
    // Add any direct ETH balance
    ethReceived += address(this).balance;
    
    // Transfer all ETH to caller
    if (ethReceived > 0) {
        (bool success, ) = msg.sender.call{value: ethReceived}("");
        require(success, "LidoAdapter: ETH transfer failed");
    }
    
    // Reset deposited amount
    depositedAmount = 0;
    
    emit EmergencyWithdrawn(address(0), ethReceived);
    return ethReceived;
}

APY Calculation

The getCurrentAPY function calculates the current APY for staking with Lido:

function getCurrentAPY(address _token)
    external
    view
    override
    returns (uint256)
{
    require(_token == address(0), "LidoAdapter: only ETH supported");
    if (!ethStakingConfig.active) return 0;
    
    // Get the current APY from Lido's oracle or calculate based on historical data
    return _getLidoAPY();
}

function _getLidoAPY() internal view returns (uint256) {
    // In a real implementation, this would query Lido's APR from their oracle
    // or calculate based on historical stETH rebasing data
    
    // For this example, we'll return a fixed APY of 4.5%
    return 450; // 4.50%
}

Get Total Deposited

The getTotalDeposited function returns the total amount of ETH deposited:

function getTotalDeposited(address _token)
    external
    view
    override
    returns (uint256)
{
    require(_token == address(0), "LidoAdapter: only ETH supported");
    return depositedAmount;
}

Usage Example

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

// Register the Lido adapter with the Yieldinator Facet
address lidoAdapter = address(new LidoAdapter(
    admin,
    stETH,
    wstETH,
    lido
));
yieldinatorFacet.registerAdapter("Lido", lidoAdapter);

// Configure staking to use wrapped stETH (wstETH)
LidoAdapter(lidoAdapter).configureStaking(true);

// Add a yield strategy using the Lido adapter
yieldinatorFacet.addYieldStrategy(
    "Lido ETH Staking",
    lidoAdapter,
    address(0), // ETH
    1, // Low risk level
    450 // 4.50% APY
);

// Send ETH to the adapter before deposit
(bool success, ) = lidoAdapter.call{value: 10 ether}("");
require(success, "ETH transfer failed");

// Deposit ETH into Lido
yieldinatorFacet.deposit(
    "Lido ETH Staking",
    10 ether
);

Security Considerations

  • Validator Risk: Lido's staking security depends on the performance and honesty of its validator set.

  • Smart Contract Risk: The adapter interacts with multiple external contracts which may have vulnerabilities.

  • Liquidity Risk: While stETH is designed to be liquid, there may be periods of limited liquidity for converting back to ETH.

  • Slippage Risk: When exchanging stETH for ETH, users may experience slippage depending on market conditions.

Risk Mitigation

  • The adapter implements strict access controls to prevent unauthorized access.

  • Emergency withdrawal functionality is available to recover funds in case of critical issues.

  • The adapter validates all inputs and handles edge cases to prevent unexpected behavior.

  • Option to use wrapped stETH (wstETH) for better composability with other DeFi protocols.

Gas Optimization

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

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

  • Using wstETH can reduce gas costs for frequent interactions with the position.

Future Improvements

  • Support for Lido on other networks (Polygon, Solana, Kusama)

  • Integration with Lido's governance system for participating in protocol decisions

  • Support for leveraged staking strategies using stETH as collateral

  • Automated reinvestment of staking rewards for compound growth

  • Integration with insurance protocols to hedge against validator slashing risks