Severity: MEDIUM
This example demonstrates the Account Initialization Denial of Service vulnerability, where using init constraint allows attackers to block users by front-running account creation.
When a program uses the init constraint to create a user's account, the instruction will fail if the account already exists. This creates an attack vector:
- Attacker monitors the mempool for pending transactions
- Attacker sees a user calling an instruction that initializes their account
- Attacker front-runs by creating the user's account first
- User's transaction fails because the account already exists
- User is permanently blocked from using that instruction!
This vulnerability can:
- Block reward claims - Users cannot claim tokens they earned
- Prevent onboarding - New users cannot complete registration
- Disrupt airdrops - Recipients cannot receive distributed tokens
- Cause financial loss - Users pay gas fees for failed transactions
- Why
initconstraint fails for existing accounts - How
init_if_neededsolves the problem - The importance of idempotent account creation
- Front-running attack patterns on Solana
initialize- Sets up reward configurationclaim_vulnerable- Claims rewards withinit(VULNERABLE)claim_secure- Claims rewards withinit_if_needed(SECURE)
Located in: src/instructions/claim_vulnerable.rs
#[derive(Accounts)]
pub struct ClaimVulnerable<'info> {
pub config: Account<'info, RewardConfig>,
/// ⚠️ VULNERABLE: Using `init` means this FAILS if account exists!
#[account(
init,
payer = user,
space = UserReward::LEN,
seeds = [b"user_reward", config.key().as_ref(), user.key().as_ref()],
bump
)]
pub user_reward: Account<'info, UserReward>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}TIMELINE OF ATTACK
==================
Block N:
┌─────────────────────────────────────────────────────────┐
│ User submits: claim_vulnerable() │
│ - Will create user's reward account │
│ - Will mark user as claimed │
└─────────────────────────────────────────────────────────┘
│
│ Transaction enters mempool
▼
┌─────────────────────────────────────────────────────────┐
│ ATTACKER monitors mempool │
│ Sees user's pending transaction │
│ Extracts: user pubkey, config address │
└─────────────────────────────────────────────────────────┘
│
│ Attacker front-runs with higher priority fee
▼
Block N (earlier in block):
┌─────────────────────────────────────────────────────────┐
│ ATTACKER's transaction executes FIRST: │
│ Creates user_reward PDA for victim │
│ │
│ ✓ User's account now exists (created by attacker!) │
└─────────────────────────────────────────────────────────┘
│
▼
Block N (later in block):
┌─────────────────────────────────────────────────────────┐
│ USER's transaction executes: │
│ claim_vulnerable() │
│ │
│ Anchor checks: init user_reward │
│ ❌ FAILS! Account already exists! │
│ │
│ User cannot claim rewards! │
└─────────────────────────────────────────────────────────┘
Located in: src/instructions/claim_secure.rs
#[derive(Accounts)]
pub struct ClaimSecure<'info> {
pub config: Account<'info, RewardConfig>,
/// ✅ SECURE: Using `init_if_needed` handles both cases!
#[account(
init_if_needed,
payer = user,
space = UserReward::LEN,
seeds = [b"user_reward", config.key().as_ref(), user.key().as_ref()],
bump
)]
pub user_reward: Account<'info, UserReward>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}| Scenario | init |
init_if_needed |
|---|---|---|
| Account doesn't exist | ✅ Creates it | ✅ Creates it |
| Account already exists | ❌ FAILS | ✅ Uses existing |
| Attacker front-runs | ❌ User blocked | ✅ User succeeds |
WITH init_if_needed:
=====================
Attacker creates user's account first?
│
▼
User calls claim_secure()
│
▼
┌─────────────────────────────────────────────────────────┐
│ Anchor checks: init_if_needed user_reward │
│ │
│ Account exists? YES │
│ Action: Use existing account ✅ │
│ │
│ Check if already claimed? Handle gracefully │
│ │
│ User successfully claims rewards! │
│ Attack neutralized! │
└─────────────────────────────────────────────────────────┘
Use init_if_needed when:
- Creating user-specific accounts (ATAs, reward accounts, profiles)
- The operation should be idempotent
- Front-running is a concern
- User experience matters
To use init_if_needed, you must enable it in Cargo.toml:
[dependencies]
anchor-lang = { version = "0.32.1", features = ["init-if-needed"] }When using init_if_needed, always check the account state:
pub fn handler(ctx: Context<ClaimSecure>) -> Result<()> {
let user_reward = &mut ctx.accounts.user_reward;
// ✅ Check if already claimed (account might have existed)
if user_reward.claimed {
msg!("User has already claimed rewards");
return Ok(());
}
// Process claim...
}| Aspect | init |
init_if_needed |
|---|---|---|
| Existing account | Fails | Succeeds |
| Idempotent | No | Yes |
| DoS resistant | No | Yes |
| Use case | One-time creation | User-facing operations |
- Use
initfor user-facing account creation - Assume users will always have uninitialized accounts
- Ignore front-running attack vectors
- Use
init_if_neededfor user accounts - Make user-facing operations idempotent
- Check account state when using
init_if_needed - Consider mempool visibility in your threat model
cd 8-ata-initialization-dos
anchor buildRemember: User-facing account creation should be idempotent. Use init_if_needed to prevent front-running DoS attacks!