Account Close Reopen Race Remediation
How to fix TOCTOU race conditions from account close/reopen between transactions.
Account Close Reopen Race Remediation
Overview
Detector Reference: Account Close Reopen Race
This guide explains how to fix race conditions where an account can be closed and reopened with different data between the time it is read and the time it is used. These TOCTOU vulnerabilities occur around CPI boundaries.
Recommended Fix
After every CPI call, revalidate the ownership, discriminator, and any cached data for accounts that will be used further:
// Before CPI: read data
let data = MyStruct::try_from_slice(&account.data.borrow())?;
let cached_amount = data.amount;
// CPI call
invoke(&instruction, &[account.clone(), other.clone()])?;
// After CPI: REVALIDATE everything
require!(*account.owner == program_id, ErrorCode::InvalidOwner);
let fresh_data = MyStruct::try_from_slice(&account.data.borrow())?;
require!(fresh_data.discriminator == MY_DISCRIMINATOR, ErrorCode::InvalidType);
// Use fresh data, not cached
process_amount(fresh_data.amount)?;
Alternative Mitigations
- Generation counters: store a monotonically increasing generation field in account data. Validate that the generation matches the expected value after CPI.
- Minimize post-CPI usage: restructure your program to perform all account reads after the last CPI call, eliminating the race window.
- Avoid caching: re-read account data from the
AccountInfoeach time it is needed rather than caching in local variables.
Common Mistakes
- Revalidating only the owner: an attacker may close and reopen an account owned by the same program but with different data. Check the discriminator and critical fields too.
- Assuming PDA safety: while PDAs reduce the attack surface (attacker needs the same seeds), they do not eliminate close-reopen races entirely.
- Ignoring
remaining_accounts: accounts passed viaremaining_accountsin Anchor bypass automatic type validation and must be manually checked.