Skip to main content

Overview

The ank-protocol module defines the minimal surface for integrating DeFi protocols with the engine/runtime. It provides the core types and traits that all protocol implementations must use.

Core Types

Action

pub enum Action {
    Noop,
    Custom(serde_json::Value),
}
An instruction sent to a protocol. This enum keeps the core protocol surface format-agnostic.
Noop
variant
No operation (useful for smoke tests or heartbeat calls)
Custom
serde_json::Value
Arbitrary JSON payload (protocol-specific schema). In TypeScript this is emitted as any; individual SDKs can provide strongly-typed builders around it.

Event

pub enum Event {
    Info(String),
    Custom(serde_json::Value),
}
An event emitted by a protocol during execution.
Info
String
Human-readable note (for logs/inspection)
Custom
serde_json::Value
Structured/opaque event payload

ExecOutcome

pub struct ExecOutcome {
    pub delta: BalancesDelta,
    pub gas_used: U64S,
    pub events: Vec<Event>,
}
Result of executing a single Action on a protocol.
delta
BalancesDelta
required
Balance updates in e18 units: token → i128 (serialized as strings in TS). Positive values credit the user; negatives debit the user. This delta is applied by the engine to the user’s balances after execution (subject to engine policies/fees).
gas_used
U64S
required
Gas consumed for this action (protocol’s estimate/measurement). Wrapped in U64S so JSON/TS use string representation (safe for JS).
events
Vec<Event>
Optional events emitted during execution

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 view_user(&self, user: UserId) -> serde_json::Value;
    fn view_market(&self) -> serde_json::Value;
    fn apply_historical(&mut self, ts: Timestamp, market_hint: Option<String>, payload: serde_json::Value) -> Result<()>;
    fn on_tick(&mut self, ts: Timestamp) -> Result<()>;
}
Trait that integrates a protocol with the engine. A protocol implementation must be Send + Sync so it can be hosted by multithreaded runners.

Methods

id

fn id(&self) -> &'static str
Stable identifier for the protocol (e.g., "aave-v3", "uniswap-v3").
return
&'static str
The protocol identifier

execute

fn execute(&mut self, ts: Timestamp, user: UserId, action: Action) -> Result<ExecOutcome>
Execute an Action at timestamp ts on behalf of user, returning an ExecOutcome. The implementor is responsible for:
  • Validating and applying the action to internal state
  • Computing the resulting balance delta
  • Estimating/measuring gas_used
  • Emitting any events of interest
ts
Timestamp
required
Simulation timestamp (e.g., block index)
user
UserId
required
User identifier executing the action
action
Action
required
The action to execute
return
Result<ExecOutcome>
The execution outcome including balance deltas, gas used, and events

view_user

fn view_user(&self, user: UserId) -> serde_json::Value
Optional per-user read-only view (free-shape JSON). Useful for UI/debugging and agent decision-making. Default implementation returns an empty object {}.
user
UserId
required
User identifier to query
return
serde_json::Value
Free-form JSON data representing the user’s state in this protocol

view_market

fn view_market(&self) -> serde_json::Value
Optional market-wide read-only view (free-shape JSON). Should be cheap to compute; cached if necessary. Default implementation returns an empty object {}.
return
serde_json::Value
Free-form JSON data representing the protocol’s market state

apply_historical

fn apply_historical(
    &mut self,
    ts: Timestamp,
    market_hint: Option<String>,
    payload: serde_json::Value,
) -> Result<()>
Apply historical data (e.g., chain events, exogenous updates). Engines/replayers can call this to feed protocol state with externally sourced data at a given timestamp. Default implementation is a no-op.
ts
Timestamp
required
Timestamp for the historical data
market_hint
Option<String>
Optional market identifier hint
payload
serde_json::Value
required
Historical data payload

on_tick

fn on_tick(&mut self, ts: Timestamp) -> Result<()>
Per-tick callback to advance protocol state (funding, interest, decay, etc.). The engine calls this once per global tick. Default implementation is a no-op.
ts
Timestamp
required
Current simulation timestamp

Example Implementation

use anyhow::Result;
use ank_accounting::{BalancesDelta, Timestamp, UserId, TokenId};
use ank_protocol::{Action, ExecOutcome, Event, Protocol};

struct MyProto;

impl Protocol for MyProto {
    fn id(&self) -> &'static str {
        "my-proto"
    }

    fn execute(&mut self, _ts: Timestamp, _user: UserId, action: Action) -> Result<ExecOutcome> {
        // Parse your action here; this example just credits token 0 by 1e18
        let mut delta = BalancesDelta::default();
        delta.0.insert(TokenId(0), 1_000_000_000_000_000_000i128);
        
        Ok(ExecOutcome {
            delta,
            gas_used: 210_000u64.into(), // wrap into U64S
            events: vec![Event::Info(format!("ok: {:?}", action))],
        })
    }
}

Notes

  • Amounts and balance updates are carried via BalancesDelta (token → i128, serialized as strings in TS/JSON)
  • Gas usage is modeled as U64S (a u64 serialized as string) to avoid precision loss in JS runtimes

Build docs developers (and LLMs) love