Skip to main content
Sigvex

Lamport Underflow Remediation

How to fix lamport transfers that may exceed available balance or violate rent-exempt minimums.

Lamport Underflow Remediation

Overview

Related Detector: Lamport Underflow

Lamport transfers that exceed an account’s balance cause transaction failures, accidental account closure, or rent-exempt violations. The fix is to validate cumulative transfer amounts against the source balance, use checked arithmetic for amount calculations, and reserve the rent-exempt minimum when the account must remain active.

Before (Vulnerable)

pub fn withdraw(
    accounts: &[AccountInfo],
    amount: u64,
    fee: u64,
) -> ProgramResult {
    // VULNERABLE: unchecked arithmetic + no balance validation
    let total = amount + fee;
    **accounts[0].try_borrow_mut_lamports()? -= total;
    **accounts[1].try_borrow_mut_lamports()? += amount;
    **accounts[2].try_borrow_mut_lamports()? += fee;
    Ok(())
}

After (Fixed)

pub fn withdraw(
    accounts: &[AccountInfo],
    amount: u64,
    fee: u64,
) -> ProgramResult {
    // FIXED: checked arithmetic
    let total = amount
        .checked_add(fee)
        .ok_or(ProgramError::ArithmeticOverflow)?;

    // FIXED: balance validation with rent-exempt reserve
    let rent = Rent::get()?;
    let min_balance = rent.minimum_balance(accounts[0].data_len());
    let available = (**accounts[0].try_borrow_lamports()?)
        .checked_sub(min_balance)
        .ok_or(ProgramError::InsufficientFunds)?;

    if available < total {
        return Err(ProgramError::InsufficientFunds);
    }

    **accounts[0].try_borrow_mut_lamports()? -= total;
    **accounts[1].try_borrow_mut_lamports()? += amount;
    **accounts[2].try_borrow_mut_lamports()? += fee;
    Ok(())
}

Alternative Mitigations

1. Batch validation for multiple transfers

pub fn multi_transfer(
    source: &AccountInfo,
    destinations: &[(AccountInfo, u64)],
) -> ProgramResult {
    // Calculate total needed upfront
    let total: u64 = destinations.iter()
        .try_fold(0u64, |acc, (_, amount)| {
            acc.checked_add(*amount)
        })
        .ok_or(ProgramError::ArithmeticOverflow)?;

    // Single balance check for cumulative amount
    let balance = **source.try_borrow_lamports()?;
    if balance < total {
        return Err(ProgramError::InsufficientFunds);
    }

    // Execute transfers
    for (dest, amount) in destinations {
        **source.try_borrow_mut_lamports()? -= amount;
        **dest.try_borrow_mut_lamports()? += amount;
    }
    Ok(())
}

2. Safe account closure pattern

When intentionally closing an account, transfer remaining lamports and zero the data:

pub fn close_account(
    source: &AccountInfo,
    destination: &AccountInfo,
) -> ProgramResult {
    // Transfer all lamports to destination
    let lamports = **source.try_borrow_lamports()?;
    **source.try_borrow_mut_lamports()? = 0;
    **destination.try_borrow_mut_lamports()? += lamports;

    // Zero account data to prevent resurrection attacks
    let mut data = source.try_borrow_mut_data()?;
    data.fill(0);
    Ok(())
}

3. Rent-aware helper

fn transferable_balance(account: &AccountInfo) -> Result<u64, ProgramError> {
    let rent = Rent::get()?;
    let min_balance = rent.minimum_balance(account.data_len());
    let balance = **account.try_borrow_lamports()?;
    balance
        .checked_sub(min_balance)
        .ok_or(ProgramError::InsufficientFunds)
}

Common Mistakes

Mistake 1: Checking balance after transfer

// WRONG: underflow already occurred
**source.try_borrow_mut_lamports()? -= amount;
if **source.try_borrow_lamports()? < min_balance {
    // Too late -- lamports already deducted
}

Always check before the transfer, not after.

Mistake 2: Not accounting for rent-exempt minimum

// WRONG: checking raw balance without rent reserve
let balance = **source.try_borrow_lamports()?;
if balance >= amount {
    **source.try_borrow_mut_lamports()? -= amount;
    // Account may now be below rent-exempt minimum!
}

Subtract the rent-exempt minimum from the available balance before comparing.

Mistake 3: Checking individual transfers but not cumulative

let balance = **source.try_borrow_lamports()?;
// WRONG: each check uses the original balance, not the remaining
if balance >= amount_a { transfer_a()?; }
if balance >= amount_b { transfer_b()?; }
// Combined amount_a + amount_b may exceed balance

Track the running balance or calculate the cumulative total upfront.

References