Signing with PDAs
PDAs sign CPIs through seed slices passed to CpiContext::new_with_signer. The seeds must match the PDA constraints used at initialization.
Recipe
let seeds = &[b"vault".as_ref(), &[ctx.bumps.vault]];
let signer = &[&seeds[..]];
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
accounts,
signer,
),
amount,
)?;When to reach for this: A PDA is authority over token accounts or owns SOL being transferred.
Working Example
pub fn vault_withdraw(ctx: Context<VaultWithdraw>, amount: u64) -> Result<()> {
let bump = ctx.bumps.vault;
let authority_key = ctx.accounts.vault.authority;
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,
)?;
Ok(())
}What this demonstrates:
- Signer seeds are nested slices
&[&[&[u8]]] - Bump must be canonical
- PDA account info is the authority meta
- Use ctx.bumps when seeds match field name
Deep Dive
invoke_signed vs Anchor CPI
Anchor CPI helpers accept with_signer for the same semantics as native invoke_signed.
Multi-seed PDAs
Include every seed element in the same order used in #[account(seeds = [...])].
Gotchas
- Wrong bump in signer array - CPI signature fails.. Fix: Use stored bump or ctx.bumps.
- Missing seed component - Derived address mismatch.. Fix: Copy seeds from constraint exactly.
- Authority meta not PDA account - Token program rejects.. Fix: Pass vault PDA as authority.
- Reusing seed buffer incorrectly - Lifetime/ borrow issues.. Fix: Build seeds array per CPI call.
- PDA signs without constraint on account - Attacker substitutes PDA.. Fix: Validate seeds on-chain.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Delegate to human signer | User pays fees directly | Program custody |
| Multisig PDA patterns | Shared authority | Single seed PDA |
FAQs
What Anchor version is assumed?
0.32.1 throughout this section.
Can a PDA be a Signer in accounts struct?
No. Use seeds constraints and CPI signing.
Where is bump stored?
In your account struct field, set at initialization.
How do clients derive PDAs?
Use @solana/kit 7.0.0 with matching seed bytes.
What program id is used for PDAs?
Your Anchor program's declare_id address.
Do seeds include bump?
Not in seeds array; bump is separate parameter to find_program_address.
How to debug PDA failures?
Compare logged keys; verify seeds and program id client-side.
Are PDAs rent-exempt?
Yes when holding data; fund with payer on init.
Can one PDA sign multiple CPIs in one ix?
Yes with same signer seeds for each CPI.
What to read next?
See Related links for deeper signing pda topics.
Related
- Signed CPIs - CPI patterns
- PDA Authorities - mints and vaults
- Storing Bumps - canonical bump
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.