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:
Inspect protocol state via view_market() and view_user()
Check wallet balances
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:
Initial deposit : Strategy emits TxBundle with deposit action
Engine routes to Aave protocol via execute()
Aave returns ExecOutcome with delta: {wstETH: -1000e18} (debit wallet)
Engine applies delta to user’s Balances
Next tick : Aave’s on_tick() accrues interest
Strategy inspects view_user() for updated health factor
If LTV too low : Strategy emits borrow → stake → deposit actions
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