Skip to main content

Overview

The replay system lets you feed protocols with historical chain events (like index bumps, admin changes, liquidity updates) before or during a simulation. This is essential for:
  • Historical backtests using real on-chain data
  • Stress testing with protocol parameter changes
  • Scenario replays (e.g., simulating a past liquidation cascade)

events_csv Format

Create a CSV with columns: ts, target, market_hint, payload_json
ts,target,market_hint,payload_json
1725000000,aave-v3,,"{""kind"":""set_reserve_factor"",""token"":1,""factor_bps"":500}"
1725000100,lido,,"{""kind"":""bump_exchange_rate"",""delta_ray"":1000000000000000000000}"
1725000200,uniswap-v3,WETH/USDC:500,"{""kind"":""admin_set_fee"",""new_fee_bps"":30}"
ColumnTypeDescription
tsu64Unix timestamp (seconds) when event occurs
targetstringProtocol ID (must match Protocol::id())
market_hintstringOptional routing hint (e.g., pool pair)
payload_jsonstringJSON-encoded event payload (protocol-specific)
Ensure payload_json is properly escaped for CSV. Use double-double-quotes ("") inside the JSON string, or use a CSV library to handle escaping.

Loading Events

1

Load CSV into memory

use ank_replay::load_routed_csv;

let events = load_routed_csv("apps/cli/examples/events.csv")?;
// Vec<(String, ChainEvent)>
2

Apply to protocol registry

use ank_replay::apply_to_registry;
use indexmap::IndexMap;
use ank_protocol::Protocol;

let mut protocols: IndexMap<String, Box<dyn Protocol>> = /* ... */;

apply_to_registry(&mut protocols, &events)?;
This calls protocol.apply_historical(ts, market_hint, payload) for each event.

apply_historical Method

Protocols implement this trait method to handle historical events:
use anyhow::Result;
use ank_accounting::Timestamp;

fn apply_historical(
    &mut self,
    ts: Timestamp,
    market_hint: Option<String>,
    payload: serde_json::Value,
) -> Result<()> {
    let kind = payload["kind"].as_str().unwrap_or("");

    match kind {
        "set_reserve_factor" => {
            let token = payload["token"].as_u64().unwrap() as u32;
            let factor_bps = payload["factor_bps"].as_u64().unwrap() as u64;
            self.reserves.get_mut(&token).unwrap().reserve_factor_bps = factor_bps;
        }
        "bump_liquidity_index" => {
            let token = payload["token"].as_u64().unwrap() as u32;
            let delta = payload["delta_ray"].as_str().unwrap().parse::<u128>()?;
            self.reserves.get_mut(&token).unwrap().liquidity_index_ray += delta;
        }
        _ => anyhow::bail!("Unknown event kind: {}", kind),
    }

    Ok(())
}

Event Payloads by Protocol

Aave V3

Set reserve factor:
{
  "kind": "set_reserve_factor",
  "token": 1,
  "factor_bps": 500
}
Bump liquidity index:
{
  "kind": "bump_liquidity_index",
  "token": 1,
  "delta_ray": "1000000000000000000000000000"
}
Admin liquidation threshold change:
{
  "kind": "set_liq_threshold",
  "token": 3,
  "threshold_bps": 7500
}

Lido

Manually bump exchange rate:
{
  "kind": "bump_exchange_rate",
  "delta_ray": "10000000000000000000000"
}
Set new APY:
{
  "kind": "set_apy",
  "apy_bps": 450
}

Uniswap V3

Admin fee change:
{
  "kind": "admin_set_fee",
  "new_fee_bps": 30
}
Inject liquidity (for testing):
{
  "kind": "inject_liquidity",
  "tick_lower": -887220,
  "tick_upper": 887220,
  "liquidity": "100000000000000000000"
}

Per-Tick Replay

Apply events only at specific timestamps during the simulation:
use ank_replay::apply_rows_at_ts;

let events = load_routed_csv("events.csv")?;

