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.
Related Detectors
- Account Close Discriminator - detects closes without discriminator zeroing.
- Account Close Reopen Race - detects TOCTOU races during close/reopen.
- Account Use After Close - detects reads/writes after account closure.