Skip to main content
Sigvex

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_needed without 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 init constraints receive reduced confidence since they handle atomic initialization.

References