◂ labs
TIER 2 · CPI & PDAs · 7-bump-seed-canonicalization

BUMP SEED CANONICALIZATION

A PDA can be derived with multiple valid bumps, but only one is canonical. Accepting a non-canonical bump lets an attacker derive a second address for the "same" PDA — two accounts where the program expects one.

seen in the wild
Subtle but real: non-canonical PDAs break the one-account-one-vault invariant protocols rely on.
rust · anchor
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

    pub fn set_value(ctx: Context<BumpSeed>, key: u64, new_value: u64, bump: u8) -> ProgramResult {
        let address =
            Pubkey::create_program_address(&[key.to_le_bytes().as_ref(), &[bump]], ctx.program_id)?;
        if address != ctx.accounts.data.key() {
            return Err(ProgramError::InvalidArgument);
        }

        ctx.accounts.data.value = new_value;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct BumpSeed<'info> {
    data: Account<'info, Data>,
}

#[account]
pub struct Data {
    value: u64,
}