Account Close Balance Check
Detects incomplete account close operations where lamports are drained but account data is not zeroed, enabling account resurrection attacks.
Account Close Balance Check
Overview
Remediation Guide: How to Fix Incomplete Account Closes
The account close balance check detector identifies Solana programs that drain all lamports from an account (closing it) without zeroing the account data. When closing a Solana account, two operations must occur atomically: transferring all lamports to another account and zeroing the account data, especially the discriminator. If only the lamports are drained, an attacker can re-fund the account with the minimum rent-exempt balance, resurrecting it with the original data intact.
Sigvex tracks lamport drain operations (TransferLamports where the amount equals the full account balance) and data zeroing operations (StoreAccountData writing zero to discriminator offsets 0-8). Accounts that are drained without a corresponding data zeroing operation are flagged. CWE mapping: CWE-672 (Operation on a Resource after Expiration or Release).
Why This Is an Issue
Account resurrection is a well-known Solana vulnerability class. When an account is closed by draining lamports but its data remains intact:
- Data persistence: The Solana runtime garbage-collects accounts with zero lamports, but there is a window where the account data is still accessible. An attacker can re-fund the account before garbage collection.
- Initialization bypass: Many programs check whether an account is “initialized” by reading its discriminator. A resurrected account still has the old discriminator, passing initialization checks without going through the proper
initializeflow. - State replay: Old account state (balances, permissions, configuration) is restored exactly as it was before the close, enabling replay of previously-settled obligations.
How to Resolve
Native Rust
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
// Before: Vulnerable -- drains lamports without zeroing data
pub fn close_account_vulnerable(accounts: &[AccountInfo]) -> ProgramResult {
let target = &accounts[0];
let recipient = &accounts[1];
**recipient.lamports.borrow_mut() += target.lamports();
**target.lamports.borrow_mut() = 0;
// Data remains intact -- account can be resurrected!
Ok(())
}
// After: Zero discriminator and data before draining lamports
pub fn close_account_safe(accounts: &[AccountInfo]) -> ProgramResult {
let target = &accounts[0];
let recipient = &accounts[1];
// Step 1: Zero all account data (especially the discriminator)
let mut data = target.data.borrow_mut();
data.fill(0);
drop(data);
// Step 2: Drain lamports
**recipient.lamports.borrow_mut() += target.lamports();
**target.lamports.borrow_mut() = 0;
Ok(())
}
Anchor
// Anchor handles close safely via the close constraint
#[derive(Accounts)]
pub struct CloseVault<'info> {
#[account(mut, close = recipient)]
pub vault: Account<'info, Vault>,
#[account(mut)]
pub recipient: SystemAccount<'info>,
}
Examples
Vulnerable Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn settle_and_close(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let escrow = &accounts[0];
let seller = &accounts[1];
let buyer = &accounts[2];
// Transfer escrowed funds to seller
**seller.lamports.borrow_mut() += amount;
**escrow.lamports.borrow_mut() -= amount;
// Close escrow -- drain remaining lamports
let remaining = escrow.lamports();
**buyer.lamports.borrow_mut() += remaining;
**escrow.lamports.borrow_mut() = 0;
// BUG: escrow data not zeroed -- attacker re-funds escrow,
// old state is restored, and they can claim the escrow again
Ok(())
}
Fixed Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn settle_and_close(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let escrow = &accounts[0];
let seller = &accounts[1];
let buyer = &accounts[2];
**seller.lamports.borrow_mut() += amount;
**escrow.lamports.borrow_mut() -= amount;
// Zero all account data before closing
escrow.data.borrow_mut().fill(0);
let remaining = escrow.lamports();
**buyer.lamports.borrow_mut() += remaining;
**escrow.lamports.borrow_mut() = 0;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "account-close-balance-check",
"severity": "high",
"confidence": 0.80,
"description": "Account v1 is closed (lamports drained) without zeroing its data. An attacker can re-fund the account and resurrect it with the original data, potentially bypassing initialization checks or re-using old state.",
"location": { "function": "settle_and_close", "offset": 5 }
}
Detection Methodology
The detector performs a two-pass analysis over the function’s control-flow graph:
- Operation collection: Scans all basic blocks for lamport drain operations (where the transfer amount equals the full account balance) and data zeroing operations (writes of zero to discriminator offsets 0-8).
- Cross-reference: For each drained account, checks whether a corresponding data zeroing operation exists anywhere in the function. Accounts drained without zeroing generate a finding.
Context modifiers:
- PDA-derived accounts: confidence reduced by 50% (resurrection requires same seeds + program ID)
- Key-validated accounts: confidence reduced by 30% (explicit identity check reduces false positives)
- Anchor programs with discriminator validation: confidence reduced by 80%
- Admin/initialization functions: confidence reduced by 40%
Limitations
False positives:
- Programs that use a separate “garbage collector” instruction to zero data after close may be flagged, since the zeroing occurs in a different instruction.
- Accounts closed via CPI to another program (which handles zeroing internally) will be flagged because the zeroing is not visible in the calling function.
False negatives:
- Partial data zeroing (zeroing only some fields but not the discriminator) is not detected as insufficient.
- Close operations spread across multiple instructions within a single transaction are not tracked cross-instruction.
Related Detectors
- Close Account Drain — detects unauthorized account closing
- Account Reinitialization — detects accounts that can be re-initialized after close