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.
Related Detectors
- Missing Rent Check - detects variable transfers without rent awareness.
- Rent Epoch Validation - detects improper rent epoch handling.