Skip to main content
The Aave V3 integration simulates multi-asset lending markets with deposit, borrow, repay, withdraw, and liquidation actions. It includes per-tick variable debt accrual, health factor checks, and fractional liquidations.

Configuration

ReserveConfig

Each token reserve is configured with risk parameters:
pub struct ReserveConfig {
    pub ltv_bps: u16,                    // Loan-to-value in basis points (e.g., 8000 = 80%)
    pub liq_threshold_bps: u16,          // Liquidation threshold (e.g., 8250 = 82.5%)
    pub price_e18: u128,                 // Asset price in 18-decimal format
    pub variable_rate_ray_per_tick: u128, // Interest rate per tick (RAY format)
    pub liq_fraction_bps: u16,           // Legacy, unused
    pub liq_close_factor_bps: u16,       // Max % of debt repayable per liquidation (e.g., 5000 = 50%)
    pub liq_bonus_bps: u16,              // Liquidator bonus (e.g., 500 = 5%)
    pub protocol_fee_bps: u16,           // Protocol fee on seized collateral (e.g., 100 = 1%)
}

Initialization

use ank_protocol_aave_v3::{AaveV3, ReserveConfig};
use ank_accounting::TokenId;

let mut aave = AaveV3::with_token(
    TokenId(1), // ETH
    ReserveConfig {
        ltv_bps: 8000,
        liq_threshold_bps: 8250,
        price_e18: 2000_000000000000000000,
        variable_rate_ray_per_tick: 1_000_000_000_000_000_000_000, // ~0.0001% per tick
        liq_fraction_bps: 5000,
        liq_close_factor_bps: 5000, // 50% max liquidation
        liq_bonus_bps: 500,         // 5% bonus
        protocol_fee_bps: 100,      // 1% protocol fee
    },
    1725000000, // start timestamp
);

// Add more reserves
aave.add_token(
    TokenId(2), // USDC
    ReserveConfig {
        ltv_bps: 8500,
        liq_threshold_bps: 8800,
        price_e18: 1_000000000000000000,
        variable_rate_ray_per_tick: 2_000_000_000_000_000_000_000,
        liq_fraction_bps: 5000,
        liq_close_factor_bps: 5000,
        liq_bonus_bps: 500,
        protocol_fee_bps: 100,
    },
    1725000000,
);

Available Actions

Description: Deposit assets to earn interest and use as collateral.Parameters:
  • token (u32): Token ID to deposit
  • amount (u128): Amount in token units (e18)
Example:
{"kind":"deposit","token":1,"amount":"1000000000000000000000"}
Effects:
  • Deducts amount from wallet
  • Mints scaled deposit shares (scaled by liquidity index)
  • Increases collateral available for borrowing
Gas: ~150,000
Description: Borrow assets against deposited collateral.Parameters:
  • token (u32): Token ID to borrow
  • amount (u128): Amount to borrow
Example:
{"kind":"borrow","token":1,"amount":"500000000000000000000"}
Checks:
  • New total debt must not exceed LTV-adjusted collateral value:
    debt_value + new_borrow_value <= collateral_value * LTV
    
Effects:
  • Credits amount to wallet
  • Increases scaled debt (scaled by variable borrow index)
Gas: ~200,000
Description: Repay borrowed assets.Parameters:
  • token (u32): Token ID to repay
  • amount (u128): Amount to repay (capped to current debt)
Example:
{"kind":"repay","token":1,"amount":"500000000000000000000"}
Effects:
  • Deducts repayment from wallet
  • Reduces scaled debt
Gas: ~180,000
Description: Withdraw deposited assets (with HF check).Parameters:
  • token (u32): Token ID to withdraw
  • amount (u128): Amount to withdraw
Example:
{"kind":"withdraw","token":3,"amount":"100000000000000000000"}
Checks:
  • Post-withdrawal health factor must remain >= 1.0:
    (collateral_adj - withdraw_value_adj) >= debt_value
    
Effects:
  • Credits amount to wallet
  • Reduces scaled deposits
Gas: ~160,000
Withdraw enforces HF >= 1.0. If capped, reduce amount or repay debt first.
Description: Update oracle price for a reserve (admin/oracle action).Parameters:
  • token (u32): Token ID
  • price_e18 (u128): New price in e18 format
Example:
{"kind":"set_price","token":1,"price_e18":"2500000000000000000000"}
Effects:
  • Updates reserve price immediately
  • Used by strategies to sync Lido wstETH price from exchange rate
Gas: ~50,000
Description: Liquidate an undercollateralized position.Parameters:
  • target_user (u32): User ID to liquidate
  • debt_token (u32): Token ID of debt to repay
  • collateral_token (u32): Token ID of collateral to seize
  • repay_units (u128): Desired repay amount
Example:
{
  "kind":"liquidate",
  "target_user":2,
  "debt_token":1,
  "collateral_token":3,
  "repay_units":"100000000000000000000"
}
Mechanics:
  1. Check target HF < 1.0
  2. Cap repay to liq_close_factor_bps of target’s debt
  3. Seize collateral = repay_value * (1 + liq_bonus_bps) / collateral_price
  4. Protocol fee taken from seized collateral → treasury
  5. Liquidator receives remaining seized collateral
