Skip to main content
Sigvex

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.
  • a major DeFi lending venue 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 close constraints receive significantly reduced confidence.

References