Skip to main content

Overview

ANK strategies are closures that inspect protocol and wallet state each tick, then emit TxBundle actions. This guide shows you how to build custom strategies that interact with multiple protocols.

Strategy Pattern

A strategy is a closure with this signature:
let mut planner = move |ctx: EngineCtx,
                        prots: &IndexMap<String, Box<dyn Protocol>>,
                        portfolios: &IndexMap<UserId, Balances>| -> Vec<TxBundle> {
  // Your strategy logic here
};

Context Parameters

  • ctx — Contains ts (timestamp) and step_idx (tick counter)
  • prots — Registry of all protocol instances (e.g., "aave-v3", "lido", "uniswap-v3")
  • portfolios — Map of user wallets (UserId → Balances)

Accessing Protocol Views

Protocols expose read-only views for decision-making:
1

Query market state

Use view_market() to get protocol-wide data:
let aave_mkt = prots["aave-v3"].view_market();
let reserves = aave_mkt["reserves"].as_object().unwrap();
let eth_price = reserves["ETH"]["price_e18"].as_str().unwrap().parse::<u128>()?;
2

Query user positions

Use view_user(user_id) to inspect deposits/debts:
let aave_user = prots["aave-v3"].view_user(UserId(1));
let deposits = aave_user["deposits"].as_object().unwrap();
let wsteth_deposited = deposits.get("wstETH")
  .and_then(|v| v.as_str())
  .and_then(|s| s.parse::<u128>().ok())
  .unwrap_or(0);
3

Compute risk metrics

Use helper functions from ank_risk:
use ank_risk::compute_aave_values_from_views;

let (deposit_val, debt_val, ltv_bps, hf_bps) = 
  compute_aave_values_from_views(&aave_user, &aave_mkt);

// hf_bps > 10000 = healthy (HF > 1.0)
// ltv_bps = loan-to-value in basis points (7000 = 70%)

Emitting TxBundles

Actions are organized into bundles of transactions:
use ank_exec::{Tx, TxBundle};
use serde_json::json;

let mut txs = Vec::new();

// Deposit to Aave
txs.push(Tx {
  protocol: "aave-v3".into(),
  action: Action::Custom(json!({
    "kind": "deposit",
    "token": 3,  // wstETH
    "amount": "5000000000000000000000"  // 5000 units
  })),
  gas_limit: Some(300_000),
});

// Borrow ETH
txs.push(Tx {
  protocol: "aave-v3".into(),
  action: Action::Custom(json!({
    "kind": "borrow",
    "token": 1,  // ETH
    "amount": "2000000000000000000000"  // 2000 units
  })),
  gas_limit: Some(350_000),
});

vec![TxBundle { txs }]

Complete Example: Cross-Protocol Leverage

Here’s a full strategy that loops ETH borrowing through Lido staking and Aave deposits:
use ank_accounting::{UserId, TokenId};
use ank_engine::EngineCtx;
use ank_exec::{Tx, TxBundle, Action};
use ank_protocol::Protocol;
use ank_risk::compute_aave_values_from_views;
use indexmap::IndexMap;
use serde_json::json;

let target_ltv_bps = 7000u64;  // 70%
let band_bps = 250u64;         // ±2.5%
let user = UserId(1);

