PDAs and State Management
Program Derived Addresses (PDAs) are one of Solana's most powerful features. They enable programs to "own" accounts and sign transactions programmatically, unlocking patterns impossible in other blockchains.
What Are PDAs?
A PDA is a public key that:
- Has no private key: It's derived from seeds, not generated randomly
- Is off the Ed25519 curve: Cannot be signed by a regular keypair
- Can only be signed by the program that derives it: Using
invoke_signed
Think of PDAs as accounts controlled by code, not by a person holding a private key.
Deriving PDAs
Use Pubkey::find_program_address() (or findProgramAddressSync() in JS) to derive a PDA:
let (pda, bump) = Pubkey::find_program_address(
&[
b"user", // Static seed
user_pubkey.as_ref(), // Dynamic seed (user's pubkey)
],
program_id,
);
The function returns:
pda: The derived public keybump: A single byte (0-255) that ensures the PDA is off-curve
How It Works
Internally, Solana:
- Concatenates the seeds + program_id
- Hashes them with SHA-256
- Checks if the result is on the Ed25519 curve
- If yes, decrements the bump (starting at 255) and tries again
- Returns the first off-curve result (the "canonical bump")
// Pseudocode
for bump in (0..=255).rev() {
let hash = sha256(seeds + [bump] + program_id);
if !is_on_curve(hash) {
return (PublicKey(hash), bump);
}
}
Why PDAs?
1. Deterministic Account Addresses
You can calculate account addresses before they're created:
// Client-side: Calculate where the user's token account will be
let (token_account_pda, _) = Pubkey::find_program_address(
&[b"token", user_pubkey.as_ref(), mint_pubkey.as_ref()],
&token_program_id,
);
// No need to store this address—just recompute it when needed!
2. Program-Controlled Signing
Programs can sign transactions on behalf of PDAs using invoke_signed:
// Transfer tokens FROM a PDA (not from the user)
invoke_signed(
&transfer_instruction,
&[source_pda, destination, token_program],
&[&[b"vault", user_pubkey.as_ref(), &[bump]]], // Signer seeds
)?;
This enables:
- Escrow contracts: Lock tokens until conditions are met
- Vaults: Store user funds under program control
- Proxies: Programs calling other programs
3. One Account Per User (No Collisions)
By including the user's pubkey in the seeds, each user gets a unique PDA:
let (user_account, _) = Pubkey::find_program_address(
&[b"account", user_pubkey.as_ref()],
program_id,
);
No two users will ever get the same PDA, even if millions of users exist.
Canonical Bumps
Always use the canonical bump (the first one returned by find_program_address). Storing non-canonical bumps enables bump seed attacks where an attacker creates multiple PDAs with different bumps.
// Good: Store the canonical bump
let (pda, bump) = Pubkey::find_program_address(seeds, program_id);
account.bump = bump; // Save this for later
// Bad: Never recompute with a different bump
let pda = Pubkey::create_program_address(&[seeds, &[wrong_bump]], program_id)?;
Common PDA Patterns
Pattern 1: Global State (One PDA per Program)
let (config_pda, _) = Pubkey::find_program_address(
&[b"config"],
program_id,
);
Pattern 2: User-Specific Accounts
let (user_account, _) = Pubkey::find_program_address(
&[b"user", user_pubkey.as_ref()],
program_id,
);
Pattern 3: Associated Accounts (Multiple PDAs per User)
// User's stats PDA
let (stats_pda, _) = Pubkey::find_program_address(
&[b"stats", user_pubkey.as_ref()],
program_id,
);
// User's vault PDA
let (vault_pda, _) = Pubkey::find_program_address(
&[b"vault", user_pubkey.as_ref()],
program_id,
);
Pattern 4: Indexed PDAs (Lists)
// 10th post by this user
let (post_pda, _) = Pubkey::find_program_address(
&[b"post", user_pubkey.as_ref(), &10u64.to_le_bytes()],
program_id,
);
PDA Validation
Always verify that a passed PDA matches the expected derivation:
let expected_pda = Pubkey::create_program_address(
&[b"user", user_pubkey.as_ref(), &[bump]],
program_id,
)?;
if account.key != &expected_pda {
return Err(ProgramError::InvalidAccountData);
}
This prevents attackers from passing malicious accounts.
Next Challenge
You'll derive multiple PDAs using different seed patterns and verify they're off-curve using the PublicKey.isOnCurve() mock method!