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.
Recipe
let total = base
.checked_add(fee)
.ok_or(ProgramError::InvalidArgument)?;When to reach for this:
- Aggregating balances, shares, or reward indexes.
- Computing fees as a percentage of lamports or token units.
- Implementing vault accounting or pro-rata distributions.
Working Example
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:
- Basis points fee uses two-step checked mul/div.
- Bounds check
bpsbefore math. - Errors surface as
InvalidArgumentfor client decoding.
Deep Dive
API Choice
| Method | Behavior |
|---|---|
checked_add | None on overflow |
saturating_add | Caps at MAX |
wrapping_add | Rare on-chain - audit red flag |
Financial Patterns
- Use u128 intermediates for mul-div when product exceeds u64.
- Document rounding (floor vs ceil) for fee splits.
Rust Notes
let wide = (a as u128).checked_mul(b as u128)?;Gotchas
- Unchecked ops -
a + bwraps in release without overflow checks in some contexts.. Fix: Alwayschecked_*for user funds. - Saturating misuse -
saturating_subcan hide insolvency.. Fix: Usechecked_subwhen balance must not underflow. - Division by zero - Rust panics on
/ 0even on-chain.. Fix: Guard divisor explicitly. - Cast truncation -
as u64from u128 silently truncates.. Fix: Checktry_intobounds. - Signed confusion - Token amounts are unsigned - avoid
i64unless required.. Fix: undefined
Alternatives
| 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 |
FAQs
Does Rust overflow check on SBF?
Do not rely on it - use explicit checked math.
When is saturating OK?
Caps on gameplay stats, not token balances.
Basis points pattern?
mul then div 10000 with checked ops.
u128 CU cost?
Slightly higher - still cheaper than exploits.
Anchor helpers?
Use Rust std checked in instruction handlers.
Compare before subtract?
Still use checked_sub after compare for race safety.
Float math?
Avoid f64 for token amounts - rounding attacks.
Modulo for shards?
Check divisor non-zero.
Audit tools?
Search for +, -, * on u64 in instruction paths.
Negation?
No negative token amounts - reject signed inputs.
Batch sums?
Checked fold: try_fold pattern.
Testing overflow?
Unit test u64::MAX edge cases on host.
Related
- Fee & Treasury Patterns - Fee collection
- Vault & Escrow - Balance tracking
- Rust for Solana Best Practices - Security habits
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.