Integer Overflow / Underflow (Solana)
Detects arithmetic operations in Solana programs that can overflow or underflow, producing wrapped values that corrupt financial state.
Integer Overflow / Underflow (Solana)
Overview
Remediation Guide: How to Fix Integer Overflow
The integer-overflow detector identifies arithmetic operations in Solana programs (written in Rust) that can silently wrap on overflow or underflow. In Rust, integer arithmetic behaves differently depending on the build profile: in debug builds, overflow panics; in release builds (the standard for deployed programs), integer operations wrap silently by default unless the program uses .checked_*(), .saturating_*(), or .wrapping_*() methods explicitly.
Sigvex analyzes compiled Solana BPF bytecode and identifies arithmetic sequences that correspond to unchecked addition, subtraction, or multiplication on financial values — specifically values derived from lamport balances, token amounts, or user-supplied instruction data. The detector flags these when the wrapped value would be used in a downstream comparison or stored to account state.
The distinction from Ethereum is important: while Solidity 0.8+ adds overflow protection by default, Rust’s release profile wraps silently. A Solana program that was tested only in debug mode may pass all tests but be exploitable in production.
Why This Is an Issue
Lamport amounts and token balances are unsigned 64-bit integers (u64) in Solana. An overflow of u64 wraps from u64::MAX to 0, while an underflow wraps from 0 to u64::MAX. Both conditions create exploitable states:
- Overflow in balance accounting: Adding two large amounts that overflow to a small sum allows double-spend: the sum is smaller than one of the inputs, allowing more withdrawals than deposits.
- Underflow in balance deduction: Subtracting more than is available wraps to near
u64::MAX, creating effectively unlimited withdrawable balance. - Overflow in token minting: Calculating mint amounts that overflow allows minting a near-maximum token supply from minimal inputs.
The checked arithmetic methods (u64::checked_add, u64::checked_sub, u64::checked_mul) return Option<u64>, returning None on overflow, which the program can then handle as an error. The .saturating_*() methods cap at the type’s limits rather than returning an error.
How to Resolve
Replace unchecked arithmetic with checked or saturating variants on all financial values:
// Before: Vulnerable — release-mode arithmetic wraps silently
pub fn deposit(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let vault = &accounts[0];
let mut vault_data = VaultState::try_from_slice(&vault.data.borrow())?;
// VULNERABLE: if vault_data.balance + amount overflows u64, wraps to small value
vault_data.balance += amount;
vault_data.serialize(&mut *vault.data.borrow_mut())?;
Ok(())
}
// After: Fixed — use checked arithmetic
pub fn deposit(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let vault = &accounts[0];
let mut vault_data = VaultState::try_from_slice(&vault.data.borrow())?;
// SAFE: returns error instead of wrapping
vault_data.balance = vault_data.balance
.checked_add(amount)
.ok_or(ProgramError::ArithmeticOverflow)?;
vault_data.serialize(&mut *vault.data.borrow_mut())?;
Ok(())
}
For token amounts, always use checked arithmetic and validate inputs:
// Before: Vulnerable subtraction
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let user_account = &accounts[0];
let mut state = UserState::try_from_slice(&user_account.data.borrow())?;
// VULNERABLE: wraps to u64::MAX if amount > state.balance
state.balance -= amount;
state.serialize(&mut *user_account.data.borrow_mut())?;
Ok(())
}
// After: Fixed with checked subtraction and explicit error
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let user_account = &accounts[0];
let mut state = UserState::try_from_slice(&user_account.data.borrow())?;
state.balance = state.balance
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;
state.serialize(&mut *user_account.data.borrow_mut())?;
Ok(())
}
For Anchor programs, be aware that Anchor’s Rust environment still uses release-mode arithmetic. Use explicit checked methods in your instruction handlers:
#[program]
pub mod my_program {
pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
let from = &mut ctx.accounts.from_account;
let to = &mut ctx.accounts.to_account;
// Use checked arithmetic — Anchor does not add automatic overflow protection
from.balance = from.balance
.checked_sub(amount)
.ok_or(error!(ErrorCode::InsufficientFunds))?;
to.balance = to.balance
.checked_add(amount)
.ok_or(error!(ErrorCode::ArithmeticOverflow))?;
Ok(())
}
}
Examples
Vulnerable Code
// Staking rewards calculation — multiplication overflow
pub fn calculate_rewards(accounts: &[AccountInfo]) -> ProgramResult {
let stake_account = &accounts[0];
let mut stake = StakeAccount::try_from_slice(&stake_account.data.borrow())?;
// VULNERABLE: if stake.amount and reward_rate are both large,
// multiplication overflows to small value — user gets tiny rewards
let rewards = stake.amount * stake.reward_rate / 10000;
stake.claimable_rewards += rewards; // Also unchecked addition
stake.serialize(&mut *stake_account.data.borrow_mut())?;
Ok(())
}
Fixed Code
pub fn calculate_rewards(accounts: &[AccountInfo]) -> ProgramResult {
let stake_account = &accounts[0];
let mut stake = StakeAccount::try_from_slice(&stake_account.data.borrow())?;
// Use u128 for intermediate multiplication to avoid overflow
let rewards_u128 = (stake.amount as u128)
.checked_mul(stake.reward_rate as u128)
.ok_or(ProgramError::ArithmeticOverflow)?
/ 10000_u128;
// Safely convert back to u64
let rewards = u64::try_from(rewards_u128)
.map_err(|_| ProgramError::ArithmeticOverflow)?;
stake.claimable_rewards = stake.claimable_rewards
.checked_add(rewards)
.ok_or(ProgramError::ArithmeticOverflow)?;
stake.serialize(&mut *stake_account.data.borrow_mut())?;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "integer-overflow",
"severity": "high",
"confidence": 0.82,
"description": "Function calculate_rewards() performs unchecked multiplication of stake.amount by stake.reward_rate. In release mode, this silently wraps on overflow, producing incorrect reward calculations.",
"location": { "function": "calculate_rewards", "offset": 31 }
}
Detection Methodology
Sigvex analyzes compiled BPF bytecode for overflow-prone arithmetic patterns:
- Arithmetic instruction identification: Identifies BPF
add64,sub64,mul64instructions operating on values derived from account data or instruction parameters. - Checked method recognition: Recognizes the BPF instruction sequences produced by Rust’s
checked_add,checked_sub,checked_mul— which include a branch on overflow — and marks those as safe. - Financial value taint: Tags values loaded from lamport fields, token amount fields, or user-supplied instruction data as “financial values” requiring protection.
- Downstream impact: Verifies that overflowed values would be stored back to account state or used in comparison operations that gate financial logic.
- u128 promotion detection: Recognizes patterns where u64 values are promoted to u128 for intermediate calculations (a valid overflow prevention technique) and reduces confidence accordingly.
Limitations
False positives:
- Arithmetic on values that are provably bounded (e.g., constants, or values validated to be within a range before the operation) may produce false positives.
- Counters and indices that cannot realistically overflow may be flagged.
False negatives:
- Overflow through multiple layers of function calls (inter-procedural) is not fully tracked.
- Overflow in CPI return values from other programs is not detected.
Related Detectors
- Integer Overflow — detects a broader range of unvalidated arithmetic patterns
- Lamport Drain — detects lamport balance draining vulnerabilities
- Missing Signer Check — detects authorization failures that can combine with overflow to amplify impact