Skip to main content
Sigvex

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.

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

an account-validation framework 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(())
}

References