Sol.College

Program Derived Addresses (PDAs)

A PDA is an address derived from a program ID plus a list of seeds, deliberately chosen so that it has no private key. Only the deriving program can sign for it (via invoke_signed). Programs use PDAs as deterministic homes for state: a vault per user, a config account per program, an escrow per offer. Clients re-derive the same address from the same seeds, so nothing needs to be stored off-chain to find an account.

Reference: Solana docs: Program Derived Address · Anchor docs: PDAs

Real code, line by line

6 excerpts from different projects. Each caption links the exact lines at the fork's pinned commit. The notes below each excerpt explain the lines that matter.

pubkeyapp/tokengator · libs/api/solana/util/src/lib/tokengator-minter.ts L49–74 @ b87eed9 · MIT · original
  return PublicKey.findProgramAddressSync([PREFIX, MINTER, mint.toBuffer(), encodeToBuffer(name)], programId)} export function getCommunityPda(name: string, programId: PublicKey) {  return PublicKey.findProgramAddressSync([PREFIX, encodeToBuffer(name)], programId)} export function getWNSGroupPda(mint: PublicKey, programId: PublicKey) {  return PublicKey.findProgramAddressSync([encodeToBuffer('group'), mint.toBuffer()], programId)} export function getWNSMemberPda(mint: PublicKey, programId: PublicKey) {  return PublicKey.findProgramAddressSync([encodeToBuffer('member'), mint.toBuffer()], programId)} export function getWNSManagerPda(programId: PublicKey) {  return PublicKey.findProgramAddressSync([encodeToBuffer('manager')], programId)} function encodeToBuffer(input: string) {  return new TextEncoder().encode(input)} export function getActivityPda({ programId, mint, label }: { label: string; mint: PublicKey; programId: PublicKey }) {  return PublicKey.findProgramAddressSync([PREFIX, ACTIVITY, mint.toBuffer(), encodeToBuffer(label)], programId)}
  1. Line 49 return PublicKey.findProgramAddressSync([PREFIX, MINTER, mint.toBuffer(), encodeToBuffer(n: Derives a PDA from seeds and the program ID and returns the address with its canonical bump.
ProofOfEstimate/poe-solana · programs/poe/src/contexts/remove_estimate.rs L8–33 @ 29a7c77 · MIT · original
    #[account(mut)]    pub forecaster: Signer<'info>,    #[account(        mut,        seeds=[User::SEED_PREFIX.as_bytes(), forecaster.key().as_ref()],        bump=user.bump,      )]    pub user: Box<Account<'info, User>>,    #[account(      mut,      seeds=[Poll::SEED_PREFIX.as_bytes(), &poll.id.to_le_bytes()],      bump=poll.bump    )]    pub poll: Box<Account<'info, Poll>>,    #[account(      mut,      seeds=[UserEstimate::SEED_PREFIX.as_bytes(), poll.key().as_ref(), forecaster.key().as_ref()],      bump = user_estimate.bump,      close = forecaster    )]    pub user_estimate: Box<Account<'info, UserEstimate>>,    #[account(        init,        payer = forecaster,        seeds=[PollEstimateUpdate::SEED_PREFIX.as_bytes(), poll.key().as_ref(), &poll.num_estimate_updates.to_le_bytes()],        space= PollEstimateUpdate::LEN,
  1. Line 9 pub forecaster: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 12 seeds=[User::SEED_PREFIX.as_bytes(), forecaster.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  3. Line 13 bump=user.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  4. Line 26 close = forecaster: Closes the account at the end of the instruction and refunds its lamports.
  5. Line 30 init,: Creates and pays for a new account. Space and payer must be specified.
solanafuns/w3-solana · src/pda_helper.rs L20–42 @ 4516015 · MIT · original
        }        seeds    }     pub fn find_program_address_by_text(&self, seed_text: &str) -> (Pubkey, u8) {        let seeds = Self::get_seeds(seed_text);        Pubkey::find_program_address(seeds.as_ref(), &self.program)    }     pub fn find_program_address_by_text_suffix(        &self,        seed_text: &str,        suffix: &[u8],    ) -> (Pubkey, u8) {        let mut seeds = Self::get_seeds(seed_text);        seeds.push(suffix);        Pubkey::find_program_address(seeds.as_ref(), &self.program)    }     pub fn find_program_address(&self, seeds: &[&[u8]]) -> (Pubkey, u8) {        Pubkey::find_program_address(seeds, &self.program)    }}
  1. Line 24 pub fn find_program_address_by_text(&self, seed_text: &str) -> (Pubkey, u8) {: Derives a PDA from seeds and the program ID and returns the address with its canonical bump.
  2. Line 25 let seeds = Self::get_seeds(seed_text);: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
restacked-dev/valhalla · anchor/programs/valhalla/src/instructions/create.rs L27–52 @ 7aa9ddf · MIT · original
     #[account(mut)]    pub dao_treasury: SystemAccount<'info>,     #[account(seeds = [constants::CONFIG_SEED], bump, has_one = dev_treasury)]    pub config: Box<Account<'info, Config>>,     #[account(        init,        payer = creator,        seeds = [            identifier.to_le_bytes().as_ref(),            creator.key().as_ref(),            mint.key().as_ref(),            constants::VAULT_SEED,        ],        space = Vault::INIT_SPACE,        bump    )]    pub vault: Box<Account<'info, Vault>>,     #[account(        init_if_needed,        seeds = [            vault.key().as_ref(),            constants::VAULT_ATA_SEED
  1. Line 31 #[account(seeds = [constants::CONFIG_SEED], bump, has_one = dev_treasury)]: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  2. Line 31 #[account(seeds = [constants::CONFIG_SEED], bump, has_one = dev_treasury)]: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  3. Line 31 #[account(seeds = [constants::CONFIG_SEED], bump, has_one = dev_treasury)]: Checks that a stored pubkey field matches the passed account.
  4. Line 35 init,: Creates and pays for a new account. Space and payer must be specified.
  5. Line 49 init_if_needed,: Creates the account only if it doesn't exist. Watch for re-initialisation bugs.
khtang-7777/seloongmun · anchor/programs/seloongmun/src/lib.rs L108–133 @ e314413 · MIT · original
    #[account(        init_if_needed,        payer = signer,        space = 8 + 11,        seeds = [b"seloongmun"],        bump,    )]    pub seloongmun: Account<'info, Seloongmun>,    #[account(mut)]    pub signer: Signer<'info>,    pub system_program: Program<'info, System>,} #[derive(Accounts)]pub struct Draw<'info> {    #[account(mut)]    pub player: Signer<'info>,    #[account(        mut,        seeds = [b"safevault"],        bump,    )]    pub safe_vault: Account<'info, SafeVault>,    #[account(        mut,        seeds = [b"seloongmun"],
  1. Line 109 init_if_needed,: Creates the account only if it doesn't exist. Watch for re-initialisation bugs.
  2. Line 112 seeds = [b"seloongmun"],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  3. Line 113 bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  4. Line 117 pub signer: Signer<'info>,: Requires this account to have signed the transaction.
zklambda/zk-lokomotive-core · zkl-messenger/handlers/solana.ts L154–179 @ 3da6216 · MIT · original
            {}           ));     const whCoreBridge = new anchor.web3.PublicKey(srcNetwork.bridgeAddress);    const whConfig = findProgramAddressSync([Buffer.from("Bridge")], whCoreBridge)[0];    const whFeeCollector = findProgramAddressSync([Buffer.from("fee_collector")], whCoreBridge)[0];    //The emitter address for Solana contracts is *NOT* the contract address, but rather a PDA of the Contract    const whDerivedEmitter = findProgramAddressSync([Buffer.from("emitter")], messenger.programId)[0];    const whSequence = findProgramAddressSync([Buffer.from("Sequence"), whDerivedEmitter.toBytes()], whCoreBridge)[0];        //Throw away account used to store the message as logs on Solana are not archived for long term retreival      const whMessageKeypair = anchor.web3.Keypair.generate();        const [configAcc, configBmp] = findProgramAddressSync([        Buffer.from("config")    ], messenger.programId);           const tx = await messenger.methods                            .sendMsg(msg)                            .accounts({                                coreBridge: whCoreBridge,                                wormholeConfig: whConfig,                                wormholeFeeCollector: whFeeCollector,                                wormholeDerivedEmitter: whDerivedEmitter,                                wormholeSequence: whSequence,
  1. Line 158 const whConfig = findProgramAddressSync([Buffer.from("Bridge")], whCoreBridge)[0];: Derives a PDA from seeds and the program ID and returns the address with its canonical bump.

Go further