Create your own
Lesson illustration

Cross-Program Invocation (CPI) Explained

Hello. In the previous lessons, you defined Anchor account contexts, used constraints such as signer, mut, has_one, and init, and wrote TypeScript integration tests around an instruction’s observable state changes.

One important detail was hidden behind init: creating an account is not an operation your program performs by directly editing arbitrary state. Anchor invokes the System Program on your program’s behalf. That nested program call is a Cross-Program Invocation, or CPI. This lesson makes that mechanism explicit: what a CPI is, why Solana needs it, what information it carries, and how privileges and execution behave.


A call within a call

A Cross-Program Invocation occurs when a currently executing Solana program invokes an instruction implemented by another Solana program.

At the top level, a client sends a transaction instruction to your program. While handling that instruction, your program can construct a second instruction and ask the Solana runtime to execute it against a target program. The target completes its work, returns control to your handler, and your handler may continue.

A client invokes a primary Solana program; during execution, that program invokes an instruction on another program, waits for it to complete, and then resumes its own handler.

This is not a separate transaction and does not involve another wallet confirmation or a network round trip. It is a synchronous nested call within the same transaction. If the nested invocation fails and your program propagates that error, the whole transaction fails and its state changes are rolled back.

The key consequence is composability. Rather than each application reimplementing token logic, account creation, metadata management, or associated-token-account creation, it can invoke the well-audited program responsible for that job.

Common targets include:

  • The System Program for creating accounts and transferring SOL.
  • The SPL Token Program for minting and transferring tokens.
  • The Associated Token Account Program for creating standard token accounts.
  • The Metaplex Token Metadata Program for token and NFT metadata.
  • Another custom program whose instruction interface your application deliberately uses.

A CPI is therefore best understood as an on-chain instruction call. It has the same basic shape as an instruction your TypeScript client constructs, but the caller is now a program rather than an off-chain client.

Cross Program Invocation

Read the Solana documentation’s compact conceptual overview. It establishes CPIs as the mechanism behind program composability, then introduces the privilege and signing rules that make this safe.

In the opening overview, read the definition to establish the core idea. Then, in “Key facts,” focus on privilege extension and the shared compute-budget point. Finally, read “invoke vs invoke_signed” through the comparison. Do not try to memorize PDA signing yet; the distinction will become practical in the PDA module.


Why program ownership makes CPIs necessary

Solana programs are not general-purpose processes with unrestricted access to global state. Accounts hold state, and every account has an owner program. That ownership matters because the owner determines which program may modify the account’s data.

For example, an ordinary wallet is normally owned by the System Program. Your custom Anchor program may receive that wallet account in its instruction context, inspect it, and use the user’s signature as authorization. But it cannot simply subtract lamports from that system-owned account as though it owned it. To make a conventional SOL transfer, it calls the System Program’s transfer instruction through a CPI.

Similarly:

  • A program that wants to mint or transfer an SPL token invokes the Token Program.
  • A marketplace program that wants to create an associated token account invokes the Associated Token Account Program.
  • A token-launch program that wants to create on-chain metadata invokes the Metaplex metadata program.

The target program remains the authority over its own rules. A CPI does not bypass validation; it delegates work to the program that defines and enforces the relevant operation.

This is a useful distinction:

SituationWhat happens
A TypeScript client calls an SPL Token instruction directlyThe client constructs and submits a top-level Token Program instruction.
Your program calls an SPL Token instructionYour program constructs a nested instruction and invokes the Token Program through a CPI.
Your program changes its own data accountIt writes state it owns, subject to runtime rules and your own validation.
Your program needs another program’s operationIt invokes that program’s documented instruction interface.

A client can include several top-level instructions in one transaction, but that is not always an adequate substitute for a CPI. A CPI lets the program decide whether and how to make the target call based on validated on-chain state. For instance, a vault program may check withdrawal conditions and then request the System Program to transfer SOL as part of that same controlled operation.


