A Cross-Program Invocation lets one Solana program call another inside the same transaction. That enables atomic token transfers, swaps, vault actions, and protocol composition. It also carries the caller's account privileges and remaining compute into code controlled by a different program.
This guide gives you nine checks for reviewing a CPI before mainnet. It covers target identity, instruction data, account privileges, PDA signers, stack depth, compute, errors, state synchronization, and adversarial tests.
Read the Solana transaction guide first if instructions, accounts, and atomic execution are unfamiliar.
Checks 1 and 2: lock the target and interface
Check 1: validate the program you intend to call
A CPI is an instruction with three inputs: a target program ID, ordered account metadata, and instruction data. Treat all three as an integration contract.
Prefer a typed Anchor program account or a generated CPI interface that fixes the expected program ID. If you accept an unchecked executable account, compare its key to an allowlisted ID before invoking it. An attacker-controlled program can accept the same account list and perform different work.
#[derive(Accounts)]
pub struct TransferTokens<'info> {
pub token_program: Interface<'info, TokenInterface>,
// Other validated accounts follow.
}Token interfaces can intentionally support more than one approved program. That is different from accepting any executable account.
Check 2: generate or verify the instruction interface
Anchor helpers and declare_program!() can generate CPI modules from a target program's IDL. This reduces hand-built discriminator, serialization, and account-order mistakes, but the IDL must match the deployed program version.
A manual native CPI needs explicit checks for:
- target program ID
- instruction discriminator or opcode
- argument encoding and integer widths
- ordered account metas
- signer and writable flags
- optional and remaining-account rules
Pin the interface artifact in the repository. A dependency update should produce a visible diff before it changes onchain calls.
Checks 3 and 4: reason about privileges and state
Check 3: pass only privileges the callee needs
Signer and writable privileges extend from caller to callee. The callee cannot escalate an account beyond the privileges the caller received, but it can use every privilege you pass.
Marking an account writable is therefore an authority decision, not a performance detail. Review each meta against the callee's instruction contract. Keep unrelated writable accounts out of remaining_accounts, and reject duplicate roles where two mutable accounts must differ.
Anchor account types validate useful boundaries, but your caller still owns its business checks. Confirm mint, token authority, market, vault, owner, and stored relationships before the CPI.
Check 4: understand when account data changes become visible
The caller and callee share the same transaction and compute budget. A successful callee can modify writable accounts, and the caller may need the updated data afterward.
With Anchor accounts, call reload() before reading state that the CPI changed:
token_interface::transfer_checked(cpi_ctx, amount, decimals)?;
ctx.accounts.destination.reload()?;
require!(
ctx.accounts.destination.amount >= expected_minimum,
ErrorCode::UnexpectedDestinationBalance
);Do not assume your in-memory deserialized value refreshed itself. When a downstream invariant depends on the new value, reload and validate it explicitly.
Checks 5 and 6: constrain signing and execution depth
Check 5: build PDA signer seeds from validated state
Choose invoke when the original transaction already supplied every required signer. Choose invoke_signed, or Anchor's signer context, when your program must authorize the callee as one of its PDAs.
let market_key = ctx.accounts.market.key();
let signer_seeds: &[&[&[u8]]] = &[&[
b"vault",
market_key.as_ref(),
&[ctx.bumps.vault_authority],
]];
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
transfer_accounts,
signer_seeds,
);
token_interface::transfer_checked(cpi_ctx, amount, decimals)?;The runtime derives the PDA using the caller's program ID and supplied seeds. It then adds the matching address to the valid signer set for that invocation. Seeds are not secrets. Safety comes from correct derivation, account constraints, and business authorization.
The PDA production guide covers seed and lifecycle design.
Check 6: map the complete invocation graph
Current Solana documentation lists a maximum instruction stack depth of 5 under the baseline limit. The top-level instruction occupies the first level, leaving room for four nested CPI levels. The same documentation notes a higher limit tied to SIMD-0268, so verify activation on the target cluster before designing for it.
Indirect reentrancy is not allowed: a path such as A to B to A fails. Direct self-recursion is allowed within the stack limit.
Draw the worst-case graph, including token hooks, adapters, routers, and callbacks. A CPI hidden inside a dependency still consumes a stack level.
Checks 7 and 8: budget work and preserve errors
Check 7: simulate the full path under one compute budget
The callee consumes the caller's remaining compute units. Current Solana documentation lists a baseline CPI invocation cost of 1,000 CUs, before the callee's own work and data handling.
Simulate the final instruction graph and record:
- total and per-program units consumed
- CPI depth and inner instructions
- account count and writable set
- log truncation
- return data
- serialized transaction size
Request a measured compute limit and priority fee from the client. Raising the limit does not fix an unbounded loop, redundant CPI, or oversized account set. Use the compute optimization guide for the full measurement loop.
Check 8: preserve the callee's failure evidence
A CPI failure returns an error and aborts the transaction unless the caller handles the result. Do not replace every downstream error with one generic CpiFailed code. Preserve enough context to identify the target program, inner instruction index, custom error, and relevant account family.
Production traces should capture the signature, slot, target program, caller instruction, inner instructions, logs, units consumed, and RPC host. RPC Edge belongs at this delivery and observation boundary. It cannot fix invalid account metas or signer seeds.
The five-layer debugging trace shows how to follow a failure from the outer instruction into nested program logs and state.
Check 9: prove the boundary rejects hostile inputs
Happy-path integration tests are not enough. Your CPI suite should reject:
- an executable but unapproved target program
- a valid program with the wrong instruction data
- swapped accounts with compatible low-level types
- a read-only account where the callee needs write access
- a writable account that should never reach the callee
- a PDA signer with wrong seed order or bump
- a mismatched mint, authority, market, or vault relationship
- duplicate accounts in roles that must differ
- an invocation graph that exceeds the active stack limit
- a valid path with insufficient compute
Also assert atomic rollback. If the callee fails after the caller changed state, no partial transaction state should remain.
The 9-Check CPI Production Gate
Before enabling a cross-program path:
- allowlist and validate the target program ID
- pin the deployed interface, discriminator, arguments, and account order
- pass only required signer and writable privileges
- validate ownership plus every business relationship before invoking
- build PDA signer seeds from constrained accounts and canonical bumps
- map nested calls, reentrancy, and the cluster's active stack limit
- simulate the complete path under a measured compute budget
- reload any account data read after the callee changes it
- preserve inner failure evidence and run hostile-input tests
The durable artifact is a CPI manifest. Record the target program, deployed version or IDL hash, instruction, account contract, signer source, worst-case depth, measured compute, return-data use, and failure map.
FAQ, sources, and next step
Is a CPI a separate transaction?
No. It executes inside the current transaction. If any unhandled instruction fails, the transaction's state changes roll back atomically.
Can a callee make my read-only account writable?
No. A callee cannot escalate signer or writable privileges beyond what the caller passed.
When do I need `invoke_signed`?
It is required when the caller program needs one of its PDAs to satisfy a signer requirement in the callee instruction.
Are four nested CPIs always available?
The baseline stack depth of 5 includes the top-level instruction, leaving four nested levels. Verify the active cluster limit and count every dependency call.
Primary sources
Continue with the Solana program security guide for broader trust-boundary review. When an integration crosses multiple onchain programs, SDKs, and RPC paths, book a Builderz architecture review.



