Token Init Race
Detects token account initialization race conditions.
Token Init Race
Overview
Remediation Guide: How to Fix Token Init Race
The token init race detector identifies race conditions in SPL token account initialization where accounts may be used before initialization completes or where init_if_needed patterns lack ownership validation. An attacker can front-run the initialization transaction to substitute a malicious account at the expected address, bypassing the initialization branch and injecting arbitrary data.
Sigvex detects this by tracking token program CPI calls, init_if_needed data-length checks, and ownership/discriminator validation events, flagging token operations on recently initialized but unvalidated accounts.
Why This Is an Issue
- Front-running: an attacker monitors the mempool for initialization transactions and creates a pre-initialized account at the expected address before the legitimate transaction lands.
- Account substitution: if the program uses
init_if_neededwithout ownership validation, an attacker-owned account passes the “already initialized” check. - Token theft: performing transfers or mints on an attacker-substituted token account sends funds to the attacker.
CWE mapping: CWE-362 (Race Condition), CWE-367 (TOCTOU Race Condition).
How to Resolve
Native Solana
// After init CPI, validate immediately
invoke(&create_account_ix, &[payer.clone(), token_account.clone()])?;
// Validate ownership and mint
require!(*token_account.owner == spl_token::ID, InvalidOwner);
let token_data = spl_token::state::Account::unpack(&token_account.data.borrow())?;
require!(token_data.mint == expected_mint, InvalidMint);
require!(token_data.owner == expected_owner, InvalidTokenOwner);
Anchor
#[derive(Accounts)]
pub struct InitToken<'info> {
// init constraint atomically creates and validates
#[account(
init,
payer = user,
token::mint = mint,
token::authority = user,
)]
pub token_account: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
#[account(mut)]
pub user: Signer<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
Examples
Vulnerable
// init_if_needed without ownership check
if account.data_len() == 0 {
invoke(&init_account_ix, &accounts)?;
}
// Attacker could pre-create account with wrong owner
let token_data = Account::unpack(&account.data.borrow())?;
transfer(token_data.owner, amount)?;
Fixed
if account.data_len() == 0 {
invoke(&init_account_ix, &accounts)?;
}
// Always validate after init path
require!(*account.owner == spl_token::ID, InvalidOwner);
let token_data = Account::unpack(&account.data.borrow())?;
require!(token_data.mint == expected_mint, InvalidMint);
JSON Finding
{
"detector": "token-init-race",
"severity": "Medium",
"confidence": 0.65,
"title": "Token Operation After Unvalidated Init",
"description": "Token operation performed on an account initialized via CPI but not validated.",
"cwe": [367]
}
Detection Methodology
The detector tracks three categories: validation events (ownership, key, signer checks), token init CPI calls, and subsequent token operations. It flags init_if_needed patterns (data length checks) without ownership validation and token operations on recently CPI-initialized accounts without post-init validation.
Limitations
- Token program identification relies on heuristic name matching of syscalls and may miss custom token programs.
- The detector cannot track front-running across separate transactions; it focuses on intra-instruction race windows.
- Anchor programs with
initconstraints receive reduced confidence since they handle atomic initialization.
Related Detectors
- Generic Init Frontrun - detects frontrunnable initialization for non-token accounts.
- Missing Empty Account Check - detects missing empty checks before init.