The three ingredients of every CPI

A CPI needs the same three components as any Solana instruction:

  1. Target program ID
    Which program should execute the instruction? For a SOL transfer, this is the System Program.

  2. Required accounts
    Which accounts will the target program read, modify, or treat as authorities? A System Program transfer needs a source and destination account. Other instructions may require a mint, token accounts, an authority, a metadata account, and several program accounts.

  3. Instruction data
    Which target instruction is being requested, and with what arguments? In a SOL transfer, the instruction data identifies transfer and contains the number of lamports.

The receiving program’s documentation is the contract for all three items. You do not “call a program” generically; you invoke one specific instruction, with exactly the accounts and encoded data that instruction expects.

The Anchor CPI guide demonstrates this with a System Program SOL transfer:

cpi - Cross Program Invocation

Read Anchor’s SOL-transfer example as the concrete model for the abstract three-part structure: target program, accounts, and instruction arguments.

Start in “Cross Program Invocations” with the opening definition. Then read the sol_transfer example and its SolTransfer account context. In “Example Explanation,” focus on the three requirements. Finally, inspect the CpiContext::new and transfer lines: identify which values provide the target program and which provide the source and destination accounts.

Here is the essential Anchor pattern, with names chosen to make the roles clear:

use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer};

pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> {
    let cpi_context = CpiContext::new(
        ctx.accounts.system_program.to_account_info(),
        Transfer {
            from: ctx.accounts.sender.to_account_info(),
            to: ctx.accounts.recipient.to_account_info(),
        },
    );

    transfer(cpi_context, amount)?;
    Ok(())
}

#[derive(Accounts)]
pub struct SendSol<'info> {
    #[account(mut)]
    pub sender: Signer<'info>,

    #[account(mut)]
    pub recipient: SystemAccount<'info>,

    pub system_program: Program<'info, System>,
}

The handler send_sol is your program’s instruction. It does not itself modify the sender’s lamport balance. Instead:

  • system_program identifies the program that will execute the nested operation.
  • Transfer { from, to } supplies the accounts expected by the System Program’s transfer instruction.
  • amount becomes instruction data for that transfer.
  • CpiContext::new(...) packages the target program account and target instruction accounts.
  • Anchor’s transfer(...) helper constructs and invokes the appropriate System Program instruction.

Anchor hides much of the low-level instruction encoding. In native Solana Rust, you would explicitly build an Instruction and call invoke. Conceptually, both approaches are the same.

Solana Bytes - Cross Program Invocation

Watch “Solana Bytes - Cross Program Invocation” from the Solana channel for a brief visual walkthrough of the same idea in native Rust and Anchor.

Watch the definition for the basic intuition. Continue with the native call to see an instruction constructed and dispatched with invoke; notice that the client creates only the instruction for the custom program. Then watch the Anchor context for the role of CpiContext and the final connection to account ownership.


Privileges: a CPI cannot manufacture authority

The most important security property of a CPI is that it cannot escalate account privileges.

When a client constructs the top-level instruction, it marks each account according to its required access:

  • Signer means the account supplied a valid transaction signature.
  • Writable means the instruction may modify the account.
  • An account without either flag is read-only and non-signing.

When your program invokes another program, it may pass those accounts along, but the target program cannot receive more authority than the outer instruction granted. In particular:

  • A read-only account cannot become writable inside the CPI.
  • A non-signer cannot become a signer merely because your program asks for it.
  • A user’s top-level signature can be used by the target program only if that signed account is passed into the CPI with signer privilege.
  • Your program cannot sign as an arbitrary wallet.

In the SOL-transfer example, sender is declared as Signer<'info> and mutable. The client’s transaction must therefore include the sender’s signature. When the CPI calls the System Program, that already-established signer privilege is extended to the nested invocation. The System Program can accept the transfer because the source account is both the proper account type and an authorized signer.

