Anchor CPIs In Detail
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.
Summary
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.
Foundations
What a CPI is (and is not)
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.
Why Anchor wraps invoke
Native code builds an Instruction { program_id, accounts, data } and calls invoke or invoke_signed. Anchor generates those pieces from:
| Piece | Anchor representation |
|---|---|
| Target program | First argument to CpiContext::new / Program<T> account |
| Account metas | Struct fields converted via ToAccountInfos / ToAccountMetas |
| Instruction data | Function args on token::transfer, generated cpi::foo, etc. |
| PDA signers | signer_seeds on the context |
You still pay compute units for the inner program and still must release conflicting borrows before CPI on accounts you also mutate locally.
Privilege model in one table
| Privilege source | How it appears on CPI |
|---|---|
| User / keypair signature | Account is_signer true from the outer transaction |
| PDA of your program | invoke_signed / CpiContext::new_with_signer with matching seeds |
| PDA of another program | You cannot sign it; CPI into the owner program instead |
| Writable | Outer AccountMeta must allow writes if callee writes |
The runtime does not invent authority. It only verifies signatures already present and seed proofs you supply for your program id.
Where each recipe page sits
| Topic | Role |
|---|---|
| CPI Basics in Anchor | CpiContext patterns and first examples |
| SPL Token CPIs | anchor-spl transfer, mint, burn, Token-2022 interface |
| Signed CPIs | PDA seeds, bumps, multi-signer groups |
| CPI to Custom Programs | declare_program! and IDL-pinned modules |
| Passing Remaining Accounts | Dynamic account tails |
| CPI Safety | Program pinning, reentrancy, privilege limits |
Mechanics
One CPI is a fixed pipeline: validate outer accounts, build context, optional seeds and remaining accounts, invoke, handle the result.
End-to-end CPI pipeline
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. Prefer checks-effects-interactions style when a callee might re-enter your program.
1. CpiContext::new
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.
2. Signed CPIs with PDA seeds
When a PDA is authority (vault, mint authority, escrow), pass seeds the runtime can hash back to that address under your program id:
let bump = ctx.bumps.vault;
let seeds: &[&[u8]] = &[b"vault", user.key().as_ref(), &[bump]];
let signer = &[seeds];
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
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,
),
amount,
)?;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.
3. SPL Token CPIs via anchor-spl
anchor-spl 0.32.1 exposes helpers such as transfer, mint_to, burn, and approve that pack the correct account metas and instruction data for the SPL Token program.
use anchor_spl::token::{self, Transfer};
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.from_ata.to_account_info(),
to: ctx.accounts.to_ata.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
),
amount,
)?;Operational checklist:
- Mark source and destination token accounts
mut. - Authority must be a signer or a PDA with
with_signer. - Validate mint alignment between ATAs when amounts represent the same asset.
- For Token-2022 and dual support, use
anchor_spl::token_interfacewithInterface<'info, TokenInterface>rather than hardcoding classic Token only.
Recipe depth: SPL Token CPIs.
4. CPI to custom programs
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.
declare_program!(marketplace);
marketplace::cpi::list_item(
CpiContext::new(
ctx.accounts.marketplace_program.to_account_info(),
marketplace::cpi::accounts::ListItem { /* mirror IDL accounts */ },
),
price,
)?;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.
5. Remaining accounts
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:
let mut cpi_ctx = CpiContext::new(
ctx.accounts.target_program.to_account_info(),
target::cpi::accounts::Handle { /* fixed fields */ },
);
cpi_ctx = cpi_ctx.with_remaining_accounts(ctx.remaining_accounts.to_vec());
target::cpi::handle(cpi_ctx)?;Before forwarding:
- Cap length (
require!(len <= MAX)). - Check owner, key allowlists, discriminators, and freshness as required by the threat model.
- Preserve order documented by the callee and by your client builders (
@solana/kit7.0.0 or Anchor clients).
Detail: Passing Remaining Accounts.
6. Errors and compute
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.
Advanced
Reentrancy and shared state
Callees can CPI back into your program if clients (or intermediate programs) include your program id and accounts. Design for reentry: set in-progress flags before CPI, complete critical state transitions before yielding, and never assume invariants that a recursive entry can break mid-flight. This is especially sharp for vaults, mints, and shared config accounts.
Privilege minimization
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.
Hybrid composition
Common production shape:
- User-signed token deposit into a vault ATA (no PDA sign).
- Program logic updates internal state.
- PDA-signed CPI to a DEX, lending market, or marketplace IDL module.
- Optional remaining accounts for route hops validated against an allowlist.
Document for auditors which program ids are reachable, which seeds sign, and which remaining-account shapes clients may append.
Testing CPIs
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.
Clients and account metas
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.
Common Misconceptions
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.
FAQs
What is a CPI in Anchor terms?
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 token accounts and Program<Token> (or Token interface), build a Transfer account struct, and call token::transfer with a CpiContext. 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?
The runtime enforces a maximum CPI call depth. Design for shallow trees; deep dynamic graphs are hard to reason about for CU and reentrancy.
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.
Where should I go next in this section?
Start with CPI Basics in Anchor, then SPL Token CPIs and Signed CPIs. Add CPI to Custom Programs and Passing Remaining Accounts when composing externally. Close the loop with CPI Safety.
Related
- CPI Basics in Anchor -
CpiContextconstructors and first working patterns - SPL Token CPIs -
anchor-spltransfer, mint, burn, and Token-2022 interface - Signed CPIs - PDA seeds, bumps, and
with_signerdiscipline - CPI to Custom Programs -
declare_program!IDL modules and upgrade hygiene - Passing Remaining Accounts - dynamic tails, order, and caps
- CPI Safety - program pinning, reentrancy, and privilege boundaries
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.