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.
Recipe
Quick-reference recipe card - copy-paste ready.
#[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:
- Creating vault or escrow PDAs.
- Signing CPIs with
invoke_signed. - Auditing
seedsconstraints on#[account]. - Migrating seed schemes without orphaning funds.
Working Example
use anchor_lang::prelude::*;
#[account]
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:
- Seeds include user pubkey - each user gets unique escrow PDA.
bumpin init stores canonical bump for laterinvoke_signed.seeds+bumpon subsequent instructions prevent PDA substitution.
Deep Dive
How It Works
- PDAs are off-curve pubkeys derived from
(seeds, program_id). - Runtime finds canonical bump (255 down to 0) where address is off-curve.
- Only the owning program can sign for PDA via
invoke_signedwith exact seeds. - Wrong seed sets derive different addresses - attacker supplies lookalike accounts.
Attack Patterns
| 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 |
Rust Notes
let seeds = &[b"escrow", maker.as_ref(), &[escrow.bump]];
let signer_seeds = &[&seeds[..]];
// Pass signer_seeds to CpiContext::new_with_signerGotchas
- Bump not stored - Re-searching bump each tx wastes CU and risks mismatch. Fix: Persist bump at init.
- Optional seeds from user - Attacker picks seeds mapping to their account. Fix: Fix protocol prefix bytes; limit user input to pubkeys.
- init_if_needed on PDA - Reinit resets state. Fix: Use
initonce +is_initializedflag. - Seed UTF-8 confusion -
"escrow"vsb"escrow". Fix: Always byte literalsb"...". - Max seed length - Total seeds > 32 bytes per component limit. Fix: Hash long inputs with
hashvinto 32 bytes. - PDA as token authority - Must sign token CPI with correct seeds. Fix: Match token account owner to PDA.
Alternatives
| 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 |
FAQs
Can users derive my PDA client-side?
Yes - PDAs are public. Security is in seeds validation and signer rules, not secrecy.
What is bump seed canonicalization?
Solana picks the highest valid bump (usually 255) - always use that bump in invoke_signed.
Can two programs share a PDA address?
No - program_id is part of derivation; same seeds + different program = different address.
How do I migrate PDA seeds?
Dual-write, migrate funds, deprecate old seed path - never change seeds with live funds without plan.
What is a seed collision attack?
Attacker passes non-PDA account matching expected key without your seeds - prevented by seeds constraint.
How many seeds can I use?
Up to 16 seed components, max 32 bytes each - see Solana SDK limits.
Does Anchor find bump automatically?
bump without value finds and stores in ctx.bumps during init; use bump = account.bump later.
How do I test wrong bump?
LiteSVM tests with incorrect bump in invoke_signed - expect signature verification failure.
Can PDAs own token accounts?
Yes - token account owner is the PDA; program signs transfers with invoke_signed.
Are program address seeds safe?
Including crate::ID in seeds prevents cross-deployment impersonation when program id changes per cluster.
Related
- Signer & Owner Checks - PDA signing
- Reinitialization Attacks - PDA reinit
- Testing CPIs & PDAs - PDA tests
- Program Derived Addresses - PDA fundamentals
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.