Skip to main content
Sigvex

Account Resurrection

Detects accounts that could be resurrected after closure via PDA re-derivation.

Account Resurrection

Overview

Remediation Guide: How to Fix Account Resurrection

The account resurrection detector identifies Solana programs where a closed account could be recreated at the same address by re-deriving its PDA with the same seeds. When an account is closed (lamports drained) but its PDA seeds are static and predictable, an attacker can create a new account at the identical address with fresh state, causing any code that cached the original address to operate on unexpected data.

Sigvex detects this by tracking lamport-drain operations alongside PDA derivation syscalls (sol_create_program_address, find_program_address) and checking whether the seeds include dynamic components (nonces, counters, or epoch values) that would prevent re-derivation.

Why This Is an Issue

Account resurrection is dangerous because:

  • Address reuse: the resurrected account has the same public key as the original, so any cached reference now points to fresh, potentially malicious state.
  • State reset: the new account starts with zero balances and no history, allowing the attacker to bypass checks that depend on accumulated state.
  • Cross-program confusion: other programs that trusted the original account’s state will unknowingly interact with the replacement.

The attack flow is: (1) close the account to drain lamports, (2) re-derive the PDA with the same static seeds, (3) create a new account at that address with attacker-controlled initial state.

CWE mapping: CWE-672 (Operation on a Resource after Expiration or Release).

How to Resolve

Native Solana

// Include a nonce/counter in PDA seeds that increments on close
pub fn close_account(accounts: &[AccountInfo], nonce: u64) -> ProgramResult {
    let account = &accounts[0];
    let destination = &accounts[1];
    let registry = &accounts[2];

    // Record the close in a registry with incrementing nonce
    let mut registry_data = registry.data.borrow_mut();
    let current_nonce = u64::from_le_bytes(registry_data[0..8].try_into()?);
    registry_data[0..8].copy_from_slice(&(current_nonce + 1).to_le_bytes());

    // Zero account data before closing
    account.data.borrow_mut().fill(0);

    // Drain lamports
    **destination.try_borrow_mut_lamports()? += account.lamports();
    **account.try_borrow_mut_lamports()? = 0;
    Ok(())
}

Anchor

#[derive(Accounts)]
pub struct CloseAccount<'info> {
    // Anchor's close constraint zeroes data and drains lamports
    #[account(mut, close = destination)]
    pub my_account: Account<'info, MyAccount>,
    #[account(mut)]
    pub destination: SystemAccount<'info>,
}

Examples

Vulnerable

pub fn close(accounts: &[AccountInfo]) -> ProgramResult {
    let account = &accounts[0];
    let destination = &accounts[1];
    // Only drains lamports — data and PDA seeds remain usable
    **destination.try_borrow_mut_lamports()? += account.lamports();
    **account.try_borrow_mut_lamports()? = 0;
    Ok(())
}

Fixed

pub fn close(accounts: &[AccountInfo]) -> ProgramResult {
    let account = &accounts[0];
    let destination = &accounts[1];
    // Zero all data to invalidate discriminator
    account.data.borrow_mut().fill(0);
    // Drain lamports
    **destination.try_borrow_mut_lamports()? += account.lamports();
    **account.try_borrow_mut_lamports()? = 0;
    Ok(())
}

JSON Finding

{
  "detector": "account-resurrection",
  "severity": "High",
  "confidence": 0.80,
  "title": "Account Resurrection Vulnerability",
  "description": "Account is closed but could be resurrected via PDA re-derivation with static seeds.",
  "cwe": [672]
}

Detection Methodology

The detector tracks three categories of operations: lamport drains (close patterns), PDA derivation syscalls with seed analysis, and revival protection patterns (epoch/nonce checks). When a close operation targets an account with no revival protection and PDA derivations use static seeds, the detector reports a high-severity finding. Additionally, it checks whether the function writes to an account after closing it without zeroing data first.

Limitations

  • The detector conservatively treats all CPI calls as potential close operations, which may produce false positives.
  • Dynamic seed analysis is limited to direct constant/variable classification and cannot track complex seed derivation chains.
  • Cross-function resurrection patterns (close in one instruction, re-derive in another) are not detected.

References