Unchecked Fee/Rent Math
Detects unchecked arithmetic in fee and rent calculations that can overflow or underflow, leading to fund loss.
Unchecked Fee/Rent Math
Overview
Remediation Guide: How to Fix Unchecked Fee/Rent Math
The unchecked fee/rent math detector identifies arithmetic operations on rent-related, fee-related, and balance-related values that do not use checked math (checked_add, checked_sub, checked_mul, checked_div). On Solana, integer overflow and underflow are silent in BPF — the operation wraps around without error. When this happens in rent calculations, fee computations, or balance checks, the result is incorrect values that can be exploited.
The detector specifically targets arithmetic in financial contexts: rent minimum balance calculations, transaction fee computations, lamport transfer amounts, and token decimal conversions.
Why This Is an Issue
Solana’s BPF runtime does not trap on integer overflow (unlike Rust’s debug mode). Unchecked arithmetic in financial calculations leads to:
- Rent calculation overflow:
rent_per_byte * account_sizecan overflow to a small value, making the program believe an account needs much less rent than required. The account can then be garbage-collected. - Balance underflow:
account_balance - withdraw_amountcan underflow tou64::MAX, making the program believe the account has an enormous balance. The attacker withdraws more than the actual balance. - Fee bypass:
fee_bps * amount / 10000can overflow to zero whenfee_bps * amountwraps, allowing fee-free operations. - Decimal conversion overflow: Converting between different token decimals (e.g., 6-decimal USDC to 9-decimal token) involves multiplication that can overflow.
CWE mapping: CWE-190 (Integer Overflow), CWE-191 (Integer Underflow).
How to Resolve
// Before: Vulnerable -- unchecked arithmetic on rent values
pub fn create_account(accounts: &[AccountInfo], size: u64) -> ProgramResult {
let rent = Rent::get()?;
let payer = &accounts[0];
// VULNERABLE: can overflow with large size values
let lamports_needed = rent.minimum_balance(size as usize);
let total_cost = lamports_needed + CREATION_FEE; // Unchecked add
**payer.try_borrow_mut_lamports()? -= total_cost; // Unchecked sub
Ok(())
}
// After: Use checked arithmetic throughout
pub fn create_account(accounts: &[AccountInfo], size: u64) -> ProgramResult {
let rent = Rent::get()?;
let payer = &accounts[0];
let lamports_needed = rent.minimum_balance(size as usize);
let total_cost = lamports_needed
.checked_add(CREATION_FEE)
.ok_or(ProgramError::ArithmeticOverflow)?;
let payer_balance = **payer.try_borrow_lamports()?;
if payer_balance < total_cost {
return Err(ProgramError::InsufficientFunds);
}
**payer.try_borrow_mut_lamports()? = payer_balance
.checked_sub(total_cost)
.ok_or(ProgramError::ArithmeticOverflow)?;
Ok(())
}
Examples
Vulnerable Code
pub fn collect_fee(accounts: &[AccountInfo], amount: u64, fee_bps: u64) -> ProgramResult {
let user = &accounts[0];
let treasury = &accounts[1];
// VULNERABLE: fee_bps * amount can overflow to a small value
let fee = fee_bps * amount / 10_000;
let net_amount = amount - fee; // Can underflow if fee > amount
**user.try_borrow_mut_lamports()? -= amount;
**treasury.try_borrow_mut_lamports()? += fee;
**accounts[2].try_borrow_mut_lamports()? += net_amount;
Ok(())
}
Fixed Code
pub fn collect_fee(accounts: &[AccountInfo], amount: u64, fee_bps: u64) -> ProgramResult {
let user = &accounts[0];
let treasury = &accounts[1];
// FIXED: validate fee_bps range
if fee_bps > MAX_FEE_BPS {
return Err(ProgramError::InvalidArgument);
}
// FIXED: checked arithmetic throughout
let fee = (fee_bps as u128)
.checked_mul(amount as u128)
.ok_or(ProgramError::ArithmeticOverflow)?
.checked_div(10_000)
.ok_or(ProgramError::ArithmeticOverflow)? as u64;
let net_amount = amount
.checked_sub(fee)
.ok_or(ProgramError::ArithmeticOverflow)?;
let user_balance = **user.try_borrow_lamports()?;
if user_balance < amount {
return Err(ProgramError::InsufficientFunds);
}
**user.try_borrow_mut_lamports()? -= amount;
**treasury.try_borrow_mut_lamports()? += fee;
**accounts[2].try_borrow_mut_lamports()? += net_amount;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "unchecked-fee-rent-math",
"severity": "critical",
"confidence": 0.85,
"description": "Unchecked multiplication in fee calculation: fee_bps * amount can overflow silently in BPF, producing an incorrect (wrapped) fee value. Use checked_mul or cast to u128 for intermediate calculation.",
"location": { "function": "collect_fee", "offset": 3 }
}
Detection Methodology
The detector targets arithmetic in financial contexts:
- Fee/rent variable identification: Identifies variables involved in fee or rent calculations by analyzing variable names, data sources (Rent sysvar, fee parameters), and usage patterns (passed to transfer operations or balance comparisons).
- Arithmetic operation scanning: For each
Assignstatement with a binary arithmetic expression (Add,Sub,Mul,Div), checks whether any operand is a fee/rent-related variable. - Checked math verification: Determines whether the operation uses checked arithmetic (the operation is wrapped in a
checked_*call pattern or uses saturating arithmetic). Unchecked operations on financial variables produce findings. - Context-specific messaging: Generates finding details based on the specific operation type (multiplication overflow, subtraction underflow, division truncation) with tailored recommendations.
Limitations
False positives:
- Operations where the value range is constrained by prior bounds checks (e.g.,
fee_bpsvalidated to be less than 10000) may still be flagged because the detector does not perform full range analysis. - Saturating arithmetic (
saturating_add,saturating_sub) is safe but may not be recognized in all cases.
False negatives:
- Fee calculations that use intermediate variables without recognizable fee/rent naming conventions.
- Programs that perform fee arithmetic through helper functions where the financial context is not visible.
Related Detectors
- Integer Overflow — general integer overflow detection
- Missing Rent Check — missing rent exemption verification
- Lamport Drain — unauthorized lamport withdrawal patterns