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