Safe Arithmetic
Token amounts, fees, and share calculations must never wrap silently. On-chain, use checked_*, saturating_*, or explicit bounds checks and map failures to ProgramError.
Search across all documentation pages
Token amounts, fees, and share calculations must never wrap silently. On-chain, use checked_*, saturating_*, or explicit bounds checks and map failures to ProgramError.
let total = base
.checked_add(fee)
.ok_or(ProgramError::InvalidArgument)?;When to reach for this:
use solana_program::program_error::ProgramError;
pub fn apply_fee(amount: u64, bps: u64) -> Result<u64, ProgramError> {
if bps > 10_000 {
return Err(ProgramError::InvalidArgument);
}
let fee = amount
.checked_mul(bps)
.and_then(|v| v.checked_div(10_000))
.ok_or(ProgramError::InvalidArgument)?;
amount
.checked_sub(fee)
.ok_or(ProgramError::InvalidArgument)
}What this demonstrates:
bps before math.InvalidArgument for client decoding.| Method | Behavior |
|---|---|
checked_add | None on overflow |
saturating_add | Caps at MAX |
wrapping_add | Rare on-chain - audit red flag |
let wide = (a as u128).checked_mul(b as u128)?;a + b wraps in release without overflow checks in some contexts. Fix: Always checked_* for user funds.saturating_sub can hide insolvency. Fix: Use checked_sub when balance must not underflow./ 0 even on-chain. Fix: Guard divisor explicitly.as u64 from u128 silently truncates. Fix: Check try_into bounds.i64 unless required. Fix: undefined| Alternative | Use When | Don't Use When |
|---|---|---|
| spl-math / fixed-point crates | DEX curve math | Simple counters |
| u128 everywhere | Intermediate precision | CU-sensitive tight loops |
| Off-chain precompute | Display-only values | Authoritative balances |
Do not rely on it - use explicit checked math.
Caps on gameplay stats, not token balances.
mul then div 10000 with checked ops.
Slightly higher - still cheaper than exploits.
Use Rust std checked in instruction handlers.
Still use checked_sub after compare for race safety.
Avoid f64 for token amounts - rounding attacks.
Check divisor non-zero.
Search for +, -, * on u64 in instruction paths.
No negative token amounts - reject signed inputs.
Checked fold: try_fold pattern.
Unit test u64::MAX edge cases on host.
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026