Bump Seed Canonicalization Remediation
How to ensure PDA derivation uses the canonical bump seed from find_program_address rather than create_program_address with an attacker-supplied bump.
Bump Seed Canonicalization Remediation
Overview
Related Detector: Bump Seed Canonicalization
find_program_address returns the canonical bump — the highest bump value (255 down to 0) that produces a valid off-curve PDA. Using create_program_address with an attacker-supplied bump allows deriving different PDAs from the same seeds — effectively bypassing the uniqueness guarantee. The fix is to always use find_program_address or validate the bump against a stored canonical value.
Recommended Fix
Before (Vulnerable)
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program_error::ProgramError,
pubkey::Pubkey,
};
pub fn update_vault(accounts: &[AccountInfo], bump: u8, amount: u64) -> ProgramResult {
let vault_account = &accounts[0];
let user = &accounts[1];
// VULNERABLE: attacker supplies bump — derives a different PDA
// than the canonical one, allowing access to an unexpected account
let expected_vault = Pubkey::create_program_address(
&[b"vault", user.key.as_ref(), &[bump]], // bump is user-controlled!
&crate::id(),
)?;
if vault_account.key != &expected_vault {
return Err(ProgramError::InvalidAccountData);
}
// Proceeds with the attacker-chosen vault account
withdraw(vault_account, amount)?;
Ok(())
}
After (Fixed — Use find_program_address)
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program_error::ProgramError,
pubkey::Pubkey,
};
pub fn update_vault(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let vault_account = &accounts[0];
let user = &accounts[1];
// FIXED: find_program_address always returns the canonical bump
// No user-supplied bump — the canonical PDA is deterministic
let (expected_vault, _canonical_bump) = Pubkey::find_program_address(
&[b"vault", user.key.as_ref()],
&crate::id(),
);
if vault_account.key != &expected_vault {
return Err(ProgramError::InvalidAccountData);
}
withdraw(vault_account, amount)?;
Ok(())
}
Alternative Mitigations
1. Store Canonical Bump and Validate
If find_program_address is too expensive in the hot path (it iterates until it finds a valid bump), store the canonical bump in the PDA itself and validate on load:
use anchor_lang::prelude::*;
#[account]
pub struct Vault {
pub authority: Pubkey,
pub amount: u64,
pub bump: u8, // Stored canonical bump — set during initialization
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + 32 + 8 + 1,
seeds = [b"vault", authority.key().as_ref()],
bump, // Anchor fills in the canonical bump automatically
)]
pub vault: Account<'info, Vault>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let vault = &mut ctx.accounts.vault;
vault.authority = ctx.accounts.authority.key();
vault.bump = ctx.bumps.vault; // Store canonical bump
Ok(())
}
#[derive(Accounts)]
pub struct UpdateVault<'info> {
#[account(
mut,
seeds = [b"vault", authority.key().as_ref()],
bump = vault.bump, // Validates against stored canonical bump
has_one = authority,
)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>,
}
2. Native Program with Stored Bump
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program_error::ProgramError,
pubkey::Pubkey,
};
pub fn update_vault(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let vault_account = &accounts[0];
let user = &accounts[1];
// Read canonical bump that was stored during initialization
let vault_data = vault_account.data.borrow();
let stored_bump = vault_data[64]; // Canonical bump stored at offset 64
// Derive using stored bump (caller cannot substitute a different one)
let expected_vault = Pubkey::create_program_address(
&[b"vault", user.key.as_ref(), &[stored_bump]],
&crate::id(),
)?;
if vault_account.key != &expected_vault {
return Err(ProgramError::InvalidAccountData);
}
// Safe: bump came from the account itself, set at initialization by find_program_address
withdraw(vault_account, amount)?;
Ok(())
}
3. Anchor seeds and bump Constraints
Anchor handles this automatically when using the seeds and bump constraints:
#[derive(Accounts)]
pub struct WithdrawCtx<'info> {
// Anchor:
// 1. Derives PDA using the provided seeds
// 2. Uses find_program_address to get canonical bump
// 3. Validates vault.key() == derived PDA
// No manual bump handling required
#[account(
mut,
seeds = [b"vault", user.key().as_ref()],
bump, // Uses canonical bump from find_program_address
)]
pub vault: Account<'info, Vault>,
pub user: Signer<'info>,
}
Common Mistakes
Mistake 1: Accepting Bump from Instruction Data Without Validation
// WRONG: bump from instruction data is attacker-controlled
pub fn process(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
let bump = instruction_data[0]; // User-supplied!
let pda = Pubkey::create_program_address(
&[b"seed", &[bump]],
&crate::id(),
)?;
// Different attacker-chosen bumps produce different PDAs
// ...
}
Mistake 2: Not Storing Bump After Initialization
// INCOMPLETE: derives canonical bump during init but doesn't store it
pub fn initialize(accounts: &[AccountInfo]) -> ProgramResult {
let (pda, canonical_bump) = Pubkey::find_program_address(&[b"vault"], &crate::id());
// canonical_bump is thrown away! Subsequent calls must call find_program_address
// every time (expensive) or accept user-supplied bumps (vulnerable)
Ok(())
}