Cross Program Invocation (CPI)
A Cross Program Invocation (CPI) refers to when one program invokes the instructions of another program. This allows for the composability of Solana programs.
You can think of instructions as API endpoints that a program exposes to the network and a CPI as one API internally invoking another API.
Cross Program Invocation
Key Points
- Cross Program Invocations enable Solana program instructions to directly invoke instructions on another program.
- Signer privileges from a caller program extend to the callee program.
- When making a Cross Program Invocation, programs can sign on behalf of PDAs derived from their own program ID.
- The callee program can make further CPIs to other programs, up to a depth of 4.
What is a CPI?
A Cross Program Invocation (CPI) is when one program invokes the instructions of another program.
Writing a program instruction with a CPI follows the same pattern as building an instruction to add to a transaction. Under the hood, each CPI instruction must specify:
- Program address: Specifies the program to invoke
- Accounts: Lists every account the instruction reads from or writes to, including other programs
- Instruction Data: Specifies which instruction to invoke on the program, plus any data the instruction needs (function arguments)
When a program makes a Cross Program Invocation (CPI) to another program:
- The signer privileges from the initial transaction extend to the callee program (ex. A->B)
- The callee program can make further CPIs to other programs, up to a depth of 4 (ex. B->C, C->D)
- The programs can "sign" on behalf of the PDAs derived from its program ID
The Solana program runtime sets a
max_instruction_stack_depth
constant
MAX_INSTRUCTION_STACK_DEPTH
of 5. This represents the max height of the program instruction invocation
stack. The stack height begins at 1 for the initial transaction and increases
by 1 each time a program invokes another instruction. This setting limits
invocation depth for CPIs to 4.
When a transaction is processed, account privileges extend from one program to another. Here's what that means:
Let's say Program A receives an instruction with:
- An account that signed the transaction
- An account that can be written to (mutable)
When Program A makes a CPI to Program B:
- Program B gets to use these same accounts with their original permissions
- Program B can sign with the signer account
- Program B can write to the writable account
- Program B can even pass these same permissions forward if it makes its own CPIs
Cross Program Invocations
The
invoke
function handles CPIs that don't require PDA signers. The function calls the
invoke_signed
function with an empty signers_seeds
array, indicating no PDAs
required for signing.
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {invoke_signed(instruction, account_infos, &[])}
The following examples show how to make a CPI using the Anchor Framework and Native Rust. The example programs include a single instruction that transfers SOL from one account to another using a CPI.
Anchor Framework
The following examples present three ways to create Cross Program Invocations (CPIs) in an Anchor program, each at a different level of abstraction. All examples work the same way. The main purpose is to show the implementation details of a CPI.
- Example 1: Uses Anchor's
CpiContext
and helper function to construct the CPI instruction. - Example 2: Uses the
system_instruction::transfer
function from thesolana_program
crate to construct the CPI instruction. Example 1 abstracts this implementation. - Example 3: Constructs the CPI instruction manually. This approach is useful when no crate exists to help build the instruction.
use anchor_lang::prelude::*;use anchor_lang::system_program::{transfer, Transfer};declare_id!("9AvUNHjxscdkiKQ8tUn12QCMXtcnbR9BVGq3ULNzFMRi");#[program]pub mod cpi {use super::*;pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> {let from_pubkey = ctx.accounts.sender.to_account_info();let to_pubkey = ctx.accounts.recipient.to_account_info();let program_id = ctx.accounts.system_program.to_account_info();let cpi_context = CpiContext::new(program_id,Transfer {from: from_pubkey,to: to_pubkey,},);transfer(cpi_context, amount)?;Ok(())}}#[derive(Accounts)]pub struct SolTransfer<'info> {#[account(mut)]sender: Signer<'info>,#[account(mut)]recipient: SystemAccount<'info>,system_program: Program<'info, System>,}
Native Rust
The following example shows how to make a CPI from a program written in Native Rust. The program includes a single instruction that transfers SOL from one account to another using a CPI. The test file uses the LiteSVM to test the program.
use borsh::BorshDeserialize;use solana_program::{account_info::AccountInfo,entrypoint,entrypoint::ProgramResult,program::invoke,program_error::ProgramError,pubkey::Pubkey,system_instruction,};// Declare program entrypointentrypoint!(process_instruction);// Define program instructions#[derive(BorshDeserialize)]enum ProgramInstruction {SolTransfer { amount: u64 },}impl ProgramInstruction {fn unpack(input: &[u8]) -> Result<Self, ProgramError> {Self::try_from_slice(input).map_err(|_| ProgramError::InvalidInstructionData)}}pub fn process_instruction(_program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Deserialize instruction datalet instruction = ProgramInstruction::unpack(instruction_data)?;// Process instructionmatch instruction {ProgramInstruction::SolTransfer { amount } => {// Parse accountslet [sender_info, recipient_info, system_program_info] = accounts else {return Err(ProgramError::NotEnoughAccountKeys);};// Verify the sender is a signerif !sender_info.is_signer {return Err(ProgramError::MissingRequiredSignature);}// Create and invoke the transfer instructionlet transfer_ix = system_instruction::transfer(sender_info.key,recipient_info.key,amount,);invoke(&transfer_ix,&[sender_info.clone(),recipient_info.clone(),system_program_info.clone(),],)?;Ok(())}}}
Cross Program Invocations with PDA Signers
The
invoke_signed
function handles CPIs that require PDA signers. The function takes the seeds for
deriving signer PDAs as signer_seeds
.
You can reference the Program Derived Address page for details on how to derive PDAs.
pub fn invoke_signed(instruction: &Instruction,account_infos: &[AccountInfo],signers_seeds: &[&[&[u8]]],) -> ProgramResult {// --snip--invoke_signed_unchecked(instruction, account_infos, signers_seeds)}
When processing an instruction that includes a CPI, the Solana runtime
internally calls
create_program_address
using the signers_seeds
and the program_id
of the calling program. When a
valid PDA verified, the address is
added as a valid signer.
The following examples demonstrate how to make a CPI with PDA signers using the Anchor Framework and Native Rust. The example programs include a single instruction that transfers SOL from a PDA to the recipient account using a CPI signed by the PDA.
Anchor Framework
The following examples include three approaches to implementing Cross Program Invocations (CPIs) in an Anchor program, each at a different level of abstraction. All examples are functionally equivalent. The main purpose is to illustrate the implementation details of a CPI.
- Example 1: Uses Anchor's
CpiContext
and helper function to construct the CPI instruction. - Example 2: Uses the
system_instruction::transfer
function fromsolana_program
crate to construct the CPI instruction. Example 1 is an abstraction of this implementation. - Example 3: Constructs the CPI instruction manually. This approach is useful when there is no crate available to help build the instruction you want to invoke.
use anchor_lang::prelude::*;use anchor_lang::system_program::{transfer, Transfer};declare_id!("BrcdB9sV7z9DvF9rDHG263HUxXgJM3iCQdF36TcxbFEn");#[program]pub mod cpi {use super::*;pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> {let from_pubkey = ctx.accounts.pda_account.to_account_info();let to_pubkey = ctx.accounts.recipient.to_account_info();let program_id = ctx.accounts.system_program.to_account_info();let seed = to_pubkey.key();let bump_seed = ctx.bumps.pda_account;let signer_seeds: &[&[&[u8]]] = &[&[b"pda", seed.as_ref(), &[bump_seed]]];let cpi_context = CpiContext::new(program_id,Transfer {from: from_pubkey,to: to_pubkey,},).with_signer(signer_seeds);transfer(cpi_context, amount)?;Ok(())}}#[derive(Accounts)]pub struct SolTransfer<'info> {#[account(mut,seeds = [b"pda", recipient.key().as_ref()],bump,)]pda_account: SystemAccount<'info>,#[account(mut)]recipient: SystemAccount<'info>,system_program: Program<'info, System>,}
Native Rust
The following example shows how to make a CPI with PDA signers from a program written in Native Rust. The program includes a single instruction that transfers SOL from a PDA to the recipient account using a CPI signed by the PDA. The test file uses the LiteSVM to test the program.
use borsh::BorshDeserialize;use solana_program::{account_info::AccountInfo,entrypoint,entrypoint::ProgramResult,program::invoke_signed,program_error::ProgramError,pubkey::Pubkey,system_instruction,};// Declare program entrypointentrypoint!(process_instruction);// Define program instructions#[derive(BorshDeserialize)]enum ProgramInstruction {SolTransfer { amount: u64 },}impl ProgramInstruction {fn unpack(input: &[u8]) -> Result<Self, ProgramError> {Self::try_from_slice(input).map_err(|_| ProgramError::InvalidInstructionData)}}pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Deserialize instruction datalet instruction = ProgramInstruction::unpack(instruction_data)?;// Process instructionmatch instruction {ProgramInstruction::SolTransfer { amount } => {// Parse accountslet [pda_account_info, recipient_info, system_program_info] = accounts else {return Err(ProgramError::NotEnoughAccountKeys);};// Derive PDA and verify it matches the account provided by clientlet recipient_pubkey = recipient_info.key;let seeds = &[b"pda", recipient_pubkey.as_ref()];let (expected_pda, bump_seed) = Pubkey::find_program_address(seeds, program_id);if expected_pda != *pda_account_info.key {return Err(ProgramError::InvalidArgument);}// Create the transfer instructionlet transfer_ix = system_instruction::transfer(pda_account_info.key,recipient_info.key,amount,);// Create signer seeds for PDAlet signer_seeds: &[&[&[u8]]] = &[&[b"pda", recipient_pubkey.as_ref(), &[bump_seed]]];// Invoke the transfer instruction with PDA as signerinvoke_signed(&transfer_ix,&[pda_account_info.clone(),recipient_info.clone(),system_program_info.clone(),],signer_seeds,)?;Ok(())}}}
Is this page helpful?