Account Close Discriminator
Detects account closes without discriminator zeroing, enabling resurrection attacks.
Account Close Discriminator
Overview
Remediation Guide: How to Fix Account Close Discriminator
The account close discriminator detector identifies Solana programs that close accounts (by draining lamports or issuing close CPIs) without zeroing the 8-byte discriminator field. Anchor programs use discriminators to validate account types during deserialization. If the discriminator is not zeroed on close, an attacker can recreate the account at the same PDA address, and the old discriminator passes type validation, enabling resurrection attacks where the recreated account is treated as the original type despite having fresh, attacker-controlled state.
Sigvex tracks close operations (lamport drains and close CPIs) alongside discriminator-region stores, flagging closes where no zero-value write to offset 0-7 occurs nearby.
Why This Is an Issue
- Type validation bypass: the resurrected account retains the original discriminator, passing Anchor’s or native discriminator checks.
- State confusion: code that validates the account type and then reads fields gets fresh (zero or attacker-controlled) data while believing it is the original account.
- Compound with resurrection: this vulnerability combines with account resurrection to form a complete exploit chain: close without zeroing, re-derive PDA, create new account, pass type checks.
CWE mapping: CWE-672 (Operation on a Resource after Expiration or Release).
How to Resolve
Native Solana
pub fn close_account(accounts: &[AccountInfo]) -> ProgramResult {
let account = &accounts[0];
let destination = &accounts[1];
// CRITICAL: Zero discriminator before draining lamports
let mut data = account.data.borrow_mut();
data[0..8].fill(0);
drop(data);
// Drain lamports
**destination.try_borrow_mut_lamports()? += account.lamports();
**account.try_borrow_mut_lamports()? = 0;
Ok(())
}
Anchor
// Anchor's close constraint zeros discriminator automatically
#[derive(Accounts)]
pub struct Close<'info> {
#[account(mut, close = destination)]
pub my_account: Account<'info, MyAccount>,
#[account(mut)]
pub destination: SystemAccount<'info>,
}
Examples
Vulnerable
// Drains lamports but does NOT zero discriminator
**destination.try_borrow_mut_lamports()? += account.lamports();
**account.try_borrow_mut_lamports()? = 0;
// Discriminator still valid — resurrection possible!
Fixed
// Zero discriminator FIRST
account.data.borrow_mut()[0..8].fill(0);
// Then drain lamports
**destination.try_borrow_mut_lamports()? += account.lamports();
**account.try_borrow_mut_lamports()? = 0;
JSON Finding
{
"detector": "account-close-discriminator",
"severity": "High",
"confidence": 0.80,
"title": "Account Close Without Discriminator Zeroing",
"description": "Account closed without zeroing the 8-byte discriminator field, enabling resurrection attacks.",
"cwe": [672]
}
Detection Methodology
The detector identifies close operations (full lamport drains via AccountLamports and close CPI calls with instruction discriminator 9) and tracks StoreAccountData writes to the discriminator region (offsets 0-7) with zero values and 8-byte size. If a close operation has no nearby discriminator zeroing (within 2 blocks), a finding is generated.
Limitations
- Close CPI detection uses heuristic instruction discriminator matching and may miss custom close patterns.
- The detector requires explicit zero writes and cannot track zeroing via
fill(0)across the full data range if it does not use the expected HIR pattern. - Anchor programs with
closeconstraints receive significantly reduced confidence.
Related Detectors
- Account Resurrection - detects resurrection via PDA re-derivation.
- Account Close Reopen Race - detects TOCTOU races during close/reopen.
- Account Use After Close - detects use of accounts after closure.