Unchecked Arithmetic Remediation
How to fix unchecked arithmetic operations that could silently overflow or underflow.
Unchecked Arithmetic Remediation
Overview
Related Detector: Unchecked Arithmetic
Solana BPF programs run in release mode where integer overflow wraps silently instead of panicking. Replace all raw arithmetic operators with their checked equivalents (checked_add, checked_sub, checked_mul, checked_div) and handle the error explicitly.
Recommended Fix
Before (Vulnerable)
pub fn calculate_payout(
accounts: &[AccountInfo],
staked_amount: u64,
reward_rate: u64,
duration: u64,
) -> ProgramResult {
// VULNERABLE: all operations can overflow silently
let reward = staked_amount * reward_rate * duration;
let total = staked_amount + reward;
**accounts[0].try_borrow_mut_lamports()? -= total;
**accounts[1].try_borrow_mut_lamports()? += total;
Ok(())
}
After (Fixed)
pub fn calculate_payout(
accounts: &[AccountInfo],
staked_amount: u64,
reward_rate: u64,
duration: u64,
) -> ProgramResult {
// FIXED: checked arithmetic with error handling
let reward = staked_amount
.checked_mul(reward_rate)
.and_then(|r| r.checked_mul(duration))
.ok_or(ProgramError::ArithmeticOverflow)?;
let total = staked_amount
.checked_add(reward)
.ok_or(ProgramError::ArithmeticOverflow)?;
let balance = **accounts[0].try_borrow_lamports()?;
if balance < total {
return Err(ProgramError::InsufficientFunds);
}
**accounts[0].try_borrow_mut_lamports()? -= total;
**accounts[1].try_borrow_mut_lamports()? += total;
Ok(())
}
Alternative Mitigations
1. Use u128 intermediate for multiplications
When multiplying two u64 values, use u128 to avoid overflow:
let reward = u64::try_from(
(staked_amount as u128)
.checked_mul(reward_rate as u128)
.ok_or(ProgramError::ArithmeticOverflow)?
.checked_div(PRECISION as u128)
.ok_or(ProgramError::ArithmeticOverflow)?
).map_err(|_| ProgramError::ArithmeticOverflow)?;
2. Saturating arithmetic for non-critical paths
When overflow should clamp to the maximum value rather than error:
// Display-only: clamp instead of error
let display_total = amount_a.saturating_add(amount_b);
let display_product = price.saturating_mul(quantity);
3. Anchor checked_math! macro
use anchor_lang::prelude::*;
pub fn calculate(ctx: Context<Calculate>, a: u64, b: u64) -> Result<()> {
// Anchor's checked_math! wraps all operations
let result = checked_math! { a * b + ctx.accounts.state.bonus }
.ok_or(ErrorCode::Overflow)?;
Ok(())
}
Common Mistakes
Mistake 1: Only checking the final operation
// WRONG: intermediate overflow not caught
let intermediate = a * b; // Can overflow here!
let result = intermediate.checked_add(c).ok_or(ProgramError::ArithmeticOverflow)?;
Every operation in the chain must use checked arithmetic.
Mistake 2: Using wrapping_* instead of checked_*
// WRONG: wrapping is the same as raw overflow behavior
let result = amount.wrapping_add(bonus);
wrapping_* silently wraps — use checked_* which returns None on overflow.
Mistake 3: Forgetting subtraction underflow
// WRONG: subtraction underflow wraps to u64::MAX
let remaining = balance - withdrawal;
// FIXED: checked subtraction
let remaining = balance
.checked_sub(withdrawal)
.ok_or(ProgramError::InsufficientFunds)?;
Mistake 4: Not validating inputs before arithmetic
// Checked arithmetic prevents overflow, but extreme inputs
// may produce unexpected but technically valid results
let fee = amount.checked_mul(fee_bps).ok_or(ProgramError::ArithmeticOverflow)?;
// If fee_bps = 10001, fee could exceed amount
Validate input ranges before performing arithmetic.