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.
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)}- 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.
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,- Line 9
pub forecaster: Signer<'info>,: Requires this account to have signed the transaction. - 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. - Line 13
bump=user.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 26
close = forecaster: Closes the account at the end of the instruction and refunds its lamports. - Line 30
init,: Creates and pays for a new account. Space and payer must be specified.
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) }}- 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. - 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.
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- 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. - 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. - Line 31
#[account(seeds = [constants::CONFIG_SEED], bump, has_one = dev_treasury)]: Checks that a stored pubkey field matches the passed account. - Line 35
init,: Creates and pays for a new account. Space and payer must be specified. - Line 49
init_if_needed,: Creates the account only if it doesn't exist. Watch for re-initialisation bugs.
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"],- Line 109
init_if_needed,: Creates the account only if it doesn't exist. Watch for re-initialisation bugs. - Line 112
seeds = [b"seloongmun"],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 113
bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 117
pub signer: Signer<'info>,: Requires this account to have signed the transaction.
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,- 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
- tokengator: explore the full project
- poe-solana: explore the full project
- w3-solana: explore the full project
- valhalla: explore the full project
- seloongmun: explore the full project
- zk-lokomotive-core: explore the full project
- Next lesson: Cross-Program Invocations (CPIs)