// In your tick loop:
for tick in 0..steps {
    let ts = start_ts + tick;

    // Apply any events scheduled for this ts
    apply_rows_at_ts(&mut protocols, &events, ts)?;

    // Run strategy as usual
    engine.tick(user, |ctx, prots, portfolios| {
        // ...
    })?;
}
This ensures events are applied before the strategy runs for that tick.

Example: Historical Lido Rate Bump

Suppose Lido’s exchange rate jumped on 2023-09-01 at 12:00 UTC (ts=1693569600):
ts,target,market_hint,payload_json
1693569600,lido,,"{""kind"":""bump_exchange_rate"",""delta_ray"":""5000000000000000000000000""}"
Load and apply:
let events = load_routed_csv("lido_bump.csv")?;
apply_to_registry(&mut protocols, &events)?;
Now when your simulation reaches ts=1693569600, Lido’s ER will reflect the historical bump.

CSV Generation from Chain Data

Use a script to extract events from blockchain logs:
import pandas as pd
from web3 import Web3

w3 = Web3(Web3.HTTPProvider('https://eth-mainnet.g.alchemy.com/v2/...'))

# Example: Aave ReserveFactorChanged events
ABI = [ /* event ABI */ ]
contract = w3.eth.contract(address='0x...', abi=ABI)

events = contract.events.ReserveFactorChanged.getLogs(
    fromBlock=18000000,
    toBlock=18100000
)

rows = []
for e in events:
    ts = w3.eth.get_block(e['blockNumber'])['timestamp']
    payload = {
        "kind": "set_reserve_factor",
        "token": e['args']['asset'],  # map to your token ID
        "factor_bps": e['args']['newFactor']
    }
    rows.append({
        'ts': ts,
        'target': 'aave-v3',
        'market_hint': '',
        'payload_json': json.dumps(payload)
    })

df = pd.DataFrame(rows)
df.to_csv('aave_events.csv', index=False)

Combining with Price Oracle

Replay both events and prices:
steps: 1000
start_ts: 1693569600
user: 1
log_level: INFO
risk_out_csv: "risk_out.csv"
prices_csv: "historical_prices.csv"   # Price feed
events_csv: "historical_events.csv"   # Protocol events

leverage:
  token: 1
  initial_deposit_units: 10000
  target_ltv_bps: 7000
  band_bps: 250
This gives you a full historical replay with real market conditions and protocol state changes.

Programmatic Event Injection

Manually create ChainEvent structs:
use ank_replay::{ChainEvent, apply_to_registry};
use serde_json::json;

let events = vec![
    (
        "aave-v3".to_string(),
        ChainEvent {
            ts: 1725000000,
            market_hint: None,
            payload: json!({
                "kind": "set_reserve_factor",
                "token": 1,
                "factor_bps": 600
            }),
        },
    ),
    (
        "lido".to_string(),
        ChainEvent {
            ts: 1725000100,
            market_hint: None,
            payload: json!({
                "kind": "bump_exchange_rate",
                "delta_ray": "2000000000000000000000000"
            }),
        },
    ),
];

apply_to_registry(&mut protocols, &events)?;

Troubleshooting

1

Check protocol ID spelling

Ensure target in CSV matches protocol.id() exactly (case-sensitive).
2

Validate JSON syntax

Use a JSON validator on your payload_json column. Common errors: missing quotes, unescaped quotes.
3

Handle missing fields gracefully

Protocols should return descriptive errors if required payload fields are missing:
let token = payload["token"].as_u64()
    .ok_or_else(|| anyhow::anyhow!("Missing 'token' field in payload"))?;

CSV Escaping Helper (Python)

import csv
import json

events = [
    {
        'ts': 1725000000,
        'target': 'aave-v3',
        'market_hint': '',
        'payload_json': json.dumps({"kind": "set_reserve_factor", "token": 1, "factor_bps": 500})
    },
]

with open('events.csv', 'w', newline='') as f:
    writer = csv.DictWriter(f, fieldnames=['ts', 'target', 'market_hint', 'payload_json'])
    writer.writeheader()
    writer.writerows(events)
This auto-escapes JSON quotes correctly.

Next Steps

Build docs developers (and LLMs) love