Skip to main content
The Lido integration simulates wrapped staked ETH (wstETH) with per-tick exchange rate growth. It’s commonly used as collateral in Aave for cross-protocol leverage strategies.

Configuration

LidoConfig

pub struct LidoConfig {
    pub token_in: TokenId,                   // Staking token (e.g., ETH)
    pub token_out: TokenId,                  // Share token (e.g., wstETH)
    pub reward_rate_ray_per_tick: u128,      // Exchange rate growth per tick (RAY)
}

Initialization

use ank_protocol_lido::{Lido, LidoConfig};
use ank_accounting::TokenId;

let lido = Lido::new(LidoConfig {
    token_in: TokenId(1),   // ETH
    token_out: TokenId(3),  // wstETH
    reward_rate_ray_per_tick: 1_000_000_000_000_000_000_000, // ~0.0001% per tick
});

Available Actions

Description: Stake ETH to receive wstETH shares.Parameters:
  • amount (u128): Amount of ETH to stake (e18)
Example:
{"kind":"stake","amount":"1000000000000000000000"}
Mechanics:
shares = amount / exchange_rate_ray
Effects:
  • Deducts amount ETH from wallet
  • Credits shares wstETH to wallet
Gas: ~120,000
Description: Burn wstETH shares to receive ETH.Parameters:
  • shares (u128): Amount of wstETH shares to burn
Example:
{"kind":"unstake","shares":"500000000000000000000"}
Mechanics:
amount = shares * exchange_rate_ray
Effects:
  • Deducts shares wstETH from wallet
  • Credits amount ETH to wallet
Gas: ~140,000
Description: Convert stETH to wstETH (simplified 1:1 in this model).Parameters:
  • amount (u128): Amount to wrap
Example:
{"kind":"wrap","amount":"100000000000000000000"}
This is a simplified action for compatibility. In production Lido, wrapping uses exchange rate.
Gas: ~80,000

Exchange Rate Growth

The exchange rate compounds per tick:
// on_tick()
exchange_rate_ray += reward_rate_ray_per_tick

// Example:
// Tick 0: 1.000000000000000000000000000 (1.0)
// Tick 1: 1.001000000000000000000000000 (1.001)
// Tick 2: 1.002000000000000000000000000 (1.002)
This simulates staking rewards accruing to wstETH holders.

View Methods

view_user

Returns user shares and equivalent underlying:
{
  "shares": "500000000000000000000",
  "underlying_equiv_units": "505000000000000000000"
}

view_market

Returns protocol state:
{
  "token_in": 1,
  "token_out": 3,
  "exchange_rate_ray": "1005000000000000000000000000",
  "reward_rate_ray_per_tick": "1000000000000000000000",
  "total_shares": "10000000000000000000000"
}

Usage Examples

Simple Stake

use ank_protocol::{Action, Protocol};

// Stake 100 ETH
lido.execute(
    ts,
    user_id,
    Action::Custom(serde_json::json!({
        "kind": "stake",
        "amount": "100000000000000000000"
    })),
)?;

Lido → Aave Leverage Loop

use serde_json::json;

// Step 1: Stake ETH in Lido
let stake_out = lido.execute(
    ts,
    user_id,
    Action::Custom(json!({"kind":"stake","amount":"1000"})),
)?;
let wsteth_shares = stake_out.delta.0[&TokenId(3)];

// Step 2: Deposit wstETH to Aave
aave.execute(
    ts,
    user_id,
    Action::Custom(json!({
        "kind": "deposit",
        "token": 3,
        "amount": wsteth_shares.to_string()
    })),
)?;

// Step 3: Borrow ETH from Aave
aave.execute(
    ts,
    user_id,
    Action::Custom(json!({
        "kind": "borrow",
        "token": 1,
        "amount": "700"
    })),
)?;

// Step 4: Loop back to Step 1 with borrowed ETH

Sync wstETH Price to Aave

// Get Lido exchange rate
let lido_view = lido.view_market();
let er_ray = lido_view["exchange_rate_ray"].as_str().unwrap().parse::<u128>()?;

// Get ETH price from Aave
let aave_view = aave.view_market();
let eth_price = aave_view["reserves"]["1"]["price_e18"]
    .as_str()
    .unwrap()
    .parse::<u128>()?;

// wstETH price = ETH price * exchange_rate
let wsteth_price = ray_mul(eth_price, er_ray);

// Update Aave oracle
aave.execute(
    ts,
    0, // admin/oracle user
    Action::Custom(json!({
        "kind": "set_price",
        "token": 3,
        "price_e18": wsteth_price.to_string()
    })),
)?;

Cross-Protocol Integration

Lido wstETH is typically used as collateral in Aave:
// 1. User stakes ETH → receives wstETH
// 2. User deposits wstETH to Aave
// 3. User borrows ETH against wstETH collateral
// 4. Strategy monitors LTV and adjusts via:
//    - Borrow more ETH → stake → deposit (leverage up)
//    - Withdraw wstETH → unstake → repay (deleverage)
The exchange rate growth means wstETH value increases over time, improving health factor and enabling more borrowing.

Protocol-Specific Behavior

Non-Rebasing Shares

wstETH uses a share-based model (not rebasing):
  • User receives fixed shares on stake
  • Underlying value grows via exchange_rate_ray
  • No balance updates required per tick
This is gas-efficient and compatible with DeFi protocols that don’t support rebasing tokens (like Aave).

Exchange Rate Formula

underlying = shares * exchange_rate_ray / RAY
shares = underlying * RAY / exchange_rate_ray

Reward Accrual

Rewards accrue linearly per tick (simplified from real beacon chain rewards):
exchange_rate_ray(t+1) = exchange_rate_ray(t) + reward_rate_ray_per_tick
Real Lido uses beacon chain oracle updates (~daily).

Limitations

The Lido simulation does not model:
  • Unstaking delays (real Lido has ~1-5 day withdrawal queue)
  • Slashing events (exchange rate can decrease)
  • stETH vs wstETH distinction (simplified to wstETH only)
  • MEV rewards
  • Post-merge staking dynamics
  • Withdrawal queue mechanics
  • Node operator set
  • DAO governance
  • Oracle committee
Use this model to test yield-bearing collateral strategies, not to predict exact Lido APRs.

Advanced: Deleverage Strategy

When LTV exceeds threshold, unwind position:
// Check LTV
let user_view = aave.view_user(user_id);
let ltv_bps = user_view["ltv_bps"].as_str().unwrap().parse::<u64>()?;

if ltv_bps > target_ltv_bps + band_bps {
    // 1. Withdraw wstETH from Aave (HF-safe amount)
    let withdraw_amount = calculate_safe_withdrawal(...);
    aave.execute(
        ts,
        user_id,
        Action::Custom(json!({
            "kind": "withdraw",
            "token": 3,
            "amount": withdraw_amount.to_string()
        })),
    )?;
    
    // 2. Unstake wstETH → ETH
    lido.execute(
        ts,
        user_id,
        Action::Custom(json!({
            "kind": "unstake",
            "shares": withdraw_amount.to_string()
        })),
    )?;
    
    // 3. Repay ETH debt
    let eth_received = ...; // from unstake output
    aave.execute(
        ts,
        user_id,
        Action::Custom(json!({
            "kind": "repay",
            "token": 1,
            "amount": eth_received.to_string()
        })),
    )?;
}

See Also

Build docs developers (and LLMs) love