Skip to main content
Sigvex

Rent Withdrawal Balance

Detects partial lamport withdrawals without rent-exemption balance checks.

Rent Withdrawal Balance

Overview

Remediation Guide: How to Fix Rent Withdrawal Balance

The rent withdrawal balance detector identifies partial lamport withdrawals from program-owned accounts that do not verify the remaining balance stays above the rent-exempt minimum. Unlike full drains (which intentionally close accounts), partial withdrawals should preserve the account by keeping it rent-exempt. Without this check, a withdrawal can leave the account vulnerable to garbage collection.

Sigvex scans for TransferLamports operations with non-full-drain amounts and checks whether the source account has a prior balance comparison involving AccountLamports.

Why This Is an Issue

  • Account deletion: a withdrawal that drops the balance below rent exemption causes the runtime to garbage-collect the account.
  • Permanent data loss: unlike a full drain (intentional close), a partial withdrawal that triggers deletion is an unintended consequence.
  • Attacker exploitation: if the withdrawal amount is user-controlled, an attacker can craft amounts that leave accounts just below rent exemption.

CWE mapping: CWE-664 (Improper Control of a Resource Through its Lifetime).

How to Resolve

Native Solana

let rent = Rent::get()?;
let min_balance = rent.minimum_balance(account.data_len());
let remaining = account.lamports().checked_sub(amount)
    .ok_or(ProgramError::InsufficientFunds)?;

require!(remaining >= min_balance, ErrorCode::WouldBreakRentExemption);

**account.try_borrow_mut_lamports()? -= amount;
**destination.try_borrow_mut_lamports()? += amount;

Anchor

// Add constraint for rent preservation
#[account(
    mut,
    constraint = account.to_account_info().lamports() - amount >= 
        Rent::get()?.minimum_balance(account.to_account_info().data_len())
)]
pub account: Account<'info, MyState>,

Examples

Vulnerable

// Partial withdrawal without balance check
**account.try_borrow_mut_lamports()? -= user_amount;
**destination.try_borrow_mut_lamports()? += user_amount;

Fixed

let rent = Rent::get()?;
let min = rent.minimum_balance(account.data_len());
require!(account.lamports() - user_amount >= min, NotRentExempt);
**account.try_borrow_mut_lamports()? -= user_amount;
**destination.try_borrow_mut_lamports()? += user_amount;

JSON Finding

{
  "detector": "rent-withdrawal-balance",
  "severity": "High",
  "confidence": 0.75,
  "title": "Partial Withdrawal Without Rent-Exemption Check",
  "description": "Lamports transferred without verifying remaining balance stays above rent-exempt minimum.",
  "cwe": [664]
}

Detection Methodology

The detector collects accounts that have balance checks (branch conditions involving AccountLamports in comparison operations) and flags TransferLamports where the source account is not in the balance-checked set. Full drain operations (where the amount is AccountLamports) are excluded since they indicate intentional closure.

Limitations

  • Balance checks on a different account than the transfer source are not matched.
  • The detector cannot verify that a balance check actually compares against the rent-exempt minimum versus any other threshold.
  • Anchor programs receive reduced confidence since Anchor’s close constraint handles lamport management.

References