PDA & Seed Attacks
Program Derived Addresses (PDAs) anchor protocol state to deterministic seeds. Seed and bump mistakes let attackers impersonate vaults, bypass authority checks, or collide with other programs' PDAs.
Search across all documentation pages
Program Derived Addresses (PDAs) anchor protocol state to deterministic seeds. Seed and bump mistakes let attackers impersonate vaults, bypass authority checks, or collide with other programs' PDAs.
Quick-reference recipe card - copy-paste ready.
// `State::INIT_SPACE` requires `#[account] #[derive(InitSpace)]` on the State struct.
#[account(
init,
payer = payer,
space = 8 + State::INIT_SPACE,
seeds = [b"state", user.key().as_ref()],
bump,
)]
pub state: Account<'info, State>,
// Store bump on account for later invokes
state.bump = ctx.bumps.state;When to reach for this:
invoke_signed.seeds constraints on #[account].use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Escrow {
pub maker: Pubkey,
pub bump: u8,
}
#[derive(Accounts)]
pub struct InitEscrow<'info> {
#[account(mut)]
pub maker: Signer<'info>,
#[account(
init,
payer = maker,
space = 8 + Escrow::INIT_SPACE,
seeds = [b"escrow", maker.key().as_ref()],
bump,
)]
pub escrow: Account<'info, Escrow>,
pub system_program: Program<'info, System>,
}
pub fn init_escrow(ctx: Context<InitEscrow>) -> Result<()> {
ctx.accounts.escrow.maker = ctx.accounts.maker.key();
ctx.accounts.escrow.bump = ctx.bumps.escrow;
Ok(())
}What this demonstrates:
bump in init stores canonical bump for later invoke_signed.seeds + bump on subsequent instructions prevent PDA substitution.(seeds, program_id).invoke_signed with exact seeds.| Pattern | Risk | Mitigation |
|---|---|---|
| Missing bump in seeds | Wrong PDA accepted | bump or bump = state.bump |
| User-controlled seeds only | Predictable collisions | Include program-chosen prefix bytes |
| Seed canonicalization | Multiple bumps | Always use canonical bump from find_program_address |
| Cross-program PDA | Same seeds, different program | Never reuse seed strings across programs |
let seeds = &[b"escrow", maker.as_ref(), &[escrow.bump]];
let signer_seeds = &[&seeds[..]];
// Pass signer_seeds to CpiContext::new_with_signerinit_if_needed skips creation for an existing account but still runs your handler body; the reset comes from your own unguarded writes, or from close-then-recreate. Fix: Use init once, or guard every field write in the handler."escrow" vs b"escrow". Fix: Always byte literals b"...".hashv into 32 bytes.| Alternative | Use When | Don't Use When |
|---|---|---|
| Single global PDA | Singleton config | Per-user state |
| Keypair-owned account | External signer needed | Custody must stay in program |
| Named seed registry | Many PDA types | Simple one-off escrow |
Yes - PDAs are public. Security is in seeds validation and signer rules, not secrecy.
Solana picks the highest valid bump (usually 255) - always use that bump in invoke_signed.
No - program_id is part of derivation; same seeds + different program = different address.
Dual-write, migrate funds, deprecate old seed path - never change seeds with live funds without plan.
Attacker passes non-PDA account matching expected key without your seeds - prevented by seeds constraint.
Up to 16 seed components, max 32 bytes each - see Solana SDK limits.
bump without value finds and stores in ctx.bumps during init; use bump = account.bump later.
LiteSVM tests with incorrect bump in invoke_signed - expect signature verification failure.
Yes - token account owner is the PDA; program signs transfers with invoke_signed.
Do not add crate::ID as a seed - the program ID is already an input to find_program_address, so it adds no domain separation and burns 32 bytes of your seed budget. Use a program-chosen literal prefix (b"vault") plus a version tag for separation within one program.
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.
Reviewed by Chris St. John·Last updated Jul 16, 2026