Skip to main content
ANK provides pluggable protocol integrations through a common Protocol trait, enabling deterministic simulation of complex DeFi strategies across multiple protocols.

Supported Protocols

Aave V3

Multi-asset lending with variable debt accrual, liquidations, and dynamic pricing

Lido wstETH

Liquid staking with exchange rate growth and auto-sync with Aave collateral

Uniswap V3

Concentrated liquidity AMM with position management and swaps

Pendle

Principal and yield token splitting with V2 AMM

Hyperliquid

Perpetual futures with margin trading, funding rates, and liquidations

Protocol Trait

All protocols implement a common interface:
pub trait Protocol {
    fn id(&self) -> &'static str;
    fn on_tick(&mut self, ts: Timestamp) -> Result<()>;
    fn execute(&mut self, ts: Timestamp, user: UserId, action: Action) -> Result<ExecOutcome>;
    fn view_user(&self, user: UserId) -> serde_json::Value;
    fn view_market(&self) -> serde_json::Value;
    fn apply_historical(&mut self, ts: Timestamp, src: Option<String>, payload: serde_json::Value) -> Result<()>;
}

Key Methods

  • on_tick: Called each simulation tick to accrue interest, update rates, apply funding, etc.
  • execute: Routes user actions (deposits, borrows, swaps) and returns balance deltas
  • view_user: Returns user position state (deposits, debts, shares, etc.)
  • view_market: Returns protocol-level state (prices, rates, liquidity, etc.)
  • apply_historical: Replays historical events to mutate protocol state before simulation

Registry Setup

Protocols are registered in a map and routed by the Engine:
use indexmap::IndexMap;
use ank_protocol::Protocol;

let mut protocols: IndexMap<String, Box<dyn Protocol>> = IndexMap::new();

// Aave V3
let aave = ank_protocol_aave_v3::AaveV3::with_token(
    TokenId(1), // ETH
    ank_protocol_aave_v3::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,
        liq_fraction_bps: 5000,
        liq_close_factor_bps: 5000,
        liq_bonus_bps: 500,
        protocol_fee_bps: 100,
    },
    start_ts,
);
protocols.insert("aave-v3".into(), Box::new(aave));

// Lido
let lido = ank_protocol_lido::Lido::new(
    ank_protocol_lido::LidoConfig {
        token_in: TokenId(1),  // ETH
        token_out: TokenId(3), // wstETH
        reward_rate_ray_per_tick: 1_000_000_000_000_000_000_000,
    },
);
protocols.insert("lido".into(), Box::new(lido));

// Uniswap V3
let uni = ank_protocol_uniswap_v3::Uniswapv3::new(
    ank_protocol_uniswap_v3::PoolConfig {
        token0: TokenId(1),
        token1: TokenId(2),
        fee_bps: 30,
        tick_spacing: 60,
    },
);
protocols.insert("uniswap-v3".into(), Box::new(uni));

// Pendle
let pendle = ank_protocol_pendle::Pendle::new(
    ank_protocol_pendle::PendleConfig {
        token_underlying: TokenId(1),
        token_sy: TokenId(4),
        token_pt: TokenId(5),
        token_yt: TokenId(6),
        token_lp: TokenId(7),
        yield_rate_ray_per_tick: 1_000_000_000_000_000_000_000,
        series_start_ts: start_ts,
        maturity_ts: start_ts + 86400 * 90,
        scalar_root: 5.0,
        fee_root: 0.001,
        anchor_init: 1.0,
        amm_fee_bps: 30,
    },
);
protocols.insert("pendle".into(), Box::new(pendle));

// Hyperliquid
let hyper = ank_protocol_hyperliquid::Hyperliquid::new(
    ank_protocol_hyperliquid::HyperConfig {
        token_quote: TokenId(2), // USDC
        taker_fee_bps: 5,
        maint_margin_bps: 300,
        liq_fee_bps: 100,
    },
    2000_000000000000000000, // initial ETH price
);
protocols.insert("hyperliquid".into(), Box::new(hyper));

Token Conventions

By convention, token IDs are:
  • 1 = ETH
  • 2 = USDC
  • 3 = wstETH (Lido)
  • 4 = SY (Pendle)
  • 5 = PT (Pendle)
  • 6 = YT (Pendle)
  • 7 = LP (Pendle)

Cross-Protocol Strategies

The modular design enables complex cross-protocol strategies:
// Example: Lido → Aave leverage loop
let mut planner = move |ctx, prots, portfolios| -> Vec<TxBundle> {
    let mut txs = vec![];
    
    // 1. Stake ETH in Lido to get wstETH
    txs.push(Tx {
        protocol: "lido".into(),
        action: Action::Custom(json!({"kind":"stake","amount":"1000"})),
    });
    
    // 2. Deposit wstETH to Aave as collateral
    txs.push(Tx {
        protocol: "aave-v3".into(),
        action: Action::Custom(json!({"kind":"deposit","token":3,"amount":"1000"})),
    });
    
    // 3. Borrow ETH against wstETH
    txs.push(Tx {
        protocol: "aave-v3".into(),
        action: Action::Custom(json!({"kind":"borrow","token":1,"amount":"700"})),
    });
    
    vec![TxBundle { user: 1, txs }]
};

Simulation Lifecycle

  1. Initialization: Protocols are configured and registered
  2. Per-tick:
    • Engine calls on_tick() for each protocol (accrual, funding, etc.)
    • Strategy planner inspects state via view_user()/view_market()
    • Engine routes TxBundle actions to protocols via execute()
    • Balance deltas are applied to user wallets
  3. Replay (optional): apply_historical() mutates protocol state from CSV events

Determinism

All protocols are deterministic:
  • Time-based accrual is tick-driven (not wall-clock)
  • No external oracle calls during simulation
  • Prices are set explicitly via actions or oracle CSV
  • RNG is not used; all math is fixed-point (RAY/WAD)

Limitations

These are simplified protocol models for backtesting. They do not simulate:
  • MEV/front-running
  • Gas costs (tracked symbolically only)
  • Oracle latency
  • Multi-block atomicity
  • Slippage beyond AMM math
  • Flash loan attacks
  • Governance changes
  • Multi-chain bridge risks
Use ANK to explore strategy logic and risk metrics, not for production deployment without validation against mainnet conditions.

Build docs developers (and LLMs) love