Bump Seed Canonicalization
Detects PDA operations using potentially non-canonical bump seeds via create_program_address, which can enable PDA collision and address confusion attacks.
Bump Seed Canonicalization
Overview
Remediation Guide: How to Fix Bump Seed Canonicalization
The bump seed canonicalization detector identifies Solana programs that create or use Program Derived Addresses (PDAs) via create_program_address with a user-supplied bump seed that has not been verified to be canonical. The canonical bump is the first valid bump value (starting from 255 counting down) returned by find_program_address. Using a non-canonical bump can allow multiple different addresses to be derived for the same seed set, creating ambiguity and enabling collision attacks.
Sigvex scans for sol_create_program_address syscall invocations and checks whether the bump seed argument was produced by sol_find_program_address (safe) or loaded from account data (a stored canonical bump — also safe), or taken from unvalidated input (flagged).
Why This Is an Issue
Solana’s PDA derivation starts from bump = 255 and decrements until it finds a bump that produces a valid off-curve address. The first (highest) valid bump is the “canonical bump.” Using find_program_address always returns the canonical bump.
The problem arises when a program stores the bump and later uses create_program_address with a stored or user-supplied bump. If an attacker can supply a non-canonical bump:
- Different bump values may produce different PDAs, confusing the program’s state model
- An attacker can front-run initialization to create a PDA using a non-canonical bump before the legitimate user creates it with the canonical bump
- The program’s validation logic (which may compare against a canonical address) fails silently
This is a well-known vulnerability class in production Solana programs, cataloged in the coral-xyz/sealevel-attacks repository.
How to Resolve
// Before: Vulnerable — uses user-provided bump with create_program_address
pub fn initialize_vault(accounts: &[AccountInfo], user_bump: u8) -> ProgramResult {
let vault_pda = Pubkey::create_program_address(
&[b"vault", accounts[0].key.as_ref(), &[user_bump]], // user_bump not validated!
&crate::id(),
)?;
// vault_pda may not match the canonical PDA
if vault_pda != *accounts[1].key {
return Err(ProgramError::InvalidSeeds);
}
Ok(())
}
// After: Use find_program_address or validate stored canonical bump
pub fn initialize_vault(accounts: &[AccountInfo]) -> ProgramResult {
// find_program_address always returns the canonical bump
let (canonical_pda, canonical_bump) = Pubkey::find_program_address(
&[b"vault", accounts[0].key.as_ref()],
&crate::id(),
);
if canonical_pda != *accounts[1].key {
return Err(ProgramError::InvalidSeeds);
}
// Store the canonical bump for future use
let vault_data = &mut accounts[1].data.borrow_mut();
vault_data[0] = canonical_bump;
Ok(())
}
If using a stored canonical bump:
pub fn use_vault(accounts: &[AccountInfo]) -> ProgramResult {
let vault = &accounts[1];
let data = vault.data.borrow();
let stored_bump = data[0]; // Loaded from account data — treated as canonical
// Verify the vault address matches what we expect
let expected_pda = Pubkey::create_program_address(
&[b"vault", accounts[0].key.as_ref(), &[stored_bump]],
&crate::id(),
)?;
if expected_pda != *vault.key {
return Err(ProgramError::InvalidSeeds);
}
Ok(())
}
Examples
Vulnerable Code
// User supplies bump — non-canonical bumps can create multiple valid PDAs
pub fn create_escrow(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
// data[0] is attacker-controlled bump
let bump = data[0];
let escrow_pda = Pubkey::create_program_address(
&[b"escrow", accounts[0].key.as_ref(), &[bump]],
&crate::id(),
)?;
// Multiple bumps (e.g., 253, 254, 255) may all be valid — attacker picks one
// that gives them an address they pre-computed
require!(escrow_pda == *accounts[1].key, MyError::InvalidEscrow);
// Initialize escrow at attacker-chosen address...
Ok(())
}
Fixed Code
pub fn create_escrow(accounts: &[AccountInfo]) -> ProgramResult {
// Always use find_program_address — returns canonical bump only
let (escrow_pda, bump) = Pubkey::find_program_address(
&[b"escrow", accounts[0].key.as_ref()],
&crate::id(),
);
require!(escrow_pda == *accounts[1].key, MyError::InvalidEscrow);
// Store canonical bump in account data for future cross-reference
let data = &mut accounts[1].data.borrow_mut();
data[0] = bump;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "bump-seed-canonicalization",
"severity": "high",
"confidence": 0.80,
"description": "PDA created using create_program_address with a potentially non-canonical bump seed. Non-canonical bumps can lead to PDA collision attacks where multiple different bump values derive the same address.",
"location": { "function": "create_escrow", "offset": 5 }
}
Detection Methodology
The detector implements a two-pass analysis:
Pass 1 — Validation collection:
- Variables assigned from
sol_find_program_addresssyscall are marked as canonical bumps. - Variables loaded from
HirExpr::AccountData(any offset) are treated as stored canonical bumps — a common Anchor and native pattern for re-validating PDAs. - Branch conditions comparing variables via
BinOp::EqorBinOp::Nemark both sides as validated.
Pass 2 — PDA operation analysis:
- Identifies
sol_create_program_addresssyscalls. - Extracts the bump seed argument (typically the last argument).
- Reports findings for bump seeds not in the validated set.
sol_find_program_addresscalls are always safe — no finding generated.
Context modifiers: Anchor programs reduce confidence by 0.30x (with discriminator validation by 0.30x, without by 0.50x — Anchor’s #[account(seeds = [...], bump)] stores and validates canonical bump). Read-only functions reduce by 0.40x.
Limitations
False positives:
- Variables loaded from account data are broadly marked as validated — a program that stores a non-canonical bump and reads it back would not be flagged, even though the stored bump might be non-canonical.
- Anchor’s
seedsandbumpconstraints handle this automatically — Anchor programs receive reduced confidence.
False negatives:
- Programs that validate the bump via a custom comparison function (not a simple
Eqbranch) may not have their bump recognized as validated.
Related Detectors
- Arbitrary CPI — detects unvalidated CPI targets that can exploit PDA confusion
- Missing Owner Check — detects missing ownership validation related to PDA-derived accounts