Sign in

Cross-Program Invocation (CPI)

Solana programs are not isolated. A program can call another program in the middle of its own execution, pass it accounts, and observe the result before continuing. This is how a swap program moves SPL tokens, how a lending program reads price oracles, how any nontrivial protocol composes services. The mechanism is called Cross-Program Invocation. The runtime gives you two syscalls, invoke and invoke_signed, plus a strict set of rules about what authority crosses with the call. Once you understand those rules, the rest of Solana's ecosystem opens up to your programs.

Programs need to call programs

The programs you've written so far operate on accounts owned by your own program. You can read them, mutate them, initialize new ones, charge them rent. What you can't do, alone, is move SPL tokens, mint NFTs, place an order on a DEX, or do anything else that requires touching state owned by a different program.

That's deliberate. Solana's security model is that each account has exactly one owner program, and only that owner can mutate the account's data. So if you want to move tokens out of a vault, you can't just write zeros over the source balance and write a higher number into the destination. The tokens live in accounts owned by the Token Program. Only the Token Program can mutate them.

The way you make changes to accounts you don't own is to ask the program that owns them, politely, to do it on your behalf. You construct a request, you hand it to the runtime, and the runtime hands it to the owning program. If your request is well-formed and you have the right authority, the owning program performs the mutation and returns success. That's a CPI.

If you've worked with microservices, the pattern is familiar: one service calls another, forwarding the caller's auth credentials. The called service trusts those credentials because the platform attests to them. CPI does the same thing, where the platform is the runtime and the credentials are signer flags on accounts.

The shape of a CPI call

A CPI happens in four phases. Your handler builds an Instruction describing what it wants the called program to do. It calls the invoke syscall, passing the instruction along with references to the accounts the called program will need. The runtime suspends your program, switches execution to the called program, and runs it with those accounts. When the called program returns, control comes back to your handler with the state changes already applied.

The shape of a CPI call 1. Your handler builds an Instruction let ix = Instruction { program_id: token_program.key(), accounts: vec![ /* AccountMetas */ ], data: instruction_data, }; 2. Pass it to the invoke syscall invoke(&ix, &account_infos)?; the runtime suspends your program and switches execution 3. The called program runs its entry point sees the accounts you passed, with their signer and writable flags carried over from your frame it can read or mutate any account it received, subject to those flags 4. Control returns to your handler any state changes the called program made are now visible if it returned an error, your handler propagates that error A CPI is a function call across program boundaries, with strict rules about what crosses with you.

The Instruction struct should look familiar. It's the same shape as the instructions on the outside of the chain, the ones a wallet builds and signs. The CPI version is built inside another program and sent through the syscall instead of arriving from the network, but the runtime treats them the same way once they're being executed. Same fields, same semantics.

The depth is bounded. A transaction can invoke up to four levels deep: your handler is depth one, a CPI from it is depth two, a CPI from that is depth three, and one more layer is allowed. Beyond that, the runtime rejects the call. In practice this is plenty for any composition pattern, but it's worth knowing the limit exists.

Privileges propagate, and they never grow

This is the most important conceptual point in the lecture. The rules about what authority a CPI carries with it are the entire security model of Solana's composition layer.

When your handler runs, each account in its frame carries flags: was this account a signer in the transaction, and is it marked writable. When you CPI to another program, those flags travel through. An account that signed your transaction is still a signer in the called program's frame. An account marked writable in your handler is still writable in the called program's frame, if you pass it as writable.

Privileges propagate, but never grow Your handler's frame (program A) account signer? writable? alice (Signer) yes yes vault (mut Account) no yes mint (Account) no no these are the flags your accounts carry in your handler CPI to Token Program The called program sees (program B) account signer? writable? alice yes yes vault no yes mint no no the same flags propagate through. nothing was upgraded. The rules → Signers from your frame stay signers in the called program's frame. → Writability is preserved, or you can drop it (pass writable as readonly). → A non-signer can never become a signer mid-call. Privileges never grow. → Exception: your program can sign as a PDA it controls, via invoke_signed. A program can only delegate authority it already has. This is the entire security model of CPI.

What you cannot do is upgrade authority. If an account arrived in your handler as readonly, you cannot pass it to a CPI as writable. The runtime checks. If an account arrived as a non-signer, you cannot pretend it signed on the way down. The runtime checks. The principle: a program can only delegate authority it already has.

You can drop authority. If your handler can write to an account, you can pass it to a CPI as readonly, telling the called program "you can look but not touch." This is a useful capability, since it lets you call programs that need to read state without granting them write access they don't need.

