A cross-program invocation (CPI) is Solana's composition primitive: during your instruction, your program calls another program with an account list, instruction data, and optional PDA signer seeds. Anchor 0.32.1 wraps the raw invoke / invoke_signed path in CpiContext, generated helpers, and typed program accounts so composition is readable without hiding runtime rules.
This page is the section map. Use it to place CpiContext, PDA-signed CPIs, SPL Token helpers, custom-program modules, remaining accounts, and privilege checks on one continuum before you follow the focused recipe pages.
Anchor CPIs still obey the same SVM rules as native code. The callee must be an executable program account. Every account the callee needs must appear in the caller's instruction (or be nested correctly through further CPIs). Signer privileges come from transaction signatures or from invoke_signed with seeds that prove your program owns a PDA. Mutable accounts must be marked writable in the outer transaction when the callee will write them.
What Anchor adds is packaging. CpiContext::new holds the program AccountInfo and a struct of accounts. CpiContext::new_with_signer (or .with_signer) attaches seed slices so the runtime treats the PDA as a signer for that inner instruction. anchor-spl builds standard Token / Token-2022 layouts. declare_program! turns an external IDL into a cpi module with typed account structs. .with_remaining_accounts appends dynamic accounts for routers, oracles, and multi-hop flows.
The risk surface is concentrated. Wrong program id is arbitrary code in your privilege context. Wrong PDA seeds either fail the CPI or authorize the wrong account. Unvalidated remaining accounts are substitution attacks. Signing as a treasury PDA into an untrusted program is a vault drain. Treat every CPI as a privilege boundary: pin targets, validate identities, and map errors intentionally.
A CPI is synchronous nested execution inside one transaction instruction. Your handler runs, then the runtime enters the callee, then control returns so you can continue or fail. It is not a second top-level transaction instruction. Atomicity still applies: if the outer instruction fails after a successful inner CPI, the whole instruction aborts and state rolls back for that failure path.
Clients often compose multiple top-level instructions instead. That is not CPI. CPI is required when on-chain logic must decide the call, enforce intermediate checks, or sign as a PDA the user cannot sign for.
Outer instruction handler (your program) | | 1. constraints already ran on Context accounts | 2. optional remaining_accounts checks | 3. build CpiContext (program + accounts) | 4. optional with_signer / new_with_signer | 5. optional with_remaining_accounts v Runtime: invoke / invoke_signed | v Callee process_instruction | | Ok -> continue outer handler | Err -> outer sees ProgramError (map or propagate) v Outer Ok(()) commits instruction | any Err aborts instruction
Validate before privilege-sensitive CPI whenever possible. A callee cannot re-enter your program - the runtime rejects any invocation of a program already on the instruction stack with ReentrancyNotAllowed - but it can mutate accounts you already deserialized. Prefer checks-effects-interactions style for that reason, and reload() any account you read again after the CPI.
Unsigned (or user-signed) CPIs use CpiContext::new(program, accounts):
use anchor_lang::system_program::{self, Transfer};system_program::transfer( CpiContext::new( ctx.accounts.system_program.to_account_info(), Transfer { from: ctx.accounts.payer.to_account_info(), to: ctx.accounts.recipient.to_account_info(), }, ), lamports,)?;
The first argument is the executable program account, not a random pubkey. Prefer Program<'info, System> (or Token, or generated program types) so Anchor rejects wrong program ids at constraint time. See CPI Basics in Anchor.
When a PDA is authority (vault, mint authority, escrow), pass seeds the runtime can hash back to that address under your program id:
use anchor_spl::token_interface::{self, TransferChecked};let bump = ctx.bumps.vault;// `key()` returns a Pubkey by value - bind it to a `let` first, or the// temporary is dropped while `seeds` still borrows it (E0716).let user_key = ctx.accounts.user.key();let seeds: &[&[u8]] = &[b"vault", user_key.as_ref(), &[bump]];let signer: &[&[&[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, ), amount, ctx.accounts.mint.decimals,)?;
Rules that matter in production:
Seed order and components must match the PDA derivation and #[account(seeds = ...)] constraints.
Prefer ctx.bumps.* over hardcoded bumps after init.
The PDA account must be the authority field the callee expects.
Multiple PDAs need multiple seed groups in the signer slice.
Equivalent style: build CpiContext::new(...) then .with_signer(signer). Pick one style per codebase. Full patterns: Signed CPIs.
anchor-spl 0.32.1 exposes helpers such as transfer_checked, mint_to, burn, and approve that pack the correct account metas and instruction data for the SPL Token program. anchor_spl::token::{transfer, Transfer} still exists but is deprecated - it does not carry the mint or decimals, so prefer transfer_checked in new code.
use anchor_spl::token_interface::{self, TransferChecked};token_interface::transfer_checked( CpiContext::new( ctx.accounts.token_program.to_account_info(), TransferChecked { from: ctx.accounts.from_ata.to_account_info(), mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.to_ata.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }, ), amount, ctx.accounts.mint.decimals,)?;
Operational checklist:
Mark source and destination token accounts mut.
Authority must be a signer or a PDA with with_signer.
Pass the same mint both ATAs reference, and take decimals from that mint account.
For Token-2022 and dual support, use anchor_spl::token_interface throughout - Interface<'info, TokenInterface> for the program and InterfaceAccount<'info, Mint> / InterfaceAccount<'info, TokenAccount> for the state. Never mix anchor_spl::token::Mint with InterfaceAccount.
For other Anchor (or IDL-compatible) programs, Anchor 0.32.1 declare_program!(name) reads compile-time IDL JSON and generates name::cpi::... functions and name::cpi::accounts::... structs.
Pin the IDL to the deployed program version. Stale IDLs compile cleanly and fail (or worse, mis-decode) at runtime. Use the generated Program<'info, marketplace::program::Marketplace> type (names vary by IDL) so the program id cannot be substituted. Non-Anchor callees may still need manual discriminators and invoke / invoke_signed. See CPI to Custom Programs.
Some callees expect a variable tail: hop routes, oracle sets, optional fees. Anchor exposes accounts beyond the fixed #[derive(Accounts)] struct as ctx.remaining_accounts. Forward them with:
Inner failures surface as ProgramError (or Anchor error codes) to the caller. Map opaque failures to your #[error_code] variants when product UX needs clear client codes. Simulation shows nested logs; do not rely on verbose msg! in production paths.
Each CPI consumes CU for setup and for the callee's work. Nested CPIs and large remaining-account walks add up. Budget and measure hot instructions under Agave 4.1.1 local tooling before mainnet.
A callee cannot CPI back into your program. The Agave runtime rejects any invocation of a program already on the instruction stack with InstructionError::ReentrancyNotAllowed; direct self-recursion is the only exception. Designing around a callback into yourself produces a hard transaction failure, not a clever pattern.
What you must still design for is state divergence: a callee can mutate accounts you already deserialized, so your in-memory copies go stale the moment the CPI returns. Complete critical state transitions before you CPI out (checks-effects-interactions), call reload() on any Anchor account you read again afterwards, and remember that separate top-level instructions in the same transaction can interleave against assumptions your handler made. This is especially sharp for vaults, mints, and shared config accounts.
Signing as a high-value PDA is a deliberate grant of authority. Scope which instructions may use new_with_signer for treasury seeds. Never attach treasury seeds to a CPI whose program id or instruction selector is client-controlled. Prefer narrow authorities (one mint authority PDA, one vault PDA) over a single "god" PDA used for every external call.
Tests should cover more than happy paths: wrong program id, wrong mint, bad bump, missing mut, remaining-account order swaps, and callee custom errors. Local validators under Solana CLI 3.0.10 exercise full SBF and logs. Keep IDL fixtures in-repo for declare_program! so CI cannot drift from deployed layouts without a deliberate bump.
Outer transactions built with @solana/kit 7.0.0 (or Anchor TS) must list every account the full CPI tree will touch, with correct signer and writable flags. A missing writable flag fails at runtime even if Rust types look fine. When remaining accounts matter, publish the ordering contract next to the IDL.
Misconception: Anchor constraints fully validate the CPI target.
Constraints validate accounts you declared. They do not automatically validate every remaining account or every business rule inside a third-party program.
Misconception: Any PDA can sign if I pass seeds.
Only PDAs derived under your program id can be signed by your program. Another program's PDA requires a CPI into that owner.
Misconception: Program<'info, Token> is optional if I hardcode the Token program key in data.
Hardcoded keys in instruction data are not the same as pinning the executable account. Always pass and type-check the program account.
Misconception: Signed CPI is only for SOL transfers.
Any instruction that needs a PDA authority (token transfer, mint, burn, approve, custom marketplace list) uses the same seed mechanism.
Misconception: Remaining accounts are "unchecked so they are free."
They are unchecked by Anchor's account macros until you check them. Unvalidated tails are a top-tier exploit class.
Misconception: Client multi-instruction composition replaces CPI for PDA custody.
If only the program can sign as the vault, the client cannot submit a standalone token transfer as that vault. CPI with seeds is required.
Misconception: Mapping every CPI error to a single SomethingFailed is fine.
Prototypes can. Production clients need stable, specific codes for retries and incident response.
Misconception: declare_program! freezes security forever.
Callee upgrades change behavior under the same program id. Re-audit after upgrades even when the IDL still parses.
A typed call into another program built with CpiContext (and usually a helper module) that compiles down to invoke or invoke_signed at runtime.
When do I use CpiContext::new versus new_with_signer?
Use new when all required signers already signed the outer transaction. Use new_with_signer (or .with_signer) when a PDA of your program must authorize the inner instruction.
Where do signer seeds come from?
The same seed components used to derive the PDA, plus the bump (often from ctx.bumps). They must match on-chain derivation exactly.
How do SPL Token transfers work from a program?
Include the token accounts, the mint, and Interface<TokenInterface> (or Program<Token> for classic-only), build a TransferChecked account struct, and call token_interface::transfer_checked with a CpiContext, passing amount and the mint's decimals. PDA authorities need signer seeds.
What is declare_program! for?
It generates compile-time CPI clients from an external program's IDL so you get typed account structs and instruction helpers instead of hand-built discriminators.
How do remaining accounts relate to the IDL?
Fixed accounts live in the accounts struct and IDL. Extra accounts clients append after that list become remaining_accounts and must be validated and ordered per the callee's rules.
Can a CPI fail while my earlier state writes remain?
If the outer instruction returns Err after a CPI, the instruction fails as a unit. Structure code so you do not depend on partial success when the outer path can still fail.
Why pin Program<T> instead of UncheckedAccount for programs?
Program<T> checks the account is the expected executable id (and executable). Unchecked program accounts invite substitution attacks.
How deep can CPIs nest?
MAX_INSTRUCTION_STACK_DEPTH is 5. The top-level instruction occupies height 1, so you get 4 levels of nested CPI below it. Only nesting consumes depth: a handler that calls five different programs one after another is five CPIs all at height 2 and never approaches the cap. Design for shallow trees anyway - deep dynamic graphs are hard to reason about for CU and for post-CPI state freshness.
Do I need CPI to read another program's account?
No. Reading account data you were passed does not require CPI. CPI is for executing another program's instruction logic.
How should clients prepare accounts for CPI-heavy instructions?
List every account the outer program and all nested callees need, with correct signer/writable flags, matching the program's IDL and remaining-account spec, using @solana/kit 7.0.0 or Anchor client tooling.