Skip to main content
The ank-accounting crate (core/accounting/) defines the fundamental types for tracking token balances and applying state changes. All amounts use 1e18 scaling (like wei in Ethereum).

Core Types

From core/accounting/src/lib.rs:
pub type Timestamp = u64;     // Logical time unit (tick index or Unix seconds)
pub type UserId = u64;        // Internal user identifier
pub type Amount = u128;       // 1e18-scaled unsigned quantity

TokenId

A unique identifier for a fungible token:
pub struct TokenId(pub u32);
Example usage:
use ank_accounting::TokenId;

let eth = TokenId(1);
let usdc = TokenId(2);
let wsteth = TokenId(3);
Token IDs are arbitrary integers. The convention in ANK is: 1 = ETH, 2 = USDC, 3 = wstETH, etc.

Balances

Per-user token balances, represented as a map from TokenId to Amount:
pub struct Balances(pub IndexMap<TokenId, Amount>);
Key methods (from core/accounting/src/lib.rs:core/accounting/src/lib.rs:98-164):
impl Balances {
    pub fn get(&self, t: TokenId) -> Amount;  // Returns 0 if token absent
    pub fn set(&mut self, t: TokenId, v: Amount);
    pub fn apply_delta(&mut self, delta: &BalancesDelta) -> Result<()>;
}
Example:
use ank_accounting::{Balances, TokenId};
use indexmap::IndexMap;

let mut wallet = Balances(IndexMap::new());
wallet.set(TokenId(1), 10_000_000_000_000_000_000_000u128);  // 10000 ETH (10000e18)

let eth_balance = wallet.get(TokenId(1));
assert_eq!(eth_balance, 10_000_000_000_000_000_000_000u128);

// Non-existent token returns 0
let usdc_balance = wallet.get(TokenId(2));
assert_eq!(usdc_balance, 0);

BalancesDelta

Signed changes to balances, represented as a map from TokenId to i128:
pub struct BalancesDelta(pub IndexMap<TokenId, i128>);
  • Positive values: Credit the user (add to balance)
  • Negative values: Debit the user (subtract from balance)
Example:
use ank_accounting::{BalancesDelta, TokenId};
use indexmap::IndexMap;

let mut delta = BalancesDelta(IndexMap::new());
delta.0.insert(TokenId(1), -1_000_000_000_000_000_000i128);  // Debit 1 ETH
delta.0.insert(TokenId(2), 2_000_000i128);                   // Credit 2 USDC (assuming 1e6 decimals, but stored as 1e18)
All amounts in ANK are 1e18-scaled, regardless of the token’s real decimals. Convert externally if needed.

The 1e18 Scaling Convention

All token amounts in ANK are represented as 18-decimal fixed-point integers:
1 token = 1_000_000_000_000_000_000 (1e18)
0.5 token = 500_000_000_000_000_000 (5e17)
1000 tokens = 1_000_000_000_000_000_000_000 (1000e18)

Why 1e18?

  • Precision: Avoids floating-point errors in financial calculations
  • Ethereum compatibility: Matches wei (1 ETH = 1e18 wei)
  • Protocol consistency: Aave, Lido, and other protocols use Ray (1e27) or WAD (1e18) internally

Converting to 1e18

// Human-readable amount → 1e18
let human = 1000.5;  // 1000.5 ETH
let amount_e18 = (human * 1e18) as u128;
assert_eq!(amount_e18, 1_000_500_000_000_000_000_000u128);

// 1e18 → human-readable
let amount_e18 = 1_000_500_000_000_000_000_000u128;
let human = (amount_e18 as f64) / 1e18;
assert_eq!(human, 1000.5);
Use string parsing for large amounts to avoid precision loss:
let amount_e18: u128 = "1000500000000000000000".parse().unwrap();

Applying Deltas

The apply_delta() method updates balances based on a BalancesDelta:
pub fn apply_delta(&mut self, delta: &BalancesDelta) -> Result<()>
Logic (from core/accounting/src/lib.rs:core/accounting/src/lib.rs:149-163):
for (tok, d) in delta.0.iter() {
    let cur = self.get(*tok);
    if *d >= 0 {
        let add = *d as u128;
        let newv = cur.saturating_add(add);  // Add with overflow protection
        self.set(*tok, newv);
    } else {
        let sub = (-*d) as u128;
        let newv = cur.saturating_sub(sub);  // Subtract with underflow protection (saturates at 0)
        self.set(*tok, newv);
    }
}
Key behavior: Subtractions saturate at zero (no underflow panic).

Example: Delta Application

use ank_accounting::{Balances, BalancesDelta, TokenId};
use indexmap::IndexMap;

let mut wallet = Balances(IndexMap::new());
wallet.set(TokenId(1), 10_000_000_000_000_000_000u128);  // 10 ETH

// Create delta: debit 5 ETH, credit 100 USDC
let mut delta = BalancesDelta(IndexMap::new());
delta.0.insert(TokenId(1), -5_000_000_000_000_000_000i128);  // -5 ETH
delta.0.insert(TokenId(2), 100_000_000_000_000_000_000i128);  // +100 USDC

wallet.apply_delta(&delta).unwrap();

assert_eq!(wallet.get(TokenId(1)), 5_000_000_000_000_000_000u128);  // 5 ETH left
assert_eq!(wallet.get(TokenId(2)), 100_000_000_000_000_000_000u128);  // 100 USDC added

Saturation Behavior

let mut wallet = Balances(IndexMap::new());
wallet.set(TokenId(1), 1_000_000_000_000_000_000u128);  // 1 ETH