This makes CPIs powerful without making them an escape hatch from Solana’s authority model.

PDA signing is a deliberate exception

Programs do sometimes need to authorize transfers or other actions without a human wallet signing each time. For that purpose, Solana has Program Derived Addresses (PDAs). A PDA has no private key, but the program that derived it can ask the runtime to treat it as a signer for a CPI by providing the original seeds and bump.

At the lower level, the two CPI APIs are:

FunctionUse when
invokeAll required signers are already present from the transaction.
invoke_signedThe calling program must sign for one of its PDAs.

In Anchor, PDA signer seeds are attached to a CpiContext with .with_signer(...). We will treat that mechanism carefully after learning how PDAs are derived. For now, the key point is that PDA signing is narrowly scoped: only the program associated with that PDA derivation can establish it.


Execution, logs, and resource limits

A CPI is part of a nested execution stack. A simplified transaction trace might look like this:

  1. The client invokes your vault_program instruction.
  2. vault_program validates its accounts and business conditions.
  3. vault_program invokes a System Program transfer through a CPI.
  4. The System Program validates the transfer and changes balances if permitted.
  5. Control returns to vault_program.
  6. vault_program either completes successfully or returns an error.

Blockchain explorers usually show the nested invocation as an inner instruction. If the outer instruction is numbered 1, a CPI performed during it may appear as 1.1. Logs also make this visible: you see your program invoked, then the target program invoked, then a success or error result as control returns.

There are three practical consequences:

  • Synchronous result: your code waits for the CPI before continuing. The ? after an Anchor helper such as transfer(cpi_context, amount)? returns an error immediately if the target instruction fails.
  • Atomicity: if the overall transaction fails, successful earlier changes in the transaction, including changes from a completed CPI, do not remain committed.
  • Shared compute budget: the caller and every callee use the transaction’s compute-unit budget. A program cannot bypass compute limits by shifting work into CPIs.

As programs become more sophisticated, it is easy to chain several CPIs, such as creating an account, creating an associated token account, minting tokens, and attaching metadata. That convenience must be balanced against compute cost, explicit account validation, and clear authority boundaries.


Designing safely around CPIs

A CPI is an integration boundary. Treat it with the same care you would give a sensitive API call, except that all account inputs are part of the security boundary.

Before making a CPI, your program should:

  • Validate its own business conditions first: amounts, authority relationships, state transitions, and timing rules.
  • Constrain the target program to the expected program ID. In Anchor, Program<'info, System> is safer than accepting an unconstrained AccountInfo, because it verifies the System Program identity.
  • Pass only the accounts that the target instruction actually needs.
  • Mark only genuinely modified accounts as mutable.
  • Require signer authority only where the target instruction actually needs it.
  • Treat an error from the callee as a meaningful outcome, not as an implementation detail to ignore.

The callee also performs its own checks. For example, the Token Program verifies its token-account and authority rules even if your program has already validated a request. Proper CPI design relies on both layers: the caller enforces application-specific policy, while the callee enforces the invariant rules of the operation it owns.


Key takeaways

A CPI is a nested instruction call: one Solana program invokes a specific instruction on another program while the original transaction is executing. It is the foundation of Solana’s composability and is necessary whenever your program needs an operation controlled by another program, such as a SOL transfer or token mint.

Remember these points:

  • A CPI contains a target program ID, the target’s required accounts, and instruction data.
  • It executes synchronously within the same transaction, shares its compute budget, and participates in its atomic success or failure.
  • Account privileges flow from the outer instruction into the nested call, but they cannot be escalated.
  • invoke uses transaction-provided signers; invoke_signed enables narrowly scoped PDA signing.
  • In Anchor, CpiContext plus a target-program helper such as system_program::transfer expresses the same low-level CPI mechanism more ergonomically.

Next, you will turn this model into working code by performing a CPI to the System Program to create a new account.

Can't find a good explanation? Sign up and we'll make it for you

Sign up