Arithmetic & Overflow
On-chain programs use fixed-size integers with wrapping arithmetic in unchecked ops. Overflow and rounding bugs drain vaults, mint unbacked shares, or brick accounts permanently.
Recipe
Quick-reference recipe card - copy-paste ready.
let shares = deposit_amount
.checked_mul(total_shares)
.and_then(|v| v.checked_div(total_assets))
.ok_or(ErrorCode::MathOverflow)?;
// Prefer u128 intermediates for token math
let out = (amount_in as u128)
.checked_mul(reserve_out as u128)
.and_then(|n| n.checked_div((reserve_in as u128) + (amount_in as u128)))
.ok_or(ErrorCode::MathOverflow)? as u64;When to reach for this:
- Share minting/burning in vaults.
- AMM swap calculations inside programs.
- Interest index accrual in lending.
- Any
+,-,*,/on token amounts.
Working Example
use anchor_lang::prelude::*;
#[account]
pub struct Pool {
pub total_assets: u64,
pub total_shares: u64,
}
pub fn deposit(pool: &mut Pool, amount: u64) -> Result<u64> {
let shares = if pool.total_shares == 0 {
amount
} else {
(amount as u128)
.checked_mul(pool.total_shares as u128)
.and_then(|n| n.checked_div(pool.total_assets as u128))
.ok_or(ErrorCode::MathOverflow)? as u64
};
pool.total_assets = pool
.total_assets
.checked_add(amount)
.ok_or(ErrorCode::MathOverflow)?;
pool.total_shares = pool
.total_shares
.checked_add(shares)
.ok_or(ErrorCode::MathOverflow)?;
Ok(shares)
}
#[error_code]
pub enum ErrorCode {
MathOverflow,
}What this demonstrates:
- Division after multiplication uses
u128headroom. - First depositor 1:1 mint avoids divide-by-zero.
- State updates use
checked_addto catch overflow.
Deep Dive
How It Works
- BPF builds may not trap on overflow for plain operators in release mode.
checked_*returnsNoneon overflow - map to program error.- Division truncates toward zero - favors protocol on rounding policy.
- Fee calculations should use basis points:
amount * bps / 10_000.
Rounding Policy
| Operation | Safe default | Why |
|---|---|---|
| Mint shares | Round down | Prevent over-mint |
| Burn shares | Round up debt | Prevent under-collateral |
| Fees | Round up fee | Protocol solvency |
| User payout | Round down payout | Prevent vault drain |
Rust Notes
use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL;
const BPS: u128 = 10_000;
let fee = (amount as u128)
.checked_mul(fee_bps as u128)
.and_then(|v| v.checked_add(BPS - 1)) // ceil div trick
.and_then(|v| v.checked_div(BPS))
.ok_or(ErrorCode::MathOverflow)? as u64;Gotchas
- Plain
+on balances - Silent wrap nearu64::MAX. Fix:checked_addeverywhere on user balances. - Divide before multiply - Loses precision and enables dust attacks. Fix: Multiply first in wider type.
- Zero total shares - Divide by zero or inflation attack on first deposit. Fix: Minimum liquidity lock or virtual offset.
- Cast truncates -
u128 as u64silently truncates. Fix:u64::try_fromwith range check. - Signed confusion - Using
i64prices without bounds. Fix: Document ranges; usechecked_*on signed too. - Different token decimals - Mixing 6 and 9 decimal mints. Fix: Normalize to common precision before math.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
u128 intermediates | Standard token math | CU-critical hot paths (still often worth it) |
| Fixed-point crates | Complex curves | Simple share math |
| Off-chain calculation | View only | Authoritative settlement |
FAQs
Does Rust debug mode catch overflow on-chain?
No - deployed BPF uses release semantics; never rely on debug panics.
What is the first depositor attack?
Attacker manipulates tiny deposit + donation to inflate share price - mitigate with virtual reserves or min deposit.
Should I use saturating_add?
Only when capping is intended behavior - vault balances should error on overflow, not saturate.
How do bps fees interact with overflow?
Compute in u128: amount * bps / 10_000 with checked ops at each step.
Are floats ever OK?
Never in on-chain settlement - floats are non-deterministic across architectures.
How do I audit math?
Map every arithmetic op; grep for +, *, / without checked_ in program src.
Does Anchor help with math?
No - you implement safe math; see Safe Arithmetic.
How do oracle exponents fit in?
Scale prices to common exponent with checked pow10 before combining amounts.
Can fuzzing find overflow?
Yes - use Trident fuzzing with extreme input amounts (Fuzzing Programs).
What CU cost does u128 add?
Modest vs exploit cost - prefer safety unless profiling proves bottleneck.
Related
- Safe Arithmetic - Rust patterns
- Account Validation Attacks - economic exploits
- Sealevel Attacks Catalog - inflation attacks
- Fuzzing Programs - edge case discovery
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.