Skip to main content
Sigvex

PDA Seed Collision Remediation

How to prevent PDA seed collisions that allow distinct logical accounts to resolve to the same address.

PDA Seed Collision Remediation

Overview

Related Detector: PDA Seed Collision

A PDA seed collision happens when two different conceptual accounts produce the same Program Derived Address. This usually arises from concatenating user-controlled byte arrays without delimiters, allowing an attacker to craft inputs that derive the same address as a legitimate account and overwrite its data.

Before (Vulnerable)

let (pda, _) = Pubkey::find_program_address(
    &[user.key.as_ref(), name.as_bytes()],
    program_id,
);

The user pubkey (32 bytes) and a variable-length name are concatenated. An attacker who picks a name that happens to start with another user’s pubkey suffix can derive the same PDA.

After (Fixed)

const USER_RECORD_PREFIX: &[u8] = b"user_record";

let (pda, _) = Pubkey::find_program_address(
    &[
        USER_RECORD_PREFIX,           // namespace
        user.key.as_ref(),            // fixed-width 32 bytes
        &(name.len() as u32).to_le_bytes(), // explicit length
        name.as_bytes(),
    ],
    program_id,
);

Three changes prevent collisions:

  1. A constant prefix namespaces this PDA family from any other PDA in the program.
  2. The user key is fixed-width, so the boundary is unambiguous.
  3. The variable-length name is preceded by its length, removing parser ambiguity.

Alternative Mitigations

Hash variable-length seeds

use solana_program::keccak;

let name_hash = keccak::hash(name.as_bytes()).to_bytes();
let (pda, _) = Pubkey::find_program_address(
    &[b"user_record", user.key.as_ref(), &name_hash],
    program_id,
);

A 32-byte hash is fixed-width and collision-resistant. Use this when seeds may exceed 32 bytes (the per-seed limit).

Use the account-validation framework’s seeds constraint

#[account(
    seeds = [b"user_record", user.key().as_ref(), name.as_bytes()],
    bump,
)]
pub record: Account<'info, UserRecord>,

Anchor validates that the supplied account address matches the derivation. Combine with a length constraint on name to remove the variable-width attack surface.

Limit user-controlled seed length

If the variable seed must be raw bytes, enforce a fixed maximum length and pad with zeros:

let mut padded = [0u8; 32];
padded[..name.len()].copy_from_slice(name.as_bytes());

Common Mistakes

Concatenating two variable-length seeds. Even with a delimiter byte, attacker-chosen content can include the delimiter. Always use length prefixes or hashes.

Reusing seed structures across PDA families. If [user.key] derives both a vault and a config, an instruction expecting one will accept the other. Always namespace with a constant prefix.

Forgetting the bump seed. The canonical bump must be stored and re-supplied; otherwise an attacker can pass a non-canonical bump and derive a different but valid PDA. See Bump Seed Canonicalization.

References