PDAs in Anchor Key Points
A Program Derived Address (PDA) is a public key derived from seed bytes and a program ID that sits off the ed25519 curve, so it has no private key. Your Anchor program is the only actor that can authorize actions for that address, by proving the seeds and bump to the runtime during a CPI.
This page is the conceptual map for PDAs, Signer Seeds & Anchor - constraint-driven derivation, bump storage, PDA signing, authority patterns, client-side derivation, and the failure modes that show up in production on Agave 4.1.1.
Summary
- Anchor turns PDAs into validated, deterministic accounts:
seedsandbumpconstraints prove address identity on every instruction, and the same seed tuple is what the program uses later to sign CPIs. - Insight: Escrows, vaults, mints, config accounts, and per-user state almost always live at PDAs. Wrong seeds, missing constraints, or broken signer seeds mean stuck tokens, spoofable state, or silent address drift between client and program.
- Key Concepts: seeds, canonical bump,
find_program_address/create_program_address,#[account(seeds, bump)],ctx.bumps, stored bump,CpiContext::new_with_signer, PDA authority, client seed parity. - When to Use This Model: Designing Anchor account graphs that need deterministic addresses, program custody, or per-entity sharding; wiring TypeScript clients with @solana/kit 7.0.0; auditing instructions that touch vaults or mint authorities.
- Limitations/Trade-offs: PDAs cannot be ordinary
Signeraccounts; seed formulas are forever once accounts exist; seed arrays and bump search cost CU; clients and on-chain code must stay byte-identical on seeds and program ID. - Related Topics: PDAs in Anchor, Signing with PDAs, Storing Bumps, PDA Authorities, Client-Side PDA Derivation, Common PDA Mistakes.
Foundations
Solana programs do not hold application state inside their executable. Durable records live in accounts the program owns. A PDA is a special kind of address for those accounts (and for pure signer roles that never hold data): it is computed, not generated from a keypair.
(address, bump) = find_program_address(seeds, program_id)find_program_address tries bump values from 255 downward until the point is off-curve. That first valid value is the canonical bump. create_program_address verifies a candidate with a known bump; it does not search.
Because no private key exists for an off-curve address, a normal transaction signature cannot authorize that key. Only a program that supplies the correct seeds and bump (under the program ID used in derivation) can mark the PDA as a signer on a nested instruction via invoke_signed / Anchor with_signer.
In Anchor 0.32.1 you declare that check instead of hand-rolling it:
#[account(
seeds = [b"escrow", maker.key().as_ref(), escrow_id.to_le_bytes().as_ref()],
bump,
)]
pub escrow: Account<'info, Escrow>,Anchor re-derives the PDA from those seeds and declare_id!, then requires the passed account key to match. Optional bump = account.bump uses a stored byte so validation does not re-search. Init paths use init, payer, space, and the same seeds so the account is created at the derived address in one step.
A PDA is not a Signer<'info> in the accounts struct. Keypair signers prove presence with a transaction signature; PDAs prove presence with seeds. Use Account, SystemAccount, or other typed wrappers with seeds / bump, and sign only when issuing a CPI.
Mechanics & Interactions
Constraints as the on-chain truth
Every instruction that touches a PDA should re-assert seeds. Clients choose which public key to pass; without constraints, an attacker can substitute a lookalike account your handler treats as the canonical vault or user record.
| Concern | Anchor mechanism | What it enforces |
|---|---|---|
| Address identity | seeds = [...], bump / bump = ... | Passed key equals PDA for program ID + seeds |
| Creation | init, payer, space, seeds | Allocate and assign at the derived address |
| Mutation | mut + seeds | Same identity checks, writable flag |
| Typed body | Account<'info, T> | Owner, discriminator, deserialize |
| CPI signing | new_with_signer + seed slices | PDA appears as signer to callee programs |
Keep seed components small and stable: a static prefix (b"vault"), a distinguishing pubkey, and maybe a little-endian id. Long or variable seed stacks burn CU and invite client bugs. Document the seed tuple next to the account type so Rust, tests, and TypeScript stay aligned.
Storing bumps
On first init, Anchor exposes the resolved bump map as ctx.bumps.<field_name>. Write that into account data:
ctx.accounts.vault.bump = ctx.bumps.vault;Later instructions use bump = vault.bump in the constraint and the same byte in CPI signer seeds. Benefits:
- CU: no repeated bump search on hot paths.
- Determinism: CPI always uses the bump that made the address valid at create time.
- Clarity: one field is the source of truth for signing.
Do not accept a client-provided bump as an instruction argument for security-critical checks. Prefer on-chain storage and constraints. If legacy accounts lack a bump field, add a migration instruction that recomputes with find_program_address once and writes the result; do not invent a bump.
See Storing Bumps for patterns and migration notes.
Signing with PDAs
When the PDA must authorize a CPI (token transfer from a vault ATA, mint_to, system transfer of lamports held by a PDA, etc.), build signer seeds that match the constraint seeds, with the bump as the final one-byte seed:
let bump = ctx.accounts.vault.bump;
let seeds: &[&[u8]] = &[b"vault", authority.key().as_ref(), &[bump]];
let signer_seeds = &[seeds];
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.vault_ata.to_account_info(),
to: ctx.accounts.user_ata.to_account_info(),
authority: ctx.accounts.vault.to_account_info(),
},
signer_seeds,
),
amount,
)?;Seed order and bytes must match init and constraints. The authority meta on the CPI must be the PDA account info, not a user wallet. One PDA can sign multiple CPIs in the same instruction with the same seeds. Wrong bump or a missing seed fails the CPI as a missing signature (the whole transaction rolls back). Native equivalent: invoke_signed. Details in Signing with PDAs.
PDA authorities
Programs use PDAs as authorities when the protocol, not a human keypair, should control assets or supply:
- Vault / escrow PDA owns an ATA holding deposits; withdraw CPIs use vault seeds.
- Treasury PDA receives fees; spend paths require program-enforced rules plus signer seeds.
- Mint authority PDA mints rewards or game items; optionally renounce later by setting mint authority to
Nonevia CPI when the design freezes supply.
Always constrain mint and ATA relationships (token::authority, associated_token::authority, mint key equality) so a valid PDA signature cannot operate on the wrong mint or token account. Prefer split PDAs per market, pool, or feature over one global authority PDA: smaller blast radius if a bug appears in one path, and better Sealevel parallelism when different markets write different keys.
See PDA Authorities.
Client-side derivation
Clients must pass the same public keys the program will re-derive. With @solana/kit 7.0.0, use getProgramDerivedAddress (or generated helpers) with:
- The program address from the IDL /
declare_id!(not a random deploy key from an old env file). - Seed bytes identical to on-chain: string prefixes as ASCII bytes matching
b"...", pubkeys as 32 raw bytes, integers as little-endian (to_le_bytesparity). - The same seed order.
const [escrowPda] = await getProgramDerivedAddress({
programAddress: programId,
seeds: [
new TextEncoder().encode("escrow"),
getBytesEncoder().encode(makerAddress),
// u64 little-endian matching Rust to_le_bytes
u64ToLeBytes(escrowId),
],
});Share constants or generate seed builders from the IDL so TypeScript cannot drift from Rust. Integration tests should assert client-derived PDAs equal addresses created in Anchor tests. See Client-Side PDA Derivation.
Advanced Considerations & Applications
External program PDAs. When you CPI into another program that owns a PDA, pass that address but do not sign for it unless you are that program. Anchor's seeds::program validates foreign PDAs against another program ID. Your program only signs PDAs derived under your program ID.
Init vs re-init. Seeds fix a slot in the address space. After close, the same seeds can reopen the same address; design close paths carefully if logic assumes "once initialized, forever trusted."
Sharding and CU. Per-user PDAs keep writable locks disjoint under Sealevel. Keep global config readonly on hot paths. Store bumps, keep seeds short, and simulate before mainnet: bump search and deep CPI stacks cost CU.
Authority lifecycle. Move mint or freeze authority to a PDA only in a controlled init. Leaving a keypair mint authority after a "PDA vault" deploy is a security bug.
Testing matrix. For each PDA type: happy-path init, wrong client seeds, wrong program ID, CPI with good and bad signer seeds, and token authority mismatch. Failure cases are catalogued in Common PDA Mistakes.
Common Misconceptions
- "A PDA is just a normal account with a fancy name." It is an off-curve address; identity and signing both depend on seeds and program ID, not a private key.
- "If I pass the right pubkey from the client, seeds constraints are optional." Without on-chain re-derivation, the client chooses trust. Attackers pass lookalike accounts.
- "PDAs belong in
Signer<'info>because they sign CPIs." Transaction-level signers are keypairs. PDAs become signers only for nested invocations via seed proof. - "I can change seed formulas in an upgrade and keep the same accounts." Addresses are fixed by seeds and program ID. New formulas are new addresses; plan migration.
- "The bump is a secret." The bump is public and recoverable; security comes from program logic and constraints, not bump secrecy. Still store and reuse the canonical bump for CU and consistency.
- "Client endianness does not matter for numeric seeds." Rust
to_le_bytesand big-endian JS buffers produce different PDAs. Match LE explicitly. - "One shared authority PDA is simpler and just as safe." Simpler until one bug drains every market. Prefer scoped PDAs when scale or isolation matters.
- "
with_signeris only for Token." Any CPI that requires the PDA as a signer (System Program, Token, Token-2022, custom callees) needs signer seeds. - "UncheckedAccount is fine for PDAs during development." It is the default path to missing ownership and seed checks. Use typed accounts with seeds from the first draft.
- "find_program_address on every CPI is free." It is correct but costs CU; store the bump after init for hot paths.
FAQs
What is a PDA in one sentence?
A deterministic, off-curve address derived from seeds and a program ID, which has no private key and can be signed for only by that program via invoke_signed / Anchor with_signer.
What Anchor version and client stack does this section assume?
Anchor 0.32.1 on-chain, Rust 1.91.1, Agave 4.1.1 / Solana CLI 3.0.10 tooling, and @solana/kit 7.0.0 for client derivation and transaction building.
How do Anchor seeds and bump constraints work?
They re-derive the program address from the listed seed expressions and expected bump, then require the account's public key to match. Init creates the account at that address; later instructions re-check identity every time.
Can a PDA be declared as Signer in an Accounts struct?
No. Use a typed account (or system account) with seeds and bump. Provide signer seeds only when building a CPI that needs the PDA as authority.
Where should the bump live?
In account data, set once at initialization from ctx.bumps, then referenced with bump = account.bump and in CPI seed arrays. See Storing Bumps.
How does a PDA move tokens?
The PDA (or a PDA-owned ATA) holds the tokens; the program CPIs into the Token program with CpiContext::new_with_signer using the vault's seeds so the PDA is the authority. See Signing with PDAs and PDA Authorities.
What program ID is used when deriving PDAs for my Anchor program?
The program ID from declare_id! / the deployed program address in the IDL. Client code must use that same ID, not another program's key.
Do seeds arrays include the bump?
In Anchor constraint syntax, bump is separate (bump or bump = ...). For create_program_address and CPI signer seeds, the bump is the final one-byte seed appended to the seed list.
How do I debug ConstraintSeeds or CPI signature failures?
Log or print client-derived address, on-chain expected seeds, program ID, and bump. Diff seed order, string encoding, integer endianness, and whether the CPI authority account is the PDA. See Common PDA Mistakes.
Are PDA accounts rent-exempt?
Data-bearing PDAs need rent-exempt lamports for their space, funded by the payer on init. Pure signer PDAs still need deliberate lamport planning depending on the pattern.
Can one PDA sign multiple CPIs in a single instruction?
Yes. Reuse the same signer seed slices for each new_with_signer call.
How should TypeScript derive the same PDA as Rust?
Use @solana/kit 7.0.0 getProgramDerivedAddress with identical seed bytes, order, little-endian integers, and program address. Prefer shared constants or codegen. See Client-Side PDA Derivation.
When should I split authority PDAs instead of one global authority?
When you have multiple markets or risk domains, or want Sealevel to run non-overlapping flows in parallel. One global writable authority path serializes traffic and concentrates exploit impact.
Does upgrading the program change PDA addresses?
No, if program ID and seed formulas stay the same. Upgrades change ProgramData bytecode only. New seeds or a new program ID yield new PDAs.
Related
- PDAs in Anchor - seeds, bump, and init constraints
- Signing with PDAs -
with_signerand CPI seed slices - Storing Bumps - canonical bump persistence and reuse
- PDA Authorities - vaults, treasuries, and mint authorities
- Client-Side PDA Derivation - matching seeds in TypeScript with kit
- Common PDA Mistakes - seed mismatches, missing constraints, signing errors
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, and @solana/kit 7.0.0.