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_000 u128 ); // 10000 ETH (10000e18)
let eth_balance = wallet . get ( TokenId ( 1 ));
assert_eq! ( eth_balance , 10_000_000_000_000_000_000_000 u128 );
// 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_000 i128 ); // Debit 1 ETH
delta . 0. insert ( TokenId ( 2 ), 2_000_000 i128 ); // 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 * 1 e 18 ) as u128 ;
assert_eq! ( amount_e18 , 1_000_500_000_000_000_000_000 u128 );
// 1e18 → human-readable
let amount_e18 = 1_000_500_000_000_000_000_000 u128 ;
let human = ( amount_e18 as f64 ) / 1 e 18 ;
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_000 u128 ); // 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_000 i128 ); // -5 ETH
delta . 0. insert ( TokenId ( 2 ), 100_000_000_000_000_000_000 i128 ); // +100 USDC
wallet . apply_delta ( & delta ) . unwrap ();
assert_eq! ( wallet . get ( TokenId ( 1 )), 5_000_000_000_000_000_000 u128 ); // 5 ETH left
assert_eq! ( wallet . get ( TokenId ( 2 )), 100_000_000_000_000_000_000 u128 ); // 100 USDC added
Saturation Behavior
let mut wallet = Balances ( IndexMap :: new ());
wallet . set ( TokenId ( 1 ), 1_000_000_000_000_000_000 u128 ); // 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_000 i128 ); // -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_000 i128 ); // Debit 1000 wstETH
let outcome = ExecOutcome {
delta ,
gas_used : 180_000 u64 . 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
fn format_amount ( amount : u128 , decimals : u32 ) -> String {
let divisor = 10 u128 . 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_789 u128 ;
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 * 10 u128 . pow ( decimals ) + frac )
}
let amount = parse_amount ( "1000.5" , 18 ) ? ;
assert_eq! ( amount , 1_000_500_000_000_000_000_000 u128 );
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_000 u128 ; // 10000e18
let ltv_70_pct = pct_bps ( collateral , 7000 ); // 70%
assert_eq! ( ltv_70_pct , 7_000_000_000_000_000_000_000 u128 ); // 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_000 u128 ;
let token_price = 2_000_000_000_000_000_000 u128 ; // 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_000 i128 ); // Debit 1000 ETH
delta . 0. insert ( TokenId ( 2 ), 2_000_000_000_000_000_000_000 i128 ); // 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_000 u128 ); // 100 ETH
portfolio . set ( TokenId ( 2 ), 50_000_000_000_000_000_000_000 u128 ); // 50000 USDC
portfolio . set ( TokenId ( 3 ), 10_000_000_000_000_000_000 u128 ); // 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