let mut planner = move |ctx: EngineCtx,
                        prots: &IndexMap<String, Box<dyn Protocol>>,
                        portfolios: &IndexMap<UserId, Balances>| -> Vec<TxBundle> {
  let mut txs = Vec::new();
  let wallet = portfolios.get(&user).cloned().unwrap_or_default();

  // Read Aave state
  let aave_user = prots["aave-v3"].view_user(user);
  let aave_mkt = prots["aave-v3"].view_market();
  let (dep_val, debt_val, ltv_bps, hf_bps) = 
    compute_aave_values_from_views(&aave_user, &aave_mkt);

  // First tick: stake and deposit
  if ctx.step_idx == 0 {
    let initial = 10000u128 * 1_000_000_000_000_000_000; // 10k ETH
    txs.push(Tx {
      protocol: "lido".into(),
      action: Action::Custom(json!({"kind":"stake","amount":initial.to_string()})),
      gas_limit: Some(200_000),
    });
    txs.push(Tx {
      protocol: "aave-v3".into(),
      action: Action::Custom(json!({"kind":"deposit","token":3,"amount":initial.to_string()})),
      gas_limit: Some(300_000),
    });
    return vec![TxBundle { txs }];
  }

  // Sync Lido wstETH price to Aave
  let lido_mkt = prots["lido"].view_market();
  let er = lido_mkt["exchange_rate_ray"].as_str().unwrap().parse::<u128>().unwrap();
  let eth_px = aave_mkt["reserves"]["ETH"]["price_e18"].as_str().unwrap().parse::<u128>().unwrap();
  let wsteth_price = (er as u128 * eth_px) / 1_000_000_000_000_000_000_000_000_000; // ray → e18
  txs.push(Tx {
    protocol: "aave-v3".into(),
    action: Action::Custom(json!({"kind":"set_price","token":3,"price_e18":wsteth_price.to_string()})),
    gas_limit: None,
  });

  // Rebalance logic
  if ltv_bps < target_ltv_bps - band_bps {
    // Leverage up: borrow more ETH
    let gap = (target_ltv_bps - ltv_bps) as u128;
    let borrow_amt = (dep_val * gap) / 10_000;
    txs.push(Tx {
      protocol: "aave-v3".into(),
      action: Action::Custom(json!({"kind":"borrow","token":1,"amount":borrow_amt.to_string()})),
      gas_limit: Some(350_000),
    });
    // Stake borrowed ETH → deposit next tick
    txs.push(Tx {
      protocol: "lido".into(),
      action: Action::Custom(json!({"kind":"stake","amount":borrow_amt.to_string()})),
      gas_limit: Some(200_000),
    });
  } else if ltv_bps > target_ltv_bps + band_bps {
    // Deleverage: repay debt
    let excess = (ltv_bps - target_ltv_bps) as u128;
    let repay_amt = (debt_val * excess) / 10_000;
    let wallet_eth = wallet.get(TokenId(1));
    if wallet_eth < repay_amt {
      // Withdraw wstETH and unstake to get ETH
      let shortfall = repay_amt - wallet_eth;
      txs.push(Tx {
        protocol: "aave-v3".into(),
        action: Action::Custom(json!({"kind":"withdraw","token":3,"amount":shortfall.to_string()})),
        gas_limit: Some(400_000),
      });
      txs.push(Tx {
        protocol: "lido".into(),
        action: Action::Custom(json!({"kind":"unstake","amount":shortfall.to_string()})),
        gas_limit: Some(150_000),
      });
    }
    txs.push(Tx {
      protocol: "aave-v3".into(),
      action: Action::Custom(json!({"kind":"repay","token":1,"amount":repay_amt.to_string()})),
      gas_limit: Some(300_000),
    });
  }

  vec![TxBundle { txs }]
};

Token Conventions

By convention, ANK uses integer token IDs:
  • 1 = ETH
  • 2 = USDC
  • 3 = wstETH
  • 4–7 = Pendle (SY, PT, YT, LP)
Refer to your protocol configs to map these IDs.

Common Actions

Aave V3

{"kind":"deposit","token":1,"amount":"1000000000000000000000"}
{"kind":"borrow","token":2,"amount":"500000000000000000000"}
{"kind":"repay","token":2,"amount":"500000000000000000000"}
{"kind":"withdraw","token":1,"amount":"1000000000000000000000"}
{"kind":"set_price","token":1,"price_e18":"2000000000000000000000"}

Lido

{"kind":"stake","amount":"10000000000000000000000"}
{"kind":"unstake","amount":"5000000000000000000000"}
{"kind":"wrap","amount":"5000000000000000000000"}

Uniswap V3

{"kind":"swap_exact_in","token_in":1,"token_out":2,"amount_in":"1000000000000000000"}
Use gas_limit: None for admin actions like set_price that don’t consume real gas.

Next Steps

For production-grade strategies, consider extracting your closure into a Strategy trait:
pub trait Strategy {
  fn on_step(
    &mut self,
    ctx: EngineCtx,
    prots: &IndexMap<String, Box<dyn Protocol>>,
    portfolios: &IndexMap<UserId, Balances>
  ) -> Vec<TxBundle>;
}
This enables a strategy registry where multiple strategies can be selected via YAML config.

Build docs developers (and LLMs) love