Account Close Balance Check Remediation
How to fix incomplete account close operations that leave data intact after draining lamports.
Remediating Incomplete Account Close Operations
Overview
Related Detector: Account Close Balance Check
When closing a Solana account, both lamport drainage and data zeroing must occur. If only lamports are drained, the account data persists and an attacker can re-fund the account, resurrecting it with old state. The fix is to zero all account data — especially the discriminator bytes — before or after draining lamports.
Recommended Fix
Before (Vulnerable)
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn close_vault(accounts: &[AccountInfo]) -> ProgramResult {
let vault = &accounts[0];
let owner = &accounts[1];
// Only drains lamports -- data remains intact
**owner.lamports.borrow_mut() += vault.lamports();
**vault.lamports.borrow_mut() = 0;
Ok(())
}
After (Fixed)
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn close_vault(accounts: &[AccountInfo]) -> ProgramResult {
let vault = &accounts[0];
let owner = &accounts[1];
// Zero all data first (prevents resurrection with old state)
vault.data.borrow_mut().fill(0);
// Then drain lamports
**owner.lamports.borrow_mut() += vault.lamports();
**vault.lamports.borrow_mut() = 0;
Ok(())
}
Anchor (Preferred)
#[derive(Accounts)]
pub struct CloseVault<'info> {
#[account(mut, close = owner)]
pub vault: Account<'info, Vault>,
#[account(mut)]
pub owner: SystemAccount<'info>,
}
Anchor’s close constraint automatically zeroes the discriminator and transfers all lamports to the specified recipient.
Alternative Mitigations
-
Realloc to zero bytes: After draining lamports, resize the account data to zero bytes using
realloc. This removes all data, but note that the account must have sufficient lamports for rent exemption if you realloc before draining. -
Two-step close pattern: First zero the discriminator in one instruction, then drain lamports in a second instruction. This is less common but acceptable when close operations span multiple instructions.
-
Assign to system program: After draining lamports, reassign account ownership to the system program. This prevents the account from being used by the original program even if re-funded.
vault.assign(&solana_program::system_program::id());
Common Mistakes
- Zeroing only the discriminator but not other fields: While zeroing the discriminator prevents type-safe deserialization, sensitive data (balances, authorities) may still be readable. Zero the entire data buffer.
- Zeroing after lamport drain in a different transaction: The account may be re-funded between transactions. Always zero data and drain lamports in the same instruction.
- Forgetting to zero in error paths: If the close operation can fail partway through, ensure data is zeroed even on partial failure.
- Using
memseton a slice of data instead of the full buffer: Partial zeroing leaves exploitable state behind.