Skip to main content
Sigvex

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.

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<()> {
    // the account-validation framework'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.

References