CPIs Explained
A cross-program invocation (CPI) is how one on-chain program asks another program to run an instruction inside the same transaction. Your handler builds an Instruction (program id, account metas, data), passes a matching slice of AccountInfo values, and the runtime loads the callee and executes it before control returns to you.
CPIs are the mechanism behind Solana composability: vaults that move SOL, escrows that transfer SPL tokens, routers that call DEXes, and any flow where ownership rules force you through another program instead of writing its accounts yourself.
This page is the umbrella for Cross-Program Invocations. Sibling pages go deep on basics, account passing, invoke vs invoke_signed, System and Token targets, depth limits, reentrancy, and patterns. Here you get one coherent model so those pages fit as zooms, not separate stories.
Summary
- A CPI is a nested call into another program: the caller supplies instruction data and a privilege-constrained account list; the callee runs under the same transaction locks, signatures, and compute budget.
- Insight: You cannot rewrite accounts another program owns. Transfers, mints, account creation, and most protocol integrations require CPI into the owner program. Wrong accounts, wrong sign path, or unsafe ordering produce failed txs or security bugs.
- Key Concepts: Instruction, AccountMeta / AccountInfo, privilege propagation,
invoke,invoke_signed, PDA seeds, CPI depth, System Program CPI, SPL Token CPI, checks-effects-interactions, allowlisted callees. - When to Use: Any time your program must create accounts, move SOL or tokens, mint/burn under a program authority, or compose with another on-chain protocol in one atomic transaction.
- Limitations/Trade-offs: Nested depth is finite, CU is shared, every account must be listed up front, and arbitrary callee program ids are a trust surface. Atomic multi-program success is powerful; failure aborts the whole transaction.
- Related Topics: CPI Basics, invoke vs invoke_signed, System and Token CPIs, depth limits, reentrancy, composability patterns.
Foundations
Solana programs are stateless executables. Durable balances and layouts live in accounts owned by specific programs. The owner-writes rule is absolute: only the Token Program may rewrite a token account's data; only the System Program performs the usual create/assign/transfer lifecycle for system-owned accounts; only your program may rewrite accounts it owns.
When your logic needs someone else's owned state to change, you do not poke bytes. You CPI into the owner with the instruction that program defines.
At the API level a CPI looks like building the same shape a client would send as a top-level instruction:
Outer transaction
+-- Instruction: YourProgram
| accounts: [user, vault, token_src, token_dst, token_program, system, ...]
| data: your discriminator + args
|
| (inside YourProgram)
| invoke / invoke_signed
| |
| v
+-- Nested Instruction: TokenProgram (or System, DEX, ...)
accounts: subset of the outer list (correct order + flags)
data: transfer / mint / create_account / ...Three facts fall out of that picture:
- Same transaction, same atomicity. If the callee fails, your instruction fails, and the whole transaction rolls back.
- No ambient discovery. Every account the nested call needs must already be in the outer transaction's account list (with usable signer/writable privileges). CPIs do not fetch undeclared keys mid-flight.
- Privileges do not magically grow. Writable and signer capabilities on the CPI are bounded by what the outer message already authorized, plus PDA signatures your program may add with
invoke_signed.
Clients (including those built with @solana/kit 7.0.0) still assemble the top-level message: they list programs and accounts, sign with wallets, and set compute budget. Your program then stitches nested calls from that closed world of accounts.
Mechanics & Interactions
Building the nested instruction
A CPI uses solana_program::instruction::Instruction:
| Field | Role |
|---|---|
program_id | Callee program public key |
accounts | Vec<AccountMeta>: pubkey, is_signer, is_writable |
data | Opaque bytes the callee deserializes (opcode + args) |
You pass that instruction to invoke or invoke_signed with a slice of AccountInfo clones whose order and keys match the metas. Prefer official helpers (system_instruction::transfer, spl_token::instruction::transfer) so data layout and meta order stay correct.
Account passing and privilege rules
Account lists for CPIs are a security surface, not just bookkeeping.
- Order must match what the callee expects (helpers encode this; custom programs document it).
- Writable should be least privilege: mark writable only accounts the callee must mutate.
- Signer on a meta means the account must be a recognized signer for that nested call: either a wallet that signed the outer transaction, or a PDA your program signs for via seeds.
- Program accounts (Token, System, or a partner program) are usually readonly entries so the runtime can load the callee.
Privilege escalation bugs look like marking an account writable or signer on the CPI when the outer transaction never intended that capability. Validate flags from the outer AccountInfo before you promote them into nested metas. See Passing Accounts to CPIs for the operational checklist.
invoke vs invoke_signed
invoke | invoke_signed | |
|---|---|---|
| Forwards outer transaction signatures | Yes | Yes |
| Adds PDA "signatures" from seeds | No | Yes |
| Typical use | User-signed authority already present | Vault PDA, mint authority PDA, program-owned escrow |
| Failure mode if misused | MissingRequiredSignature when PDA must authorize | Wrong seeds / bump → signature check fails |
// Wallet (or other outer signer) is already is_signer on the account
invoke(&ix, &[from.clone(), to.clone(), authority.clone(), token_program.clone()])?;
// PDA authority: program proves seeds + bump under its program id
invoke_signed(
&ix,
&[vault.clone(), to.clone(), system_program.clone()],
&[&[b"vault", user.key.as_ref(), &[bump]]],
)?;invoke_signed does not invent wallet signatures. It only lets the current program authorize PDAs derived from its program id and the provided seed slices. Seed design and bump discipline live with PDAs; the CPI choice is simply "does the callee need a PDA as signer?" Full comparison: invoke vs invoke_signed.
Depth limits and compute
The runtime caps how deep nested CPIs may go. On current Solana (including Agave 4.1.1), maximum CPI depth is 4. Deep trees (router → DEX → token → transfer hook → …) hit the limit quickly.
Compute units are transaction-wide. Nested calls do not get a fresh CU wallet; heavy callees and logging multiply cost under the default ~1.4M CU budget (adjustable via Compute Budget instructions within protocol caps). Design flat when you can: one well-shaped instruction with a few CPIs beats a deep call chain. Details: CPI Depth & Limits.
Everyday targets: System and SPL Token
System Program CPIs create accounts, allocate space, assign owners, and transfer lamports. Typical flow for a new program-owned account: rent-exempt minimum from Rent::get(), then create_account (or allocate/assign patterns) with the payer as signer. PDA-funded or PDA-sourced SOL movement needs invoke_signed. See CPI to the System Program.
SPL Token CPIs mint, transfer, burn, approve, and related operations. You never rewrite token account bytes yourself; you build an instruction with spl_token::instruction (or Token-2022 equivalents) and invoke / invoke_signed with the correct token program id, mint, authorities, and token accounts. Validate mint matches, decimals (raw amounts), freeze state, and Token vs Token-2022 program id. PDA mint or freeze authorities use invoke_signed. See CPI to SPL Token.
Reentrancy and safety
Unlike environments with a default global mutex, Solana does not give you a free reentrancy lock. Another program you CPI into may CPI back into you (transfer hooks, callbacks, malicious user-supplied program ids). Treat external CPI as untrusted side effects.
Practical rules:
- Checks → effects → interactions: validate, update and serialize your state, then CPI out.
- Idempotent flags: once a withdraw or settle is marked done, reentry cannot double-pay.
- Allowlist callees when the program id is not a fixed constant (routers, plugins).
- Do not assume single entry if Token-2022 hooks or partner programs can call you mid-flow.
Depth limits bound the stack; they do not make unsafe ordering safe. See Reentrancy & Safety.
Advanced Considerations & Applications
Composability is the product of CPIs done carefully: one transaction can validate a user, update a ledger, swap on a DEX, and deposit into a vault, all or nothing.
| Pattern | What it does | CPI shape | Watch out for |
|---|---|---|---|
| Escrow / vault | Program custodies assets under a PDA | invoke_signed to System or Token | Seeds, bumps, authority fields |
| Router / aggregator | One entry instruction fans out to partners | Multiple invokes, allowlisted program ids | Depth, CU, slippage, remaining accounts |
| Adapter | Normalize a foreign protocol behind your API | Thin CPI wrapper + your state | Partner upgrades and account layout drift |
| Client-composed multi-ix | No nested CPI; several top-level instructions | Zero or few CPIs in your program | Weaker single-program orchestration; still atomic as one tx |
Remaining accounts let clients pass extensible account tails (extra pools, oracles, memo programs) without hard-coding every slot in your Accounts struct. Document the layout per version and validate each key and owner before CPI.
Address Lookup Tables (ALTs) address transaction size, not CPI depth. They help when account lists explode; they do not raise the depth-4 cap.
Anchor 0.32.1 generates typed CPI helpers (CpiContext, invoke_signed wrappers, anchor_spl) so metas and discriminators stay aligned with IDLs. The runtime model is unchanged: same privileges, same depth, same need for CEI ordering. Native Rust with solana_program is the same syscall surface with more manual wiring.
For architecture reviews, ask: which program owns each account we touch; which signers (wallet or PDA) authorize each nested call; whether callee program ids are fixed or allowlisted; whether state is finalized before external CPI; whether the call tree fits depth and CU under realistic simulation on Agave 4.1.1 / CLI 3.0.10 tooling.
Common Misconceptions
- "CPI is just an HTTP-style internal API call." It is a runtime-mediated nested instruction under the same locks, signatures, and CU budget, with hard account and depth rules.
- "My program can write token balances if I pass the token account." Only the Token Program owns those bytes. You must CPI with a valid transfer/mint/burn instruction and authority.
- "
invoke_signedlets me forge any signature." It only authorizes PDAs for your program id with the seeds you supply. It does not create wallet signatures. - "If I know a pubkey, the CPI can load it." Undeclared accounts are unavailable. Clients must include every account the full call tree needs.
- "Nested programs each get their own compute budget." All nested work draws from the transaction budget.
- "Reentrancy is an EVM-only problem." Hooks and callbacks make it real on Solana; order state updates before external CPI and allowlist untrusted targets.
- "Deeper composition is always better." Depth 4 and CU ceilings punish deep trees; flatter designs and client-side multi-instruction txs are often healthier.
- "Marking everything writable is safer." It expands the blast radius if a callee or bug mutates more than intended. Use least privilege.
FAQs
What is a CPI in one sentence?
A cross-program invocation is your program executing another program's instruction mid-handler, inside the same atomic transaction, with a declared account list and shared compute budget.
Why can't I just edit another program's account data?
The runtime enforces owner-writes: only the owner program may mutate an account's data. CPI is how you ask that owner to apply a supported instruction.
What must be true of accounts I pass to a CPI?
They must already appear in the outer transaction, match the callee's expected order and roles, and carry signer/writable privileges that the outer message (plus any PDA signs) legitimately allow.
When do I use invoke vs invoke_signed?
Use invoke when all required signers already signed the transaction. Use invoke_signed when a PDA derived from your program must act as a signer (vaults, mint authorities, program-owned escrows).
What is the CPI depth limit?
Maximum nested CPI depth is 4 on current Solana (Agave 4.1.1). Plan call graphs so Token, hooks, and partner programs do not exhaust the stack.
Do CPIs get separate compute unit budgets?
No. Nested execution consumes the same transaction CU pool. Simulate multi-CPI paths and set Compute Budget instructions when needed.
How do System Program CPIs fit everyday program work?
They create and fund accounts and move SOL. Rent-exempt create_account and transfer are the common building blocks; PDA sources need invoke_signed.
How do SPL Token CPIs fit everyday program work?
Mint, transfer, burn, and authority operations go through Token or Token-2022 via instruction builders and CPI. Validate program id, mint, amounts, and authorities before calling.
What is the most common CPI security mistake?
Updating balances or flags after an external CPI (or calling unallowlisted program ids), which enables reentrancy or malicious callees to observe inconsistent state.
Can a callee call back into my program?
Yes, within depth limits. Design handlers to be safe if re-entered: finalize state first, use settled flags, and treat hooks as first-class paths.
Does Anchor remove the need to understand CPIs?
No. Anchor 0.32.1 improves typing and ergonomics; you still choose invoke vs signed seeds, pass accounts correctly, respect depth/CU, and order state updates safely.
How should clients prepare for a CPI-heavy instruction?
Include every account the full nested path needs (including programs and sysvars), set an adequate compute budget, and prefer ALTs when account count inflates message size.
Is multi-instruction composition without CPI a valid alternative?
Yes. Several top-level instructions in one transaction stay atomic without nested depth. Use that when you do not need program-enforced intermediate logic between steps.
Where should I go next after this page?
Start with CPI Basics and invoke vs invoke_signed, then System/Token targets, depth, reentrancy, and Composability Patterns as you design real flows.
Related
- CPI Basics - first
invokepatterns and Instruction construction - invoke vs invoke_signed - wallet signatures vs PDA seed signing
- CPI to the System Program - create accounts and transfer SOL
- CPI to SPL Token - mint, transfer, and burn via the Token program
- CPI Depth & Limits - depth cap, CU pressure, flattening designs
- Composability Patterns - routers, adapters, and multi-program workflows
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.