Lamport Underflow
Detects lamport transfers that may exceed available balance, causing underflow, account closure, or rent-exempt violations.
Lamport Underflow
Overview
The lamport underflow detector identifies lamport transfer operations that could drain more lamports than an account contains. It detects three patterns: transfers using unchecked arithmetic in the amount, multiple consecutive transfers from the same account without cumulative balance validation, and full balance transfers that do not reserve the rent-exempt minimum. For remediation steps, see the Lamport Underflow Remediation.
Why This Is an Issue
Lamport transfers that exceed an account’s balance cause different failure modes depending on context:
- Transaction failure and DoS: The Solana runtime rejects transactions where an account’s lamport balance goes negative. Attackers who can trigger this failure path cause denial-of-service.
- Account closure: Transferring all lamports from an account closes it, destroying any stored data. If the account is needed for subsequent operations, this causes permanent data loss.
- Rent-exempt violation: Accounts that fall below the rent-exempt minimum are subject to rent collection and eventual garbage collection, causing gradual data loss.
- Arithmetic underflow in amount: When the transfer amount itself is computed using unchecked arithmetic (e.g.,
base + fee), an overflow in the computation can produce an amount that exceeds available balance.
Multiple transfers from the same source account compound the risk — each individual transfer may be within bounds, but their cumulative total may exceed the balance.
How to Resolve
// Before: Vulnerable -- no balance check, unchecked arithmetic
let amount = base_amount + fee; // Can overflow
**source.try_borrow_mut_lamports()? -= amount;
**dest.try_borrow_mut_lamports()? += amount;
// After: Fixed -- checked arithmetic and balance validation
let amount = base_amount.checked_add(fee)
.ok_or(ProgramError::ArithmeticOverflow)?;
let balance = **source.try_borrow_lamports()?;
if balance < amount {
return Err(ProgramError::InsufficientFunds);
}
**source.try_borrow_mut_lamports()? -= amount;
**dest.try_borrow_mut_lamports()? += amount;
Examples
Vulnerable Code: Multiple Transfers
pub fn distribute(accounts: &[AccountInfo]) -> ProgramResult {
let source = &accounts[0];
// No cumulative balance check
**source.try_borrow_mut_lamports()? -= 1_000_000;
**accounts[1].try_borrow_mut_lamports()? += 1_000_000;
**source.try_borrow_mut_lamports()? -= 2_000_000;
**accounts[2].try_borrow_mut_lamports()? += 2_000_000;
// Source may not have 3,000,000 lamports total
Ok(())
}
Vulnerable Code: Full Balance Drain
pub fn close_account(accounts: &[AccountInfo]) -> ProgramResult {
let source = &accounts[0];
let dest = &accounts[1];
let balance = **source.try_borrow_lamports()?;
// Transfers entire balance -- account becomes unusable
**source.try_borrow_mut_lamports()? -= balance;
**dest.try_borrow_mut_lamports()? += balance;
Ok(())
}
Fixed Code
pub fn distribute(accounts: &[AccountInfo]) -> ProgramResult {
let source = &accounts[0];
let total_needed = 1_000_000u64
.checked_add(2_000_000)
.ok_or(ProgramError::ArithmeticOverflow)?;
let balance = **source.try_borrow_lamports()?;
if balance < total_needed {
return Err(ProgramError::InsufficientFunds);
}
**source.try_borrow_mut_lamports()? -= 1_000_000;
**accounts[1].try_borrow_mut_lamports()? += 1_000_000;
**source.try_borrow_mut_lamports()? -= 2_000_000;
**accounts[2].try_borrow_mut_lamports()? += 2_000_000;
Ok(())
}
Example JSON Finding
{
"detector": "lamport-underflow",
"severity": "critical",
"confidence": 0.78,
"title": "Full Balance Transfer Without Rent-Exempt Reserve",
"description": "Account transfers entire lamport balance without reserving rent-exempt minimum.",
"cwe_ids": [672]
}
Detection Methodology
- Transfer collection: Scans all blocks for
TransferLamportsstatements, grouping by source account variable. - Arithmetic check: Examines transfer amount expressions for unchecked arithmetic (add, sub, mul operations).
- Multiple transfer analysis: Flags accounts with two or more transfers when no balance validation (comparison with
AccountLamports) is found between them. - Full drain detection: Identifies transfers where the amount expression is
AccountLamports(entire balance), checking whether rent-exemption syscalls appear in the function.
Limitations
False positives: Programs that validate cumulative balance in called helper functions will still be flagged. Intentional account closure (where draining is expected behavior) triggers the rent-exempt finding. Programs where multiple transfers are in different execution branches (not cumulative) may be incorrectly flagged. False negatives: Balance validation performed via CPI or complex conditional chains may not be recognized. Transfers performed via CPI to the System Program (rather than direct lamport manipulation) are not detected.
Related Detectors
- Critical Truncation — detects truncation in lamport amounts
- Unchecked Arithmetic — detects general unchecked arithmetic
- Unchecked Fee/Rent Math — detects unchecked fee calculations
- Close Account Drain — detects improper account closure