Arithmetic Overflow Remediation
How to prevent integer overflow and underflow in Solana programs by replacing unchecked arithmetic with checked methods and safe intermediate widening.
Arithmetic Overflow Remediation
Overview
Related Detector: Integer Overflow
Solana programs represent token amounts as u64 values. In Rust’s release build mode (which all deployed programs use), unchecked integer arithmetic wraps silently: u64::MAX + 1 becomes 0, and 0u64 - 1 becomes u64::MAX (18,446,744,073,709,551,615). A program that uses the +, -, or * operators directly on user-controlled amounts without overflow protection can be exploited to drain balances (underflow) or inflate supply counters (overflow). Multiplication is particularly dangerous — the product of two moderate u64 values can easily exceed u64::MAX.
The fix is to replace all arithmetic operators on token amounts with their checked equivalents (checked_add, checked_sub, checked_mul, checked_div), which return Option<T> and propagate None instead of wrapping.
Recommended Fix
Before (Vulnerable)
use anchor_lang::prelude::*;
#[program]
mod vulnerable_token {
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
let account = &mut ctx.accounts.user_account;
// VULNERABLE: in release mode, 0 - 1 == u64::MAX (silent underflow)
account.balance -= amount;
token::transfer(ctx.accounts.transfer_ctx(), amount)?;
Ok(())
}
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
let pool = &mut ctx.accounts.pool;
// VULNERABLE: u64::MAX + 1 == 0 (supply wraps to zero)
pool.total_deposits += amount;
Ok(())
}
pub fn compute_collateral(
ctx: Context<Compute>,
token_amount: u64,
price_per_token: u64,
) -> Result<()> {
// VULNERABLE: product can exceed u64::MAX — wraps to unexpected small value
let collateral_value = token_amount * price_per_token;
ctx.accounts.loan.collateral = collateral_value;
Ok(())
}
}
After (Fixed)
use anchor_lang::prelude::*;
#[program]
mod secure_token {
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
let account = &mut ctx.accounts.user_account;
// FIXED: checked_sub returns None on underflow — propagated as an error
account.balance = account.balance
.checked_sub(amount)
.ok_or(error!(ErrorCode::InsufficientFunds))?;
token::transfer(ctx.accounts.transfer_ctx(), amount)?;
Ok(())
}
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
let pool = &mut ctx.accounts.pool;
// FIXED: checked_add returns None on overflow
pool.total_deposits = pool.total_deposits
.checked_add(amount)
.ok_or(error!(ErrorCode::ArithmeticOverflow))?;
Ok(())
}
pub fn compute_collateral(
ctx: Context<Compute>,
token_amount: u64,
price_per_token: u64,
) -> Result<()> {
// FIXED: widen to u128 for multiplication, then bounds-check before narrowing
let collateral_value = (token_amount as u128)
.checked_mul(price_per_token as u128)
.ok_or(error!(ErrorCode::ArithmeticOverflow))
.and_then(|v| u64::try_from(v).map_err(|_| error!(ErrorCode::ArithmeticOverflow)))?;
ctx.accounts.loan.collateral = collateral_value;
Ok(())
}
}
Every arithmetic result is now either a verified value or an explicit error — the program can never silently produce a wrapped or truncated value.
Alternative Mitigations
1. Helper macro for consistent checked arithmetic
Define a project-wide macro to ensure all arithmetic sites use the same pattern:
/// Returns an error if the arithmetic operation overflows or underflows.
macro_rules! checked_add {
($a:expr, $b:expr) => {
$a.checked_add($b)
.ok_or(error!(ErrorCode::MathOverflow))?
};
}
macro_rules! checked_sub {
($a:expr, $b:expr) => {
$a.checked_sub($b)
.ok_or(error!(ErrorCode::MathOverflow))?
};
}
macro_rules! checked_mul {
($a:expr, $b:expr) => {
$a.checked_mul($b)
.ok_or(error!(ErrorCode::MathOverflow))?
};
}
// Usage:
pub fn update_balance(ctx: Context<Update>, delta: u64) -> Result<()> {
let account = &mut ctx.accounts.user;
account.balance = checked_add!(account.balance, delta);
Ok(())
}
2. Saturating arithmetic for non-financial accumulators
For counters and accumulators where clamping at the maximum is acceptable (e.g., event counters), use saturating methods instead of checked methods:
pub fn increment_counter(ctx: Context<Counter>) -> Result<()> {
let stats = &mut ctx.accounts.stats;
// Saturates at u64::MAX instead of wrapping — safe for non-value counters
stats.event_count = stats.event_count.saturating_add(1);
Ok(())
}
Do not use saturating arithmetic for financial balances or supply totals where clamping would silently misrepresent the true value.
3. u128 intermediate arithmetic for price calculations
Price calculations frequently multiply two u64 values. Always promote to u128 before multiplying:
/// Compute value = amount * price, returning an error if the result exceeds u64::MAX.
fn compute_value(amount: u64, price: u64) -> Result<u64> {
let result = (amount as u128)
.checked_mul(price as u128)
.ok_or(error!(ErrorCode::MathOverflow))?;
u64::try_from(result).map_err(|_| error!(ErrorCode::MathOverflow))
}
/// Compute ratio = numerator * scale / denominator, safe against intermediate overflow.
fn compute_scaled_ratio(numerator: u64, denominator: u64, scale: u64) -> Result<u64> {
let scaled = (numerator as u128)
.checked_mul(scale as u128)
.ok_or(error!(ErrorCode::MathOverflow))?;
let result = scaled
.checked_div(denominator as u128)
.ok_or(error!(ErrorCode::DivisionByZero))?;
u64::try_from(result).map_err(|_| error!(ErrorCode::MathOverflow))
}
4. Validate amount bounds on instruction entry
Reject obviously invalid inputs at the boundary of the instruction handler to reduce the attack surface:
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
// Reject zero — a common edge case that produces no-op behavior
require!(amount > 0, ErrorCode::InvalidAmount);
// Reject implausibly large amounts (domain-specific cap)
const MAX_SINGLE_DEPOSIT: u64 = 1_000_000_000_000; // 1,000 tokens at 9 decimals
require!(amount <= MAX_SINGLE_DEPOSIT, ErrorCode::AmountTooLarge);
// ... proceed with validated amount
Ok(())
}
Common Mistakes
Mistake 1: Using wrapping_add / wrapping_sub on financial values
// WRONG: wrapping_* explicitly opts in to silent overflow — never use for balances
account.balance = account.balance.wrapping_sub(amount);
wrapping_* methods are for bit manipulation and hash functions, not financial arithmetic.
Mistake 2: Checking against a constant cap after the arithmetic
// WRONG: the overflow happens before the cap check
let new_total = pool.total_supply + mint_amount; // Could already be 0 due to wrap
require!(new_total <= MAX_SUPPLY, ErrorCode::SupplyCapExceeded);
Use checked_add first. If the result would overflow, the error is returned before any comparison.
Mistake 3: Assuming debug-mode panics protect production code
// MISLEADING: panics in tests give false confidence
// In debug mode: panics on overflow ✓
// In release mode (production): silently wraps ✗
pool.total_deposits += amount;
Overflow panics only occur in debug builds. All Solana programs are compiled with --release for deployment. Write correct arithmetic that is safe in release mode.