Skip to main content
ANK is a modular, deterministic on-chain simulator for building and backtesting execution strategies across DeFi protocols. The architecture is designed around three core concepts: Engine, Protocols, and Strategies.

Core Design Principles

The framework is built on these foundational principles:
  • Deterministic simulation: All state changes are reproducible given the same inputs
  • Tick-based execution: Time advances in discrete steps, with state updates at each tick
  • Protocol modularity: Each protocol implements a common trait, making integration seamless
  • Strategy flexibility: Strategies are closures or structs that inspect state and emit actions

Component Overview

┌─────────────────────────────────────────────────────────┐
│                        Engine                           │
│  • Orchestrates tick advancement                        │
│  • Manages user portfolios (Balances)                   │
│  • Routes TxBundles to protocols                        │
│  • Applies balance deltas and fees                      │
└────────────┬────────────────────────────┬───────────────┘
             │                            │
             │                            │
        ┌────▼────┐                  ┌────▼────┐
        │Protocol │                  │Protocol │
        │ Aave V3 │                  │  Lido   │
        └─────────┘                  └─────────┘
             ▲                            ▲
             │                            │
             └────────────┬───────────────┘

                    ┌─────▼──────┐
                    │  Strategy  │
                    │   Planner  │
                    └────────────┘

Engine

The Engine (core/engine/) is the central orchestrator that:
  • Maintains a registry of protocol instances (IndexMap<String, Box<dyn Protocol>>)
  • Tracks per-user portfolios as token balances (IndexMap<UserId, Balances>)
  • Advances time through ticks with a monotonic counter
  • Executes transaction bundles and applies balance deltas
  • Optionally models gas fees with configurable policies
The engine is stateless regarding protocol internals. It only knows about balances and the Protocol trait interface.

Protocols

Each Protocol (core/protocol/) implements the Protocol trait:
pub trait Protocol: Send + Sync {
    fn id(&self) -> &'static str;
    fn execute(&mut self, ts: Timestamp, user: UserId, action: Action) -> Result<ExecOutcome>;
    fn on_tick(&mut self, ts: Timestamp) -> Result<()>;
    fn view_user(&self, user: UserId) -> serde_json::Value;
    fn view_market(&self) -> serde_json::Value;
}
Available protocols:
  • Aave V3: Multi-asset lending with deposits, borrows, withdrawals, liquidations, and dynamic pricing
  • Lido: wstETH staking/unstaking with exchange rate growth per tick
  • Uniswap V3: Simple fixed-fee pool for swaps and price impact testing
  • Pendle: Full PT/YT yield tokenization with SY wrapper and AMM

Strategies

A Strategy is code that runs each tick to:
  1. Inspect protocol state via view_market() and view_user()
  2. Check wallet balances
  3. Emit a Vec<TxBundle> containing actions to execute
Strategies can be simple closures or structured types. The closure-based pattern:
let mut planner = move |ctx: EngineCtx,
                        prots: &IndexMap<String, Box<dyn Protocol>>,
                        portfolios: &IndexMap<UserId, Balances>| -> Vec<TxBundle> {
    // Decision logic here
    vec![TxBundle { txs: vec![...] }]
};

Tick-Based Execution Model

ANK simulates time through discrete ticks. Each tick represents a fixed time interval (e.g., 1 second, 1 block).

Tick Lifecycle

┌─────────────────────────────────────────────────────────┐
│                    Tick N                               │
├─────────────────────────────────────────────────────────┤
│ 1. Call on_tick() on all protocols                      │
│    → Interest accrual, rate updates, etc.               │
├─────────────────────────────────────────────────────────┤
│ 2. Call strategy planner with EngineCtx                 │
│    → Returns Vec<TxBundle>                              │
├─────────────────────────────────────────────────────────┤
│ 3. Execute each bundle:                                 │
│    a. PreTx callback (optional gas precharge)           │
│    b. Protocol.execute(action) → ExecOutcome            │
│    c. PostTx callback (apply delta + fees)              │
├─────────────────────────────────────────────────────────┤
│ 4. Increment step_idx and ts                            │
└─────────────────────────────────────────────────────────┘

EngineCtx

Every tick, the strategy receives an EngineCtx:
pub struct EngineCtx {
    pub ts: Timestamp,      // Current block/timestamp
    pub step_idx: u64,      // Monotonic tick counter
}
This allows strategies to make time-aware decisions (e.g., cooldowns, expiries).

Token Convention

By convention, token IDs are assigned as:
// Core tokens
TokenId(1) = ETH
TokenId(2) = USDC
TokenId(3) = wstETH

// Pendle-specific
TokenId(4) = SY (Standardized Yield)
TokenId(5) = PT (Principal Token)
TokenId(6) = YT (Yield Token)
TokenId(7) = LP (Liquidity Pool shares)
Token IDs are purely internal identifiers. They don’t correspond to on-chain addresses.

Repository Structure

core/
  accounting/     # Balances, TokenId, Amount primitives
  engine/         # Engine orchestration and tick execution
  exec/           # Stateless Tx/TxBundle execution helpers
  protocol/       # Protocol trait definition
  math/           # Ray/WAD math utilities
  risk/           # Risk metrics and CSV output
  oracle/         # Price feed management
  replay/         # Historical event replay

protocols/
  aave-v3/        # Aave V3 protocol implementation
  lido/           # Lido staking protocol
  uniswap-v3/     # Uniswap V3 swap pool
  pendle/         # Pendle yield tokenization

apps/
  api/            # HTTP API with backtest endpoints

Data Flow Example

Here’s how a simple leverage strategy executes:
  1. Initial deposit: Strategy emits TxBundle with deposit action
  2. Engine routes to Aave protocol via execute()
  3. Aave returns ExecOutcome with delta: {wstETH: -1000e18} (debit wallet)
  4. Engine applies delta to user’s Balances
  5. Next tick: Aave’s on_tick() accrues interest
  6. Strategy inspects view_user() for updated health factor
  7. If LTV too low: Strategy emits borrow → stake → deposit actions
  8. Repeat for each tick

Next Steps

Engine

Learn about Engine orchestration and tick execution

Protocols

Understand the Protocol trait and integration system

Strategies

Develop custom strategies and planners

Accounting

Master balance primitives and delta application

Build docs developers (and LLMs) love