Skip to main content
Superteam Brasil
Back
+20 XP
5/12

Program Derived Addresses (PDAs) Deep Dive

Program Derived Addresses are one of Solana's most powerful and unique features. They enable programs to "own" accounts and sign transactions without needing a private key.

What Are PDAs?

A PDA is a public key that:

  1. Is derived deterministically from seeds and a program ID
  2. Is not on the Ed25519 elliptic curve (no corresponding private key exists)
  3. Can only be "signed for" by the program that derived it
// In Anchor
let (pda, bump) = Pubkey::find_program_address(
    &[b"user", user.key().as_ref()],
    program_id
);

Why PDAs Are Off-Curve

Solana uses Ed25519 for signatures. Ed25519 keys lie on an elliptic curve. PDAs are specifically designed to fall off this curve, which means:

  • No private key can exist for a PDA
  • Only the deriving program can sign for the PDA (via CPI with invoke_signed)

This creates program-controlled accounts that can't be compromised by key theft.

PDA Derivation Process

seeds = [b"user", user_pubkey.as_ref()]
program_id = your_program_id

for bump in 255..=0 {
    candidate = sha256(seeds + [bump] + program_id)
    if !is_on_curve(candidate) {
        return (candidate, bump)  // This is your PDA
    }
}

The canonical bump is the first bump (starting from 255) that produces an off-curve address.

Seeds Design Patterns

Pattern 1: User-Scoped Account

seeds = [b"user", user.key().as_ref()]

One account per user. Used for user profiles, settings, etc.

Pattern 2: User + Resource

seeds = [b"escrow", user.key().as_ref(), trade_id.to_le_bytes().as_ref()]

Multiple accounts per user, one per resource (escrow, order, etc.).

Pattern 3: Global Singleton

seeds = [b"config"]

One global account for the program (config, authority, etc.).

Pattern 4: Nested PDAs

// User stats PDA
seeds_stats = [b"stats", user.key().as_ref()]

// User vault PDA (derived from stats PDA)
seeds_vault = [b"vault", stats_pda.key().as_ref()]

PDAs can be derived from other PDAs for complex hierarchies.

Storing the Bump

Always store the canonical bump in your account data:

#[account]
pub struct UserAccount {
    pub authority: Pubkey,
    pub bump: u8,  // Store this!
    pub data: u64,
}

Why? Because recalculating the bump on every instruction is expensive (requires up to 256 hash operations). Storing it makes subsequent operations cheaper.

PDAs as Signers

PDAs can sign CPIs using invoke_signed:

invoke_signed(
    &transfer_instruction,
    &[
        pda_account.to_account_info(),
        recipient.to_account_info(),
        system_program.to_account_info(),
    ],
    &[&[b"user", user.key().as_ref(), &[bump]]], // Signer seeds
)?;

The program derives the PDA, verifies it matches, and "signs" the CPI.

Security Considerations

1. Seed Collision

Be careful with seed design:

// BAD: Can collide if strings contain delimiters
seeds = [user_name.as_bytes()]  // "alice" and "ali" + "ce" collide!

// GOOD: Use fixed-size data or well-defined separators
seeds = [b"user", user.key().as_ref()]

2. Bump Validation

Always use the canonical bump:

// In Anchor, this is automatic:
#[account(
    seeds = [b"user", authority.key().as_ref()],
    bump = user.bump  // Validates stored bump is canonical
)]

PDA vs Regular Account

FeatureRegular AccountPDA
Has private keyYesNo
Can self-signYesNo (requires program)
Can hold SOLYesYes
Can own other accountsYesYes
Deterministic addressNoYes
On Ed25519 curveYesNo

Real-World Examples

Token vaults (escrow programs):

seeds = [b"vault", escrow.key().as_ref()]

Associated Token Accounts:

seeds = [wallet.key(), token_program.key(), mint.key()]

Governance proposal accounts:

seeds = [b"proposal", governance.key(), proposal_id.to_le_bytes()]

Next Challenge

You'll derive multiple related PDAs from a common base to understand PDA hierarchies.

PreviousNext

Discussion