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.
Search across all documentation pages
PDAs sign CPIs through seed slices passed to CpiContext::new_with_signer. The seeds must match the PDA constraints used at initialization.
let seeds: &[&[u8]] = &[b"vault", &[ctx.bumps.vault]];
let signer: &[&[&[u8]]] = &[seeds];
token_interface::transfer_checked(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
accounts,
signer,
),
amount,
decimals,
)?;invoke_signed and new_with_signer both take &[&[&[u8]]]: the outer slice holds one seed group per PDA, and each group is the seed list for that PDA ending in its bump byte.
When to reach for this: A PDA is authority over token accounts or owns SOL being transferred.
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{
self, Mint, TokenAccount, TokenInterface, TransferChecked,
};
#[account]
pub struct Vault {
pub authority: Pubkey,
pub bump: u8,
}
#[derive(Accounts)]
pub struct VaultWithdraw<'info> {
#[account(seeds = [b"vault", vault.authority.as_ref()], bump = vault.bump)]
pub vault: Account<'info, Vault>,
#[account(mut, token::mint = mint, token::authority = vault)]
pub vault_ata: InterfaceAccount<'info, TokenAccount>,
#[account(mut, token::mint = mint)]
pub user_ata: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn vault_withdraw(ctx: Context<VaultWithdraw>, amount: u64) -> Result<()> {
// Bind the Pubkey to a local first. `ctx.accounts.vault.authority` is a
// Pubkey by value; calling `.as_ref()` on a temporary inside the seed
// array is an E0716 borrow error.
let authority_key = ctx.accounts.vault.authority;
let bump = ctx.accounts.vault.bump;
let seeds: &[&[u8]] = &[b"vault", authority_key.as_ref(), &[bump]];
let signer_seeds: &[&[&[u8]]] = &[seeds];
token_interface::transfer_checked(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
TransferChecked {
from: ctx.accounts.vault_ata.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.user_ata.to_account_info(),
authority: ctx.accounts.vault.to_account_info(),
},
signer_seeds,
),
amount,
ctx.accounts.mint.decimals,
)?;
Ok(())
}What this demonstrates:
&[&[&[u8]]] - one inner group per PDAPubkey values to a let before .as_ref() in the seed arraytransfer_checked / TransferChecked - anchor_spl::token::{transfer, Transfer} are deprecated, and the spl_token::instruction::transfer they build rejects the Token-2022 program id outrightAnchor CPI helpers accept with_signer for the same semantics as native invoke_signed.
Include every seed element in the same order used in #[account(seeds = [...])].
| 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 |
0.32.1 throughout this section.
No. Use seeds constraints and CPI signing.
In your account struct field, set at initialization.
Use @solana/kit 7.0.0 with matching seed bytes.
Your Anchor program's declare_id address.
Not in seeds array; bump is separate parameter to find_program_address.
Compare logged keys; verify seeds and program id client-side.
Yes when holding data; fund with payer on init.
Yes with same signer seeds for each CPI.
See Related links for deeper signing pda topics.
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026