There is one exception to the "no new signers" rule, and it's the central mechanism that makes Solana programs useful. Your program can sign as a PDA it controls, by calling invoke_signed and passing the PDA's seeds. The runtime re-derives the PDA, confirms it belongs to your program, and treats it as a signer in the called program's frame. This is how a vault PDA authorizes a token transfer out of its own token account: the program proves it knows the seeds, the runtime accepts that as the PDA's signature.

invoke and invoke_signed

The runtime exposes two syscalls. invoke forwards existing signers downstream. invoke_signed does the same, plus it lets your program sign as one or more PDAs.

Two ways to call another program invoke syscall: invoke(&ix, &accounts)?; what propagates: signers that signed your tx are still signers downstream use when: a human or wallet signed, and that signature is enough example: Alice signs your handler. You CPI to Token Program to move tokens from Alice's account. Alice is still the signer in the inner call. forwarding existing signatures invoke_signed syscall: invoke_signed(&ix, &accounts, signer_seeds)?; what propagates: existing signers, PLUS your program signs as a PDA use when: an account is owned by a PDA your program controls, and you need to authorize on its behalf example: a vault PDA owns a token account. you CPI to transfer from it. pass seeds; runtime re-derives, accepts as signer. programs signing on their own behalf Both syscalls forward signers. invoke_signed adds one capability: your program acting as a PDA.

The signer seeds passed to invoke_signed are an array of seed arrays. Each inner array re-derives one PDA. So you can sign on behalf of multiple PDAs in a single call, by passing multiple sets of seeds. The runtime walks each one, derives the address, and adds that address to the signer set for the inner call. If any of the derived addresses doesn't match an account in the call, the CPI fails.

A concrete shape for the seeds, with the bump appended at the end:

rust
let bump = ctx.accounts.vault.bump;
let mint_key = ctx.accounts.mint.key();
let seeds: &[&[u8]] = &[b"vault", mint_key.as_ref(), &[bump]];
let signer_seeds: &[&[&[u8]]] = &[seeds];

invoke_signed(&ix, &account_infos, signer_seeds)?;

The double-reference shape is awkward to read at first, but it makes sense once you see what it represents: a list of seed sets, where each seed set is itself a list of byte slices. One set per PDA you're signing for.

Anchor's CpiContext

Writing raw Instruction structs and calling invoke directly is verbose and easy to get wrong. Anchor provides a typed wrapper called CpiContext that handles the plumbing for known programs, and the SPL Token bindings in anchor_spl give you typed structs for every Token Program instruction.

A token transfer through Anchor looks like this:

rust
use anchor_spl::token::{self, Transfer};

let cpi_accounts = Transfer {
    from: ctx.accounts.source.to_account_info(),
    to: ctx.accounts.destination.to_account_info(),
    authority: ctx.accounts.authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(
    ctx.accounts.token_program.to_account_info(),
    cpi_accounts,
);
token::transfer(cpi_ctx, amount)?;

The Transfer struct names every account the Token Program's transfer instruction expects, with the correct field names. CpiContext::new packages the program reference together with the accounts. token::transfer builds the right Instruction, calls invoke, and returns the result. You wrote no AccountMeta lists, no instruction data buffers, no syscall calls. The macro layer handled all of it.

For invoke_signed, the equivalent constructor is CpiContext::new_with_signer, which takes the same arguments plus a signer seeds slice. Everything else is identical. The signature on token::transfer is the same. Only the context changes.

This is the form you'll write in real code. The raw syscall form exists so you can drop down to it when no typed wrapper exists for the program you're calling, but for any of the popular SPL programs, the typed wrappers cover the common operations cleanly.

Composition is the payoff

The reason CPI matters is not the syscall mechanics. It's what composition unlocks once the mechanics are in place.

A swap program can be 200 lines of code because the actual token movement lives in the SPL Token Program, and the swap program just CPIs to it. A lending protocol can pull oracle prices by CPI-ing into Pyth or Switchboard, instead of operating its own price feeds. A vault aggregator can route deposits across half a dozen yield strategies, calling into each one through CPI, without knowing the internals of any of them. The whole ecosystem assembles like this.

The privilege rules are what make composition safe. Because authority never grows through a CPI, a program you call cannot do anything with the accounts you gave it that you couldn't have done yourself. You can trust the called program with whatever signers and writable accounts you forwarded, no more. That property is enforced by the runtime rather than by convention, which is why the Solana ecosystem can compose contracts written by mutually distrusting teams. You don't have to audit Token Program every time you transfer. You just need to know what authority you're handing it. And the runtime guarantees that's all the authority it gets.