Wallet Effects (liquidator):
  • Deduct repay_units of debt token
  • Credit seized collateral (minus protocol fee)
Gas: ~250,000

Interest Accrual

On each tick, reserves accrue interest:
// Variable borrow index compounds per tick
let one_plus_rate = RAY + variable_rate_ray_per_tick;
let factor = ray_pow(one_plus_rate, ticks_elapsed);
variable_borrow_index_ray = ray_mul(variable_borrow_index_ray, factor);

// Liquidity index also compounds (simplified to match borrow)
liquidity_index_ray = ray_mul(liquidity_index_ray, factor);
User debt = scaled_debt * variable_borrow_index_ray User deposits = scaled_deposits * liquidity_index_ray

Health Factor

Health factor determines liquidation eligibility:
HF = (collateral_value_adj / debt_value) * 10_000 // in bps

collateral_value_adj = Σ (deposit_value_i * liq_threshold_bps_i / 10_000)
  • HF >= 10,000 (100%): Healthy position
  • HF < 10,000: Eligible for liquidation

Auto-Liquidation

Aave V3 has an optional auto-liquidation pass that runs after on_tick():
  • Scans all users
  • If HF < 1.0, picks largest debt and largest collateral
  • Liquidates up to liq_close_factor_bps
To disable:
aave.auto_liquidate = false; // via apply_historical with set_auto_liquidate event

Usage Examples

Deposit and Borrow

use ank_protocol::{Action, Protocol};

// Deposit 1000 ETH
aave.execute(
    ts,
    user_id,
    Action::Custom(serde_json::json!({
        "kind": "deposit",
        "token": 1,
        "amount": "1000000000000000000000"
    })),
)?;

// Borrow 700 ETH (70% LTV)
aave.execute(
    ts,
    user_id,
    Action::Custom(serde_json::json!({
        "kind": "borrow",
        "token": 1,
        "amount": "700000000000000000000"
    })),
)?;

Withdraw with HF Check

// Attempt to withdraw wstETH
let result = aave.execute(
    ts,
    user_id,
    Action::Custom(serde_json::json!({
        "kind": "withdraw",
        "token": 3,
        "amount": "100000000000000000000"
    })),
);

match result {
    Ok(_) => println!("Withdraw successful"),
    Err(e) if e.to_string().contains("HF") => {
        // Reduce amount or repay debt first
    }
    Err(e) => return Err(e),
}

Sync Lido wstETH Price

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

// Get ETH price
let eth_price = 2000_000000000000000000u128;

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

// Update Aave oracle
protocols["aave-v3"].execute(
    ts,
    0,
    Action::Custom(serde_json::json!({
        "kind": "set_price",
        "token": 3,
        "price_e18": wsteth_price.to_string()
    })),
)?;

View Methods

view_user

Returns user position:
{
  "deposits": {
    "1": "1000000000000000000000",
    "3": "500000000000000000000"
  },
  "debts": {
    "1": "700000000000000000000"
  },
  "dep_value_e18": "3000000000000000000000",
  "debt_value_e18": "1400000000000000000000",
  "ltv_bps": "4666",
  "hf_bps": "17678"
}

view_market

Returns reserve configs and totals:
{
  "reserves": {
    "1": {
      "ltv_bps": "8000",
      "liq_threshold_bps": "8250",
      "price_e18": "2000000000000000000000",
      "var_borrow_index_ray": "1000000000000000000000000000",
      "rate_ray_per_tick": "1000000000000000000000",
      "liq_close_factor_bps": "5000",
      "liq_bonus_bps": "500",
      "protocol_fee_bps": "100"
    }
  },
  "treasury_collateral": {
    "3": "5000000000000000000"
  },
  "last_events": []
}

Protocol-Specific Behavior

Scaled Balances

Internal accounting uses scaled balances to track shares:
scaled_deposit = amount / liquidity_index_ray
actual_deposit = scaled_deposit * liquidity_index_ray // grows over time

scaled_debt = amount / variable_borrow_index_ray
actual_debt = scaled_debt * variable_borrow_index_ray // grows over time
This allows monotonic interest growth without per-user updates.

Liquidation Math

repay_capped = min(desired_repay, debt * liq_close_factor_bps / 10_000)
seized_value = repay_capped * debt_price * (1 + liq_bonus_bps / 10_000)
seized_collateral = seized_value / collateral_price
protocol_fee_collateral = seized_collateral * protocol_fee_bps / 10_000
liquidator_collateral = seized_collateral - protocol_fee_collateral

Close Factor

Limits single liquidation to X% of debt (e.g., 50%) to prevent full wipeout on small price moves.

Limitations

The Aave V3 simulation does not model:
  • Stable rate borrowing (only variable rate)
  • Isolation mode
  • E-mode (efficiency mode)
  • Flash loans
  • Supply/borrow caps
  • Siloed assets
  • Multi-chain portals
  • Liquidation bonuses varying by asset pair
  • Interest rate strategy curves (rate is fixed per tick)
  • Reserve factor split (deposit yield < borrow cost)
Use this model to test strategy logic and health factor dynamics, not to predict exact mainnet APYs.

See Also

Build docs developers (and LLMs) love