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.
Recommended Fix
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:
- A constant prefix namespaces this PDA family from any other PDA in the program.
- The user key is fixed-width, so the boundary is unambiguous.
- The variable-length
nameis 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 Anchor’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.