3. PDA (Program Derived Address)
Learn how to create and use Program Derived Addresses for secure, deterministic account management.
What is a PDA?
A Program Derived Address (PDA) is an account address that:
- Is derived deterministically from seeds + program ID
- Has no private key (only the program can sign)
- Enables programs to own and control accounts
Key concept: PDAs allow your program to “sign” transactions without needing a wallet.
Creating a PDA Account
Source: instructions/user.rs
Rust (Program Side)
use crate::constants::*;
use crate::state::*;
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct CreateUserAccount<'info> {
#[account(
init, // Create new account
payer = authority, // Who pays for rent
space = UserAccount::LEN, // Account size
seeds = [SEED_USER_ACCOUNT, authority.key().as_ref()], // PDA seeds
bump // Auto-find bump
)]
pub user_account: Account<'info, UserAccount>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
pub fn create_user_account_handler(ctx: Context<CreateUserAccount>) -> Result<()> {
let user = &mut ctx.accounts.user_account;
let clock = Clock::get()?;
user.authority = ctx.accounts.authority.key();
user.points = 0;
user.created_at = clock.unix_timestamp;
user.updated_at = clock.unix_timestamp;
user.bump = ctx.bumps.user_account; // Store bump for later use
msg!("User account created: {}", user.authority);
Ok(())
}
TypeScript (Client Side)
import { PublicKey, SystemProgram } from "@solana/web3.js";
import { Program } from "@coral-xyz/anchor";
// Derive PDA address
const [userPda, bump] = PublicKey.findProgramAddressSync(
[
Buffer.from("user_account"),
user.publicKey.toBuffer()
],
program.programId
);
console.log("User PDA:", userPda.toBase58());
console.log("Bump:", bump);
// Create user account
const tx = await program.methods
.createUserAccount()
.accounts({
userAccount: userPda,
authority: user.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([user])
.rpc();
console.log("Transaction signature:", tx);
Understanding Seeds and Bump
Seeds
Seeds are byte arrays used to derive the PDA:
seeds = [
SEED_USER_ACCOUNT, // Static seed (b"user_account")
authority.key().as_ref() // Dynamic seed (user's pubkey)
]
Common seed patterns:
| Pattern | Example | Use Case |
|---|---|---|
| Static only | [b"config"] | Singleton account |
| Static + Pubkey | [b"user", user_key] | User-specific account |
| Static + Multiple | [b"vault", mint, owner] | Token vault per mint+owner |
| Static + Number | [b"nft", &id.to_le_bytes()] | Numbered items |
Bump Seed
The bump is a number (0-255) that ensures the derived address is NOT on the ed25519 curve (no private key exists).
// Anchor automatically finds the canonical bump
#[account(
seeds = [SEED_USER_ACCOUNT, authority.key().as_ref()],
bump // Anchor finds bump automatically on init
)]
Why store the bump?
- Avoids recomputation on every transaction
- Slightly more efficient
- Canonical bump is always used
Using Stored Bump
When interacting with existing PDAs, use the stored bump:
#[derive(Accounts)]
pub struct UpdateUserAccount<'info> {
#[account(
mut,
seeds = [SEED_USER_ACCOUNT, authority.key().as_ref()],
bump = user_account.bump, // Use stored bump (more efficient)
has_one = authority @ ErrorCode::Unauthorized
)]
pub user_account: Account<'info, UserAccount>,
pub authority: Signer<'info>,
}
pub fn update_user_account_handler(
ctx: Context<UpdateUserAccount>,
points: u64
) -> Result<()> {
let user = &mut ctx.accounts.user_account;
user.points = points;
user.updated_at = Clock::get()?.unix_timestamp;
msg!("Updated user {} to {} points", user.authority, points);
Ok(())
}
PDA as Signer
PDAs can “sign” transactions using CpiContext::new_with_signer:
Source: instructions/cpi.rs
use anchor_lang::system_program::{transfer, Transfer as SystemTransfer};
#[derive(Accounts)]
pub struct TransferSolWithPda<'info> {
#[account(
mut,
seeds = [SEED_TOKEN_VAULT],
bump
)]
pub vault: SystemAccount<'info>,
#[account(mut)]
/// CHECK: Recipient can be any account
pub recipient: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
pub fn transfer_sol_with_pda_handler(
ctx: Context<TransferSolWithPda>,
amount: u64
) -> Result<()> {
// Create signer seeds for PDA
let seeds = &[
SEED_TOKEN_VAULT,
&[ctx.bumps.vault] // Include bump in seeds
];
let signer = &[&seeds[..]]; // Double slice for CPI
let cpi_accounts = SystemTransfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
};
let cpi_program = ctx.accounts.system_program.to_account_info();
// Use new_with_signer for PDA signing
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
transfer(cpi_ctx, amount)?;
msg!("Transferred {} lamports from PDA vault", amount);
Ok(())
}
Key Points for PDA Signing
- Reconstruct seeds: Use same seeds + bump
- Double slice:
&[&seeds[..]]for signer parameter - Use
new_with_signer: Notnew()
Common PDA Patterns
1. Singleton Config
// Only one config per program
#[account(
seeds = [b"config"],
bump
)]
pub config: Account<'info, ProgramConfig>,
2. User Account
// One account per user
#[account(
seeds = [b"user", user.key().as_ref()],
bump
)]
pub user_account: Account<'info, UserAccount>,
3. Token Vault
// One vault per mint
#[account(
seeds = [b"vault", mint.key().as_ref()],
bump
)]
pub vault: SystemAccount<'info>,
4. Associated Account
// Account associated with two entities
#[account(
seeds = [
b"listing",
nft_mint.key().as_ref(),
seller.key().as_ref()
],
bump
)]
pub listing: Account<'info, NftListing>,
5. Numbered Sequence
// Sequential items (NFTs, orders, etc.)
#[account(
seeds = [
b"nft",
collection.key().as_ref(),
&token_id.to_le_bytes()
],
bump
)]
pub nft: Account<'info, Nft>,
PDA Best Practices
✅ Always store bump - Saves computation
✅ Use constants for seeds - Avoid typos
✅ Make seeds unique - Prevent collisions
✅ Keep seeds simple - Easier to derive client-side
✅ Document seed patterns - Help future developers
❌ Don’t use variable-length seeds - Can cause derivation issues
❌ Don’t reuse seed patterns - Each PDA type should be unique
❌ Don’t forget bump in signer seeds - CPI will fail
Client-Side PDA Derivation
Synchronous (Recommended)
const [pda, bump] = PublicKey.findProgramAddressSync(
[Buffer.from("user_account"), userKey.toBuffer()],
programId
);
Asynchronous
const [pda, bump] = await PublicKey.findProgramAddress(
[Buffer.from("user_account"), userKey.toBuffer()],
programId
);
With Multiple Seeds
import { BN } from "@coral-xyz/anchor";
const tokenId = new BN(42);
const [nftPda] = PublicKey.findProgramAddressSync(
[
Buffer.from("nft"),
collectionKey.toBuffer(),
tokenId.toArrayLike(Buffer, "le", 8) // u64 as little-endian
],
programId
);
Debugging PDAs
Check if address is a PDA
import { PublicKey } from "@solana/web3.js";
const isPda = !PublicKey.isOnCurve(address.toBuffer());
console.log("Is PDA:", isPda);
Verify seeds match
// In program
require_keys_eq!(
user_account.key(),
Pubkey::find_program_address(
&[SEED_USER_ACCOUNT, authority.key().as_ref()],
ctx.program_id
).0,
ErrorCode::InvalidPda
);
Common Errors
| Error | Cause | Solution |
|---|---|---|
| “Seeds constraint violated” | Wrong seeds provided | Check seed order and values |
| “Address not on curve” | Using non-PDA as PDA | Derive PDA correctly |
| “Invalid bump” | Wrong bump value | Use stored bump or let Anchor find it |
| “Cross-program invocation with unauthorized signer” | Missing PDA signer seeds | Include bump in signer seeds |
Next: Account Constraints →