// Try to debit 10 ETH (more than available)
let mut delta = BalancesDelta(IndexMap::new());
delta.0.insert(TokenId(1), -10_000_000_000_000_000_000i128);  // -10 ETH

wallet.apply_delta(&delta).unwrap();

assert_eq!(wallet.get(TokenId(1)), 0);  // Saturates at 0 (no panic)
Saturation prevents panics but can mask logic errors. Check balances before applying large negative deltas.

Protocol Integration

Protocols return BalancesDelta in ExecOutcome:
use ank_protocol::{ExecOutcome, Event};
use ank_accounting::{BalancesDelta, TokenId};

// Example: User deposits 1000 wstETH to Aave
let mut delta = BalancesDelta(IndexMap::new());
delta.0.insert(TokenId(3), -1_000_000_000_000_000_000_000i128);  // Debit 1000 wstETH

let outcome = ExecOutcome {
    delta,
    gas_used: 180_000u64.into(),
    events: vec![Event::Info("Deposited 1000 wstETH".into())],
};
The engine then applies this delta to the user’s wallet:
engine.portfolios.entry(user).or_default().apply_delta(&outcome.delta)?;

TypeScript Bindings

When the ts-bindings feature is enabled, types are exported to TypeScript:

Balances

type Balances = Record<number, string>;  // token ID → amount (as decimal string)

BalancesDelta

type BalancesDelta = Record<number, string>;  // token ID → delta (as decimal string)
Example JSON:
{
  "1": "10000000000000000000000",   // 10000 ETH
  "2": "500000000000000000000000"   // 500000 USDC
}
Amounts are serialized as strings to avoid JavaScript’s Number.MAX_SAFE_INTEGER limit (2^53 - 1).

Working with Amounts

Formatting for Display

fn format_amount(amount: u128, decimals: u32) -> String {
    let divisor = 10u128.pow(decimals);
    let whole = amount / divisor;
    let frac = amount % divisor;
    format!("{}.{:0width$}", whole, frac, width = decimals as usize)
}

let eth = 1_234_567_890_123_456_789u128;
println!("{} ETH", format_amount(eth, 18));  // "1.234567890123456789 ETH"

Parsing from Strings

fn parse_amount(s: &str, decimals: u32) -> Result<u128> {
    let parts: Vec<&str> = s.split('.').collect();
    let whole: u128 = parts[0].parse()?;
    let frac: u128 = if parts.len() > 1 {
        let frac_str = format!("{:0<width$}", parts[1], width = decimals as usize);
        frac_str[..decimals as usize].parse()?
    } else {
        0
    };
    Ok(whole * 10u128.pow(decimals) + frac)
}

let amount = parse_amount("1000.5", 18)?;
assert_eq!(amount, 1_000_500_000_000_000_000_000u128);

Math Operations

// Multiply by percentage (basis points)
fn pct_bps(amount: u128, bps: u64) -> u128 {
    (amount * bps as u128) / 10_000
}

let collateral = 10_000_000_000_000_000_000_000u128;  // 10000e18
let ltv_70_pct = pct_bps(collateral, 7000);  // 70%
assert_eq!(ltv_70_pct, 7_000_000_000_000_000_000_000u128);  // 7000e18
// Divide with rounding up (useful for repayments)
fn div_up(amount: u128, divisor: u128) -> u128 {
    (amount + divisor - 1) / divisor
}

let debt_value = 12_345_000_000_000_000_000_000u128;
let token_price = 2_000_000_000_000_000_000u128;  // 2e18
let tokens_needed = div_up(debt_value, token_price);

Common Pitfalls

Off-by-one decimal errors: Always double-check that amounts are in 1e18, not 1e6 or raw integers.
Integer overflow: When multiplying large amounts, use saturating_mul() or checked_mul() to avoid panics.
Negative balances: apply_delta() saturates at 0, so protocols should check balances before emitting debits.

Best Practices

Use constants: Define token IDs as constants for readability:
const ETH: TokenId = TokenId(1);
const USDC: TokenId = TokenId(2);
Immutable snapshots: Use engine.balances(user) for cloned snapshots; use engine.balances_mut(user) only when modifying.
Log deltas: Emit Event::Info with delta details for debugging:
events.push(Event::Info(format!("Delta: {:?}", delta.0)));

Example: Building a Delta

use ank_accounting::{BalancesDelta, TokenId};
use indexmap::IndexMap;

// Swap: user gives 1000 ETH, receives 2000 USDC
let mut delta = BalancesDelta(IndexMap::new());
delta.0.insert(TokenId(1), -1_000_000_000_000_000_000_000i128);  // Debit 1000 ETH
delta.0.insert(TokenId(2), 2_000_000_000_000_000_000_000i128);   // Credit 2000 USDC

// Apply to user's wallet
wallet.apply_delta(&delta)?;

Example: Multi-Token Ledger

use ank_accounting::{Balances, TokenId};
use indexmap::IndexMap;

let mut portfolio = Balances(IndexMap::new());
portfolio.set(TokenId(1), 100_000_000_000_000_000_000u128);  // 100 ETH
portfolio.set(TokenId(2), 50_000_000_000_000_000_000_000u128);  // 50000 USDC
portfolio.set(TokenId(3), 10_000_000_000_000_000_000u128);  // 10 wstETH

// Iterate over balances
for (token, amount) in portfolio.0.iter() {
    println!("Token {}: {}", token.0, amount);
}

Engine

See how the engine manages portfolios

Protocols

Learn how protocols emit BalancesDelta

Strategies

Use balances in strategy decision-making

Build docs developers (and LLMs) love