Guide
Solana cross-program invocation (CPI) explained
A Jupiter swap that routes through three liquidity pools, a Metaplex mint that
creates metadata and transfers a token in one click, or a Garden Dice payout that
moves SOL from a program vault to your wallet — all rely on the same mechanism:
cross-program invocation (CPI). On Solana, deployed programs are not
isolated silos. Any program executing in a transaction can call another program's
instruction, and the entire call chain succeeds or fails atomically.
This guide explains how CPI works under the hood, the difference between
invoke and invoke_signed, how account permissions propagate,
depth limits, and the security mistakes auditors hunt for — building on the
account model and
program derived addresses (PDAs).
Why CPI exists: composability without middleware
Ethereum popularized "money Legos" — one contract calling another inside a single
transaction. Solana achieves the same outcome differently. There is no EVM-style
call opcode into arbitrary contract storage. Instead, every piece of
state lives in an account, and programs mutate only accounts they
own or that signers authorize. CPI is the syscall that lets program A ask the
runtime to execute an instruction on program B, passing along a curated account
list.
That design has two consequences developers feel immediately:
- Atomic bundles — swap + stake + record ledger entry either all land, or none do. No half-settled DeFi state from a reverted inner call.
- Explicit account wiring — unlike Solidity interfaces that hide storage layout, Solana forces you to pass every account the callee will touch. Clients and programs must agree on account order; mismatches fail fast with "missing required signature" or "unknown account" errors.
CPI is how the SPL Token program gets invoked millions of times per day without every dApp reimplementing transfer logic. Your custom program becomes an orchestrator; battle-tested system programs do the token math.
The CPI call stack in one transaction
Picture a transaction as a tree of instruction executions:
- The outer instruction is what the user (or bot) submitted — e.g. "place bet" on a game program.
- That program's handler may CPI into the System program to move lamports, or into the Token program to transfer SPL balances.
- Those callees may themselves CPI further — though depth is capped (currently four levels including the outer call), which prevents runaway recursion and stack blowups.
Block explorers show this as inner instructions nested under the top-level instruction. When you read a transaction on Solscan, expanding inner instructions reveals the true call graph — invaluable when a swap fails deep inside a router program.
invoke vs invoke_signed
In Rust program code (Anchor or raw solana-program), two entry points
matter:
invoke — forwarding existing signatures
Use invoke when every signer the callee needs is already a
signer on the outer transaction. Example: the user signed the tx; your program CPIs
to Token transfer with the user's wallet marked is_signer.
The runtime forwards that signature privilege inward. No extra keys required.
invoke_signed — PDA and program authority
Use invoke_signed when the callee needs a signature from an address
that has no private key — typically a
PDA
owned by your program. You pass the seed slices and bump that prove the PDA belongs
to the calling program; the runtime elevates that account to signer status for the
inner instruction only.
Classic pattern: an escrow PDA holds USDC. On settlement, your program calls
invoke_signed to Token transfer with the PDA as authority,
moving funds to the winner. The user never signs the inner transfer directly; they
signed the outer "settle" instruction that authorized your code to act.
Mixing these up is a top audit finding. Calling invoke when the inner
instruction needs a PDA signer fails at runtime. Calling invoke_signed
with attacker-controlled seeds can let a malicious program impersonate your vault.
Account metadata: signers and writable flags
Each account in a CPI is wrapped in an AccountInfo with metadata the
runtime enforces:
is_signer— account authorized this action. Outer tx signers stay signers in inner CPIs if you pass them through correctly.is_writable— account lamports or data may change. Callees reject writes to read-only accounts.- Owner program — only the owner program (or the runtime for system-owned accounts) may modify account data. CPI does not bypass ownership rules.
A subtle rule: the calling program can only pass accounts that were present in the outer instruction's account list (or accounts already loaded in the transaction). You cannot smuggle in new addresses mid-CPI. That is why complex versioned transactions with address lookup tables exist — Jupiter-style routers need dozens of pool accounts in one tx.
When building clients, mirror the callee's expected account order exactly. Anchor's
#[derive(Accounts)] and IDL files document this; hand-rolled clients
often fail because account #4 was writable in the IDL but passed read-only.
Worked example: SPL token transfer via CPI
Suppose your program releases a reward after a verified game outcome. The flow:
- User signs a transaction with accounts: game state, reward vault token account (PDA-owned), user token account, token program, your program.
- Your handler validates the game result on-chain or via a trusted oracle account.
- You build a Token program
Transferinstruction targeting the vault PDA as authority. - You call
invoke_signedwith seeds[b"vault", user_pubkey]and the stored bump. - Token program debits vault, credits user — all inside the same atomic tx as your state update marking the reward claimed.
For a deeper look at token account layout and associated token accounts, see
SPL token accounts explained.
Payment verification patterns for games and shops follow the same CPI shape in reverse
(user to vault) with the user's wallet as signer via invoke.
CPI depth, compute, and failure modes
Solana caps CPI nesting at four levels. Deep router paths must flatten
or split across transactions. Each CPI also consumes
compute units (CU) from the transaction's budget (~1.4M default,
extendable with
priority fees).
Heavy inner loops — iterating pools, deserializing large account data — can hit
Computational budget exceeded even when logic is correct.
Common runtime errors and what they usually mean:
- Missing required signature — inner instruction expected a signer
you did not mark, or you used
invokeinstead ofinvoke_signedfor a PDA authority. - Cross-program invocation with unauthorized signer — seeds in
invoke_signeddo not match the PDA address passed in accounts. - Account not writable — metadata mismatch; callee needs to mutate an account flagged read-only.
- Instruction nesting depth exceeded — more than four CPI layers; refactor or split the tx.
Always simulate transactions in dev before mainnet. Simulation surfaces CPI failures with program logs that mainnet users would otherwise only see as an opaque "transaction failed."
Security pitfalls auditors focus on
- Arbitrary CPI / account substitution — accepting a Token program
ID or mint account from untrusted input lets attackers pass a fake program that
no-ops transfers. Pin program IDs to known constants (
token::ID). - Signer privilege escalation — reusing account slices from user
input without re-deriving PDA addresses from canonical seeds. Validate
find_program_addresson-chain beforeinvoke_signed. - Reentrancy (different from EVM) — Solana's single-threaded per-account locking reduces classic reentrancy, but cross-program invocations before state is finalized can still double-spend escrows if you credit users before debiting vaults. Follow checks-effects-interactions ordering inside your handler, not just around CPIs.
- Closing accounts via CPI — draining lamports from a PDA then forgetting to zero data leaves stale state. Pair closes with discriminator resets.
- Logging sensitive seeds — debug prints of seed material in on-chain logs are public forever. Use off-chain simulation for seed debugging.
New programs should go through review before holding user funds. The program deployment guide covers upgrade authority and devnet-first discipline — CPI bugs are far cheaper to fix before mainnet vaults fill up.
Client-side composition vs on-chain CPI
Not every multi-step action needs on-chain CPI. Wallets can submit transactions with multiple top-level instructions — e.g. create associated token account, then transfer, then call your program — without your program CPIing at all. Choose on-chain CPI when:
- Intermediate state must not be observable (atomic swap + stake).
- You need PDA authority the user cannot wield directly.
- You want a single instruction interface hiding complexity from integrators.
Prefer client-side instruction bundling when steps are independent and users should approve each leg explicitly. The UX and security trade-off is product-specific; CPI is a sharp tool, not a default.
Key takeaways
- CPI lets Solana programs call other programs inside one atomic transaction.
- Use
invoketo forward user signatures; useinvoke_signedwhen a PDA must act as authority. - Account signer and writable flags must match callee expectations; all accounts must be declared in the outer transaction.
- CPI depth is capped at four levels; compute budgets limit heavy routers.
- Pin program IDs, validate PDAs on-chain, and read inner instructions on explorers when debugging failed composable flows.
Related reading
- Solana account model — owners, lamports, and why CPI passes explicit account lists
- Program derived addresses (PDAs) — seeds, bumps, and invoke_signed signing
- SPL token accounts — the most common CPI target for transfers and mints
- Solana program deployment — shipping BPF programs that use CPI safely