Back to Blog
Solana

How init and init_if_needed work under the hood and the associated token account griefing attack

A deep dive into how Anchor’s #[account(init)] and #[account(init_if_needed)] are parsed and code-generated, what “needs initialization” really means at runtime, and how using init with associated token accounts can enable griefing/DoS via pre-created ATAs.

jesjupyter (reviewed by ret2basic.eth)
January 05, 2026
30 min read
SolanaSecurityAnchor
How init and init_if_needed work under the hood and the associated token account griefing attack

In Anchor, #[account(init)] and #[account(init_if_needed)] are convenient ways to (conditionally) create and initialize accounts. But what do these constraints actually generate under the hood? And how can seemingly harmless account-creation conveniences lead to griefing/DoS patterns (especially around associated token accounts)? Let’s dive in.

Examples#

init and init_if_needed are commonly used in Anchor.

Below are two simple examples from https://github.com/solana-developers/anchor-examples.

rust
use anchor_lang::prelude::*;

declare_id!("2TiUn4ZNzQsSgwMnfD1fpzXNrBmqqB8BYeDF4xVb5WcF");

#[program]
pub mod example {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, input: u64) -> Result<()> {
        ctx.accounts.new_account.data = input;
        msg!("Changed data to: {}!", input);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub signer: Signer<'info>,
    #[account(
        init,
        payer = signer,
        space = 8 + 8
    )]
    pub new_account: Account<'info, DataAccount>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct DataAccount {
    data: u64,
}
rust
use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token::{Mint, Token, TokenAccount},
};

declare_id!("7ZpxAfW6xKiGLjyvvUPB71HQKWTqs3VdeYTY5uT784VE");

#[program]
pub mod example {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, input: u64) -> Result<()> {
        if ctx.accounts.new_account.is_initialized {
            msg!("Already initialized, data unchanged");
        } else {
            ctx.accounts.new_account.is_initialized = true;
            ctx.accounts.new_account.data = input;
            msg!("Initializing account with data: {}", input);
        }
        Ok(())
    }

    pub fn initialize_token_account(ctx: Context<InitializeTokenAccount>) -> Result<()> {
        // No additional checks needed, token account checks are done by token program
        msg!("Initialize associated token account if needed");
        msg!("Associated token account: {}", ctx.accounts.associated_token.key());
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub signer: Signer<'info>,
    #[account(
        init_if_needed,
        payer = signer,
        space = 8 + 1 + 8
    )]
    pub new_account: Account<'info, DataAccount>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct InitializeTokenAccount<'info> {
    #[account(mut)]
    pub signer: Signer<'info>,
    #[account(
        init_if_needed,
        payer = signer,
        associated_token::mint = mint,
        associated_token::authority = signer,
    )]
    pub associated_token: Account<'info, TokenAccount>,
    pub mint: Account<'info, Mint>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct DataAccount {
    is_initialized: bool, // 1 byte
    data: u64, // 8 bytes
}

The high-level intent is straightforward:

  • init creates the account (and then deserializes it into the requested wrapper type).
  • init_if_needed creates the account only if Anchor decides the account still “needs initialization”; otherwise it skips creation and only validates that the existing account matches the expected invariants.

One important nuance (we’ll formalize it later): in Anchor’s generated code, “needs initialization” is largely approximated as “is still owned by the System Program”.

But what do these constraints do internally?

Background: Anchor parser and codegen#

In Anchor, the parser extracts and structures declarative constraints from user code, while the codegen phase materializes those constraints into concrete runtime validation logic.

The entire workflow is like this:

anchor program (Rust source)
  -> rustc
      -> Anchor procedural macros
          -> syn-based parsing (AST)
          -> constraint parsing / validation
          -> code generation (TokenStream)
      -> expanded Rust code
  -> rustc continues compilation
  -> Solana BPF compilation

The parser in Anchor is responsible for analyzing Anchor-specific Rust constructs at compile time.

Concretely, it:

  • Parses Rust syntax into an abstract syntax tree (AST) using syn
  • Identifies Anchor-specific attributes (e.g. #[account(...)], #[derive(Accounts)])
  • Extracts account metadata and constraint definitions
  • Organizes constraints into structured representations (constraint groups) that can be reasoned about programmatically

At this stage, no runtime logic is generated yet. The parser’s role is purely structural and semantic extraction, translating user-written Anchor code into an internal representation suitable for validation and code generation.

The codegen phase takes the parsed constraint groups and produces concrete Rust code that will be executed at runtime.

Specifically, it:

  • Generates account validation logic (ownership checks, signer checks, mutability, PDA derivation, etc.)
  • Emits CPI boilerplate and account deserialization logic
  • Translates declarative constraints into imperative runtime checks
  • Outputs Rust TokenStreams that are injected back into the compilation pipeline

The generated code is then compiled normally by rustc and eventually deployed as a Solana BPF program.

parser phase#

In https://github.com/coral-xyz/anchor/blob/347c0599b8310d84af4086cfe5c975733a9e17cd/lang/syn/src/lib.rs#L815-L820, ConstraintToken is parsed from the attribute token stream:

impl Parse for ConstraintToken {
    fn parse(stream: ParseStream) -> ParseResult<Self> {
        accounts_parser::constraints::parse_token(stream)
    }
}

In the parse_token function, both init and init_if_needed parse into the same token variant (ConstraintToken::Init), with a boolean flag distinguishing them:

rust
// Parses a single constraint from a parse stream for `#[account(<STREAM>)]`.
pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
    let ident = stream.call(Ident::parse_any)?;
    let kw = ident.to_string();

    let c = match kw.as_str() {
        "init" => ConstraintToken::Init(Context::new(
            ident.span(),
            ConstraintInit { if_needed: false },
        )),
        "init_if_needed" => ConstraintToken::Init(Context::new(
            ident.span(),
            ConstraintInit { if_needed: true },
        )),

The only difference at this stage is if_needed: false (for init) vs if_needed: true (for init_if_needed).

When the constraint group is being built, the build will do the following check for the ConstraintInit.

rust
pub fn parse(f: &syn::Field, f_ty: Option<&Ty>) -> ParseResult<ConstraintGroup> {
    let mut constraints = ConstraintGroupBuilder::new(f_ty);
    for attr in f.attrs.iter().filter(is_account) {
        for c in attr.parse_args_with(Punctuated::<ConstraintToken, Comma>::parse_terminated)? {
            constraints.add(c)?;
        }
    }
    let account_constraints = constraints.build()?;

    Ok(account_constraints)
}
rust
#[derive(Debug, Clone)]
pub struct ConstraintInit {
    pub if_needed: bool,
}

First, it checks the if_needed flag and ensures the init-if-needed cargo feature is enabled for anchor-lang. Otherwise it emits a compile-time error (this is macro expansion time, not a runtime “revert”).

rust
        if let Some(i) = &self.init {
            if cfg!(not(feature = "init-if-needed")) && i.if_needed {
                return Err(ParseError::new(
                    i.span(),
                    "init_if_needed requires that anchor-lang be imported \
                    with the init-if-needed cargo feature enabled. \
                    Carefully read the init_if_needed docs before using this feature \
                    to make sure you know how to protect yourself against \
                    re-initialization attacks.",
                ));
            }

Later, mut is checked. If it’s provided together with init/init_if_needed, the macro emits a compile-time error ("mut cannot be provided with init").

rust
            match self.mutable {
                Some(m) => {
                    return Err(ParseError::new(
                        m.span(),
                        "mut cannot be provided with init",
                    ))
                }
                None => self
                    .mutable
                    .replace(Context::new(i.span(), ConstraintMut { error: None })),
            };

Later, the rent_exempt is being checked. If it's not explicitly skipped, it will be set to ConstraintRentExempt::Enforce.

rust
            // Rent exempt if not explicitly skipped.
            if self.rent_exempt.is_none() {
                self.rent_exempt
                    .replace(Context::new(i.span(), ConstraintRentExempt::Enforce));
            }

Later, payer is checked. If it’s not provided, the macro emits a compile-time error ("payer must be provided when initializing an account").

rust
            if self.payer.is_none() {
                return Err(ParseError::new(
                    i.span(),
                    "payer must be provided when initializing an account",
                ));
            }

For a non-PDA account being initialized (no seeds, not an ATA init), Anchor also auto-adds a signer constraint. That matches the System Program requirement: creating a regular account requires the new account’s signature.

rust
            // When initializing a non-PDA account, the account being
            // initialized must sign to invoke the system program's create
            // account instruction.
            if self.signer.is_none() && self.seeds.is_none() && self.associated_token_mint.is_none()
            {
                self.signer
                    .replace(Context::new(i.span(), ConstraintSigner { error: None }));
            }

bump is also checked. If a bump target is provided together with init, the macro emits a compile-time error ("bump targets should not be provided with init. Please use bump without a target.").

rust
            // Assert a bump target is not given on init.
            if let Some(b) = &self.bump {
                if b.bump.is_some() {
                    return Err(ParseError::new(
                        b.span(),
                        "bump targets should not be provided with init. Please use bump without a target."
                    ));
                }
            }

Lastly, there are checks for token account and mint related constraints.

rust
            // TokenAccount.
            if let Some(token_mint) = &self.token_mint {
                if self.token_authority.is_none() {
                    return Err(ParseError::new(
                        token_mint.span(),
                        "when initializing, token authority must be provided if token mint is",
                    ));
                }
            }
            if let Some(token_authority) = &self.token_authority {
                if self.token_mint.is_none() {
                    return Err(ParseError::new(
                        token_authority.span(),
                        "when initializing, token mint must be provided if token authority is",
                    ));
                }
            }

            // Mint.
            if let Some(mint_decimals) = &self.mint_decimals {
                if self.mint_authority.is_none() {
                    return Err(ParseError::new(
                        mint_decimals.span(),
                        "when initializing, mint authority must be provided if mint decimals is",
                    ));
                }
            }
            if let Some(mint_authority) = &self.mint_authority {
                if self.mint_decimals.is_none() {
                    return Err(ParseError::new(
                        mint_authority.span(),
                        "when initializing, mint decimals must be provided if mint authority is",
                    ));
                }
            }

Finally, the ConstraintInitGroup will be generated. We need to pay attention to the kind field here. It will be used to determine the type of the account to be initialized. We have 4 possible values:

  • InitKind::Program: program account
  • InitKind::Mint: mint account
  • InitKind::Token: token account
  • InitKind::AssociatedToken: associated token account
rust
        Ok(ConstraintGroup {
            init: init.as_ref().map(|i| Ok(ConstraintInitGroup {
                if_needed: i.if_needed,
                seeds: seeds.clone(),
                payer: into_inner!(payer.clone()).unwrap().target,
                space: space.clone().map(|s| s.space.clone()),
                kind: if let Some(tm) = &token_mint {
                    InitKind::Token {
                        mint: tm.clone().into_inner().mint,
                        owner: match &token_authority {
                            Some(a) => a.clone().into_inner().auth,
                            None => return Err(ParseError::new(
                                tm.span(),
                                "authority must be provided to initialize a token program derived address"
                            )),
                        },
                        token_program: token_token_program.map(|tp| tp.into_inner().token_program),
                    }
                } else if let Some(at) = &associated_token {
                    InitKind::AssociatedToken {
                        mint: at.mint.clone(),
                        owner: at.wallet.clone(),
                        token_program: associated_token_token_program.map(|tp| tp.into_inner().token_program),
                    }
                } else if let Some(d) = &mint_decimals {
                    InitKind::Mint {
                        decimals: d.clone().into_inner().decimals,
                        owner: match &mint_authority {
                            Some(a) => a.clone().into_inner().mint_auth,
                            None => return Err(ParseError::new(
                                d.span(),
                                "authority must be provided to initialize a mint program derived address"
                            ))
                        },
                        freeze_authority: mint_freeze_authority.map(|fa| fa.into_inner().mint_freeze_auth),
                        token_program: mint_token_program.map(|tp| tp.into_inner().token_program),
                        // extensions
                        group_pointer_authority: extension_group_pointer_authority.map(|gpa| gpa.into_inner().authority),
                        group_pointer_group_address: extension_group_pointer_group_address.map(|gpga| gpga.into_inner().group_address),
                        group_member_pointer_authority: extension_group_member_pointer_authority.map(|gmpa| gmpa.into_inner().authority),
                        group_member_pointer_member_address: extension_group_member_pointer_member_address.map(|gmpma| gmpma.into_inner().member_address),
                        metadata_pointer_authority: extension_metadata_pointer_authority.map(|mpa| mpa.into_inner().authority),
                        metadata_pointer_metadata_address: extension_metadata_pointer_metadata_address.map(|mpma| mpma.into_inner().metadata_address),
                        close_authority: extension_close_authority.map(|ca| ca.into_inner().authority),
                        permanent_delegate: extension_permanent_delegate.map(|pd| pd.into_inner().permanent_delegate),
                        transfer_hook_authority: extension_transfer_hook_authority.map(|tha| tha.into_inner().authority),
                        transfer_hook_program_id: extension_transfer_hook_program_id.map(|thpid| thpid.into_inner().program_id),
                    }
                } else {
                    InitKind::Program {
                        owner: owner.as_ref().map(|o| o.owner_address.clone()),
                    }
                },

So far, init vs init_if_needed differ mostly by a boolean flag plus a feature-gate. The interesting part is the runtime behavior produced by codegen.

codegen phase#

In the codegen phase, the init constraint compiles down via generate_constraint_init, called from generate_constraint (see https://github.com/coral-xyz/anchor/blob/347c0599b8310d84af4086cfe5c975733a9e17cd/lang/syn/src/codegen/accounts/constraints.rs).

rust
fn generate_constraint(
    f: &Field,
    c: &Constraint,
    accs: &AccountsStruct,
) -> proc_macro2::TokenStream {
    match c {
        Constraint::Init(c) => generate_constraint_init(f, c, accs),
        Constraint::Zeroed(c) => generate_constraint_zeroed(f, c, accs),
        Constraint::Mut(c) => generate_constraint_mut(f, c),
        Constraint::Dup(_) => quote! {}, // No-op: dup is handled by duplicate checking logic
        Constraint::HasOne(c) => generate_constraint_has_one(f, c, accs),
        Constraint::Signer(c) => generate_constraint_signer(f, c),
        Constraint::Raw(c) => generate_constraint_raw(&f.ident, c),
        Constraint::Owner(c) => generate_constraint_owner(f, c),
        Constraint::RentExempt(c) => generate_constraint_rent_exempt(f, c),
        Constraint::Seeds(c) => generate_constraint_seeds(f, c),
        Constraint::Executable(c) => generate_constraint_executable(f, c),
        Constraint::Close(c) => generate_constraint_close(f, c, accs),
        Constraint::Address(c) => generate_constraint_address(f, c),
        Constraint::AssociatedToken(c) => generate_constraint_associated_token(f, c, accs),
        Constraint::TokenAccount(c) => generate_constraint_token_account(f, c, accs),
        Constraint::Mint(c) => generate_constraint_mint(f, c, accs),
        Constraint::Realloc(c) => generate_constraint_realloc(f, c, accs),
    }
}

This will finally call generate_constraint_init_group:

rust
fn generate_constraint_init_group(
    f: &Field,
    c: &ConstraintInitGroup,
    accs: &AccountsStruct,
) -> proc_macro2::TokenStream

The if_needed flag is being extracted from the constraint group.

rust
    let if_needed = if c.if_needed {
        quote! {true}
    } else {
        quote! {false}
    };

Then the seeds are being checked and the find_pda and seeds_with_bump are being generated.

rust
    // PDA bump seeds.
    let (find_pda, seeds_with_bump) = match &c.seeds {
        None => (quote! {}, quote! {}),
        Some(c) => match &c.seeds {
            // If the bump is provided with init *and target*, then force it to be the
            // canonical bump.
            //
            // Note that for `#[account(init, seeds)]`, find_program_address has already
            // been run in the init constraint find_pda variable.
            SeedsExpr::List(list) => {
                // Optional prefix (either empty or "<list>,")
                let maybe_seeds_plus_comma = (!list.is_empty()).then(|| quote! { #list, });

                let validate_pda = if let Some(b) = &c.bump {
                    quote! {
                        if #field.key() != __pda_address {
                            return Err(anchor_lang::error::Error::from(
                                anchor_lang::error::ErrorCode::ConstraintSeeds
                            ).with_account_name(#name_str)
                             .with_pubkeys((#field.key(), __pda_address)));
                        }
                        if __bump != #b {
                            return Err(anchor_lang::error::Error::from(
                                anchor_lang::error::ErrorCode::ConstraintSeeds
                            ).with_account_name(#name_str)
                             .with_values((__bump, #b)));
                        }
                    }
                } else {
                    quote! {
                        if #field.key() != __pda_address {
                            return Err(anchor_lang::error::Error::from(
                                anchor_lang::error::ErrorCode::ConstraintSeeds
                            ).with_account_name(#name_str)
                             .with_pubkeys((#field.key(), __pda_address)));
                        }
                    }
                };

                let bump_tok = if f.is_optional {
                    quote!(Some(__bump))
                } else {
                    quote!(__bump)
                };

                (
                    quote! {
                        let (__pda_address, __bump) = Pubkey::find_program_address(
                            &[ #maybe_seeds_plus_comma ],
                            __program_id,
                        );
                        __bumps.#field = #bump_tok;
                        #validate_pda
                    },
                    quote! {
                        &[
                            #maybe_seeds_plus_comma
                            &[__bump][..]
                        ][..]
                    },
                )
            }
            SeedsExpr::Expr(expr) => {
                let bump_tok = if f.is_optional {
                    quote!(Some(__bump))
                } else {
                    quote!(__bump)
                };

                (
                    quote! {
                        let __seeds_slice: &[&[u8]] = #expr;
                        let (__pda_address, __bump) =
                            Pubkey::find_program_address(__seeds_slice, __program_id);
                        __bumps.#field = #bump_tok;

                        // Build signer seeds at runtime = seeds + bump
                        let mut __signer_seeds_vec: ::std::vec::Vec<&[u8]> = __seeds_slice.to_vec();
                        __signer_seeds_vec.push(&[__bump][..]);
                        let __signer_seeds = __signer_seeds_vec;

                        if #field.key() != __pda_address {
                            return Err(anchor_lang::error::Error::from(
                                anchor_lang::error::ErrorCode::ConstraintSeeds
                            ).with_account_name(#name_str)
                             .with_pubkeys((#field.key(), __pda_address)));
                        }
                    },
                    quote! { &__signer_seeds[..] },
                )
            }
        },
    };

Later, the match &c.kind will be used to generate the code for the different types of accounts like Program, Mint, Token and AssociatedToken.

What “needs initialization” means to Anchor#

Before diving into each InitKind, it’s useful to pin down the key conditional in Anchor’s generated code:

  • For init, Anchor always runs the account-creation path.
  • For init_if_needed, Anchor runs the account-creation path only if the current owner is the System Program; otherwise it skips creation and instead deserializes + validates invariants.

This “owner == system program” heuristic is what makes init_if_needed work well for cases like ATAs (which, once created, are owned by the token program), and also what makes it potentially dangerous if you treat an already-existing program-owned account as “fresh” without additional application-level checks.

Here, “owner” refers to AccountInfo.owner (the program id that owns the account), not the wallet/authority that controls it.

InitKind::Program (regular program-owned account)#

The owner is being checked, if not provided, the default owner will be the currently executing program.

rust
       // Define the owner of the account being created. If not specified,
            // default to the currently executing program.
            let (owner, owner_optional_check) = match owner {
                None => (
                    quote! {
                        __program_id
                    },
                    quote! {},
                ),

                Some(o) => {
                    // We clone the `check_scope` here to avoid collisions with the
                    // `payer_optional_check`, which is in a separate scope
                    let owner_optional_check = check_scope.clone().generate_check(o);
                    (
                        quote! {
                            &#o
                        },
                        owner_optional_check,
                    )
                }
            };

Later, the generate_create_account will be used.

rust
            // CPI to the system program to create the account.
            let create_account = generate_create_account(
                field,
                quote! {space},
                owner.clone(),
                quote! {#payer},
                seeds_with_bump,
            );

Internally, generate_create_account handles a subtle but important edge case: the target account may already hold lamports (for example, someone pre-funded a PDA address).

  • If the account’s lamports are zero, Anchor uses a normal system_program::create_account CPI.
  • If the account already has lamports, Anchor does not use create_account; instead it tops up rent if needed, then uses system_program::allocate and system_program::assign (with signer seeds for PDAs). This makes the flow robust against “pre-fund” griefing.

In other words, if if_needed is false (plain init), or if if_needed is true but the target account is still owned by the System Program, Anchor takes the creation path via generate_create_account. Otherwise, it assumes the account already exists and deserializes it via the checked path.

rust
                    // Create the account. Always do this in the event
                    // if needed is not specified or the system program is the owner.
                    let pa: #ty_decl = if !#if_needed || actual_owner == &anchor_lang::solana_program::system_program::ID {
                        #payer_optional_check

                        // CPI to the system program to create.
                        #create_account

                        // Convert from account info to account context wrapper type.
                        #from_account_info_unchecked
                    } else {
                        // Convert from account info to account context wrapper type.
                        #from_account_info
                    };

If it’s the init_if_needed case and Anchor skipped creation, it will validate basic invariants such as space, owner, and rent exemption.

rust
                    // Assert the account was created correctly.
                    if #if_needed {
                        #owner_optional_check
                        if space != actual_field.data_len() {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSpace).with_account_name(#name_str).with_values((space, actual_field.data_len())));
                        }

                        if actual_owner != #owner {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintOwner).with_account_name(#name_str).with_pubkeys((*actual_owner, *#owner)));
                        }

                        {
                            let required_lamports = __anchor_rent.minimum_balance(space);
                            if pa.to_account_info().lamports() < required_lamports {
                                return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintRentExempt).with_account_name(#name_str));
                            }
                        }
                    }

InitKind::Mint#

The logic is pretty similar to the Program case. After a lot of checks in the mint params, the generate_create_account will be used again.

rust
            let create_account = generate_create_account(
                field,
                mint_space,
                quote! {&#token_program.key()},
                quote! {#payer},
                seeds_with_bump,
            );

In the construction of the stream, if the account is to be created via the CPI call, the create_account will be called and extensions will be initialized accordingly. If it's init_if_needed case, the account is considered to be already initialized, so it will be checked against the basic configuration like mint_authority, freeze_authority and decimals.

rust
                    if !#if_needed || owner_program == &anchor_lang::solana_program::system_program::ID {
                        // Define payer variable.
                        #payer_optional_check

                        // Create the account with the system program.
                        #create_account

                        let cpi_program_id = #token_program.key();

                        // Initialize extensions.
                        if let Some(extensions) = #extensions {
                            ...
                        }

                        // Initialize the mint account.
                        let accounts = ::anchor_spl::token_interface::InitializeMint2 {
                            mint: #field.to_account_info(),
                        };
                        let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program_id, accounts);
                        ::anchor_spl::token_interface::initialize_mint2(cpi_ctx, #decimals, &#owner.key(), #freeze_authority)?;
                    }

                    let pa: #ty_decl = #from_account_info_unchecked;
                    if #if_needed {
                        if pa.mint_authority != anchor_lang::solana_program::program_option::COption::Some(#owner.key()) {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintMintMintAuthority).with_account_name(#name_str));
                        }
                        if pa.freeze_authority
                            .as_ref()
                            .map(|fa| #freeze_authority.as_ref().map(|expected_fa| fa != *expected_fa).unwrap_or(true))
                            .unwrap_or(#freeze_authority.is_some()) {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintMintFreezeAuthority).with_account_name(#name_str));
                        }
                        if pa.decimals != #decimals {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintMintDecimals).with_account_name(#name_str).with_values((pa.decimals, #decimals)));
                        }
                        if owner_program != &#token_program.key() {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintMintTokenProgram).with_account_name(#name_str).with_pubkeys((*owner_program, #token_program.key())));
                        }
                    }

InitKind::Token#

The logic is pretty similar to the Program case. After a lot of checks in the token params, the generate_create_account will be used again.

rust
            let create_account = generate_create_account(
                field,
                quote! {#token_account_space},
                quote! {&#token_program.key()},
                quote! {#payer},
                seeds_with_bump,
            );

In the construction of the stream, if the account is to be created via the CPI call, the create_account will be called and the token account will be initialized accordingly. If it's init_if_needed case, the account is considered to be already initialized, so it will be checked against the basic configuration like mint, owner and token_program.

rust
                    if !#if_needed || owner_program == &anchor_lang::solana_program::system_program::ID {
                        #payer_optional_check

                        // Create the account with the system program.
                        #create_account

                        // Initialize the token account.
                        let cpi_program_id = #token_program.key();
                        let accounts = ::anchor_spl::token_interface::InitializeAccount3 {
                            account: #field.to_account_info(),
                            mint: #mint.to_account_info(),
                            authority: #owner.to_account_info(),
                        };
                        let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program_id, accounts);
                        ::anchor_spl::token_interface::initialize_account3(cpi_ctx)?;
                    }

                    let pa: #ty_decl = #from_account_info_unchecked;
                    if #if_needed {
                        if pa.mint != #mint.key() {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenMint).with_account_name(#name_str).with_pubkeys((pa.mint, #mint.key())));
                        }
                        if pa.owner != #owner.key() {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenOwner).with_account_name(#name_str).with_pubkeys((pa.owner, #owner.key())));
                        }
                        if owner_program != &#token_program.key() {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenTokenProgram).with_account_name(#name_str).with_pubkeys((*owner_program, #token_program.key())));
                        }
                    }

InitKind::AssociatedToken#

The logic here is slightly different. If Anchor takes the creation path, it calls ::anchor_spl::associated_token::create to create the associated token account. If Anchor skips creation (the init_if_needed + non-system-owned case), it checks invariants like mint, owner, token_program, and that the address is the derived ATA.

rust
                    if !#if_needed || owner_program == &anchor_lang::solana_program::system_program::ID {
                        #payer_optional_check

                        ::anchor_spl::associated_token::create(
                            anchor_lang::context::CpiContext::new(
                                associated_token_program.key(),
                                ::anchor_spl::associated_token::Create {
                                    payer: #payer.to_account_info(),
                                    associated_token: #field.to_account_info(),
                                    authority: #owner.to_account_info(),
                                    mint: #mint.to_account_info(),
                                    system_program: system_program.to_account_info(),
                                    token_program: #token_program.to_account_info(),
                                }
                            )
                        )?;
                    }
                    let pa: #ty_decl = #from_account_info_unchecked;
                    if #if_needed {
                        if pa.mint != #mint.key() {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenMint).with_account_name(#name_str).with_pubkeys((pa.mint, #mint.key())));
                        }
                        if pa.owner != #owner.key() {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenOwner).with_account_name(#name_str).with_pubkeys((pa.owner, #owner.key())));
                        }
                        if owner_program != &#token_program.key() {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintAssociatedTokenTokenProgram).with_account_name(#name_str).with_pubkeys((*owner_program, #token_program.key())));
                        }

                        if pa.key() != ::anchor_spl::associated_token::get_associated_token_address_with_program_id(&#owner.key(), &#mint.key(), &#token_program.key()) {
                            return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::AccountNotAssociatedTokenAccount).with_account_name(#name_str));
                        }
                    }

Associated token account (ATA) griefing with init#

In the runtime logic, the ::anchor_spl::associated_token::create logic will be called to create the associated token account when the init is being used.

rust
                    if !#if_needed || owner_program == &anchor_lang::solana_program::system_program::ID {
                        #payer_optional_check

                        ::anchor_spl::associated_token::create(
                            anchor_lang::context::CpiContext::new(
                                associated_token_program.key(),
                                ::anchor_spl::associated_token::Create {
                                    payer: #payer.to_account_info(),
                                    associated_token: #field.to_account_info(),
                                    authority: #owner.to_account_info(),
                                    mint: #mint.to_account_info(),
                                    system_program: system_program.to_account_info(),
                                    token_program: #token_program.to_account_info(),
                                }
                            )
                        )?;
                    }

So, if if_needed is false (plain init), or if if_needed is true but the account is still system-owned, Anchor will attempt to create the ATA via a CPI into the associated token account program.

This is normally the case for a lot of ATA accounts owned by the program:

rust
    #[account(
        init,
        payer = signer,
        associated_token::mint = mint,
        associated_token::authority = vault,
    )]
    pub associated_token: Account<'info, TokenAccount>,

But what if the ATA already exists — potentially because a third party created it in advance (which is allowed), or simply because the user already interacted with that mint elsewhere?

Recall that an ATA address is a PDA derived from (wallet, token_program_id, mint) under the ATA program id, so “creating an ATA early” means creating the canonical address that your program will later expect.

In the associated token account program, the process_create_associated_token_account handler enforces two relevant properties:

  1. There is no requirement that the funder_info (payer) equals the wallet_account_info (the wallet that will own the ATA). So anyone can pay to create someone else’s ATA.
rust
    let funder_info = next_account_info(account_info_iter)?;
    let associated_token_account_info = next_account_info(account_info_iter)?;
    let wallet_account_info = next_account_info(account_info_iter)?;
    let spl_token_mint_info = next_account_info(account_info_iter)?;
    let system_program_info = next_account_info(account_info_iter)?;
    let spl_token_program_info = next_account_info(account_info_iter)?;
    let spl_token_program_id = spl_token_program_info.key;

    let (associated_token_address, bump_seed) = get_associated_token_address_and_bump_seed_internal(
        wallet_account_info.key,
        spl_token_mint_info.key,
        program_id,
        spl_token_program_id,
    );
  1. For the non-idempotent create path (“Create” / CreateMode::Always), if the account is already initialized, the ATA account is no longer system-owned, and creation fails:
rust
    if *associated_token_account_info.owner != system_program::id() {
        return Err(ProgramError::IllegalOwner);
    }

Therefore, if you use init for an ATA, any pre-existing ATA at that address causes Anchor’s CPI create to fail. A third party can “grief” by creating the ATA first, forcing your instruction to fail (a denial-of-service on that code path).

Note: newer ATA program versions also support an idempotent create instruction (“CreateIdempotent” / CreateMode::Idempotent) that returns Ok(()) if the ATA already exists with the correct mint/owner. Anchor’s #[account(init, associated_token::...)] flow calls ::anchor_spl::associated_token::create, which maps to the non-idempotent create path, so the failure mode remains.

init_if_needed is not affected in the same way because it skips the CPI create call when the ATA already exists (i.e., when the account owner is the token program, not the System Program). In that case, Anchor instead validates that the provided account is the correct ATA (mint/owner/token program and derived address).

rust
                    if !#if_needed || owner_program == &anchor_lang::solana_program::system_program::ID {

A mitigation strategy is to prefer init_if_needed for ATA accounts when possible.

If you’re not relying on Anchor’s init/init_if_needed for ATAs, another robust option is to CPI into ::anchor_spl::associated_token::create_idempotent instead of create, so “already exists” becomes a no-op (as long as the existing account matches the expected mint/owner).

Share this article

Related Articles