Skip to content

Latest commit

 

History

History

README.md

Account Initialization DoS - Security Example

Vulnerability Overview

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.

The Problem

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:

  1. Attacker monitors the mempool for pending transactions
  2. Attacker sees a user calling an instruction that initializes their account
  3. Attacker front-runs by creating the user's account first
  4. User's transaction fails because the account already exists
  5. User is permanently blocked from using that instruction!

Real-World Impact

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

What You'll Learn

  1. Why init constraint fails for existing accounts
  2. How init_if_needed solves the problem
  3. The importance of idempotent account creation
  4. Front-running attack patterns on Solana

Program Structure

  1. initialize - Sets up reward configuration
  2. claim_vulnerable - Claims rewards with init (VULNERABLE)
  3. claim_secure - Claims rewards with init_if_needed (SECURE)

The Vulnerability: init Constraint

Vulnerable Code

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>,
}

The Attack

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!                              │
└─────────────────────────────────────────────────────────┘

The Fix: init_if_needed Constraint

Secure Code

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>,
}

Why This Works

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

Attack Neutralized

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!                                     │
└─────────────────────────────────────────────────────────┘

Important Considerations

When to Use init_if_needed

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

Feature Flag Required

To use init_if_needed, you must enable it in Cargo.toml:

[dependencies]
anchor-lang = { version = "0.32.1", features = ["init-if-needed"] }

Security Note

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...
}

Key Differences

Aspect init init_if_needed
Existing account Fails Succeeds
Idempotent No Yes
DoS resistant No Yes
Use case One-time creation User-facing operations

Key Takeaways

DON'T

  • Use init for user-facing account creation
  • Assume users will always have uninitialized accounts
  • Ignore front-running attack vectors

DO

  • Use init_if_needed for user accounts
  • Make user-facing operations idempotent
  • Check account state when using init_if_needed
  • Consider mempool visibility in your threat model

Building This Example

cd 8-ata-initialization-dos
anchor build

Additional Resources


Remember: User-facing account creation should be idempotent. Use init_if_needed to prevent front-running DoS attacks!