Unchecked Fee/Rent Math Remediation
How to use checked arithmetic in fee and rent calculations to prevent overflow and underflow exploits.
Unchecked Fee/Rent Math Remediation
Overview
Related Detector: Unchecked Fee/Rent Math
Unchecked arithmetic in fee and rent calculations can overflow or underflow silently in Solana’s BPF runtime, producing incorrect values that attackers exploit. The fix is to replace all raw arithmetic operators (+, -, *, /) with their checked equivalents (checked_add, checked_sub, checked_mul, checked_div) and handle the error case explicitly.
Recommended Fix
Before (Vulnerable)
pub fn transfer_with_fee(
accounts: &[AccountInfo],
amount: u64,
fee_bps: u64,
) -> ProgramResult {
// VULNERABLE: unchecked multiplication can overflow
let fee = amount * fee_bps / 10_000;
// VULNERABLE: unchecked subtraction can underflow
let net = amount - fee;
**accounts[0].try_borrow_mut_lamports()? -= amount;
**accounts[1].try_borrow_mut_lamports()? += net;
**accounts[2].try_borrow_mut_lamports()? += fee;
Ok(())
}
After (Fixed)
pub fn transfer_with_fee(
accounts: &[AccountInfo],
amount: u64,
fee_bps: u64,
) -> ProgramResult {
// FIXED: validate bounds
if fee_bps > 10_000 {
return Err(ProgramError::InvalidArgument);
}
// FIXED: use u128 intermediate to prevent overflow
let fee = u64::try_from(
(amount as u128)
.checked_mul(fee_bps as u128)
.ok_or(ProgramError::ArithmeticOverflow)?
/ 10_000u128
).map_err(|_| ProgramError::ArithmeticOverflow)?;
// FIXED: checked subtraction
let net = amount
.checked_sub(fee)
.ok_or(ProgramError::ArithmeticOverflow)?;
// FIXED: verify source has sufficient balance
let source_balance = **accounts[0].try_borrow_lamports()?;
if source_balance < amount {
return Err(ProgramError::InsufficientFunds);
}
**accounts[0].try_borrow_mut_lamports()? -= amount;
**accounts[1].try_borrow_mut_lamports()? += net;
**accounts[2].try_borrow_mut_lamports()? += fee;
Ok(())
}
Alternative Mitigations
1. Use u128 for intermediate calculations
When multiplying two u64 values, cast to u128 for the intermediate result:
// Safe: intermediate u128 cannot overflow for u64 * u64
let fee = ((amount as u128) * (fee_bps as u128) / 10_000u128) as u64;
This is safe because u64::MAX * u64::MAX fits in a u128. Convert back to u64 only after division brings the result into range.
2. Anchor’s checked_* math helpers
use anchor_lang::prelude::*;
pub fn process(ctx: Context<MyContext>, amount: u64) -> Result<()> {
let rent = Rent::get()?;
let min_balance = rent.minimum_balance(ctx.accounts.target.to_account_info().data_len());
// Use Anchor's error types with checked math
let remaining = amount
.checked_sub(min_balance)
.ok_or(error!(ErrorCode::ArithmeticOverflow))?;
Ok(())
}
3. Fixed-point decimal library
For complex financial calculations, use a fixed-point decimal type:
use spl_math::precise_number::PreciseNumber;
let amount = PreciseNumber::new(transfer_amount as u128)?;
let fee_rate = PreciseNumber::new(fee_bps as u128)?
.checked_div(&PreciseNumber::new(10_000)?)?;
let fee = amount.checked_mul(&fee_rate)?;
let net = amount.checked_sub(&fee)?;
Common Mistakes
Mistake 1: Checking only one operation in a chain
let fee = amount.checked_mul(fee_bps).ok_or(ProgramError::ArithmeticOverflow)?;
// WRONG: division result used in unchecked subtraction
let fee_final = fee / 10_000;
let net = amount - fee_final; // Still unchecked
Every operation in the chain must use checked arithmetic.
Mistake 2: Using wrapping_* instead of checked_*
// WRONG: wrapping_sub wraps silently, same as raw subtraction
let net = amount.wrapping_sub(fee);
wrapping_* operations produce wrapped results without errors. Use checked_* which returns None on overflow.
Mistake 3: Truncation in fee calculation order
// WRONG: division before multiplication loses precision
let fee = (fee_bps / 10_000) * amount; // Always 0 when fee_bps < 10000
Perform multiplication before division to preserve precision:
let fee = amount.checked_mul(fee_bps)?.checked_div(10_000)?;
Mistake 4: Not validating input ranges
// Checked math prevents overflow, but fee_bps = u64::MAX
// still produces unexpected results
let fee = amount.checked_mul(fee_bps).ok_or(...)?;
Always validate that input parameters are within expected ranges before performing arithmetic.