Cross Program Invocation
A cross-program invocation (CPI) occurs when one Solana program directly invokes the instructions of another program. This allows for program composability. If you think of a Solana instruction as an API endpoint that a program exposes to the network, a CPI is like one endpoint internally invoking another.
When making a CPI, a program can sign on behalf of a PDA derived from its program ID. These signer privileges extend from the caller program to the callee program.
Cross-program invocation example
When making a CPI, account privileges extend from one program to another. Let's say Program A receives an instruction with a signer account and a writable account. Program A then makes a CPI to Program B. Now, Program B can use the same accounts as Program A, with their original permissions. (Meaning Program B can sign with the signer account and can write to the writable account.) If Program B makes its own CPIs, it can pass these same permissions forward, up to a depth of 4.
The maximum height of the program instruction invocation is called the
max_instruction_stack_depth
and is set to the MAX_INSTRUCTION_STACK_DEPTH
constant of 5.
The stack height begins at 1 for the initial transaction and increases by 1 each time a program invokes another instruction, limiting invocation depth for CPIs to 4.
CPIs with PDA signers
When a CPI requires a PDA signer, the invoke_signed
function is used.
It takes the signer seeds used for deriving signer PDAs
The Solana runtime internally calls create_program_address
using the signers_seeds and the program_id of the caller program.
When a PDA is verified, it is added as a valid signer.
pub fn invoke_signed(instruction: &Instruction,account_infos: &[AccountInfo],signers_seeds: &[&[&[u8]]],) -> ProgramResult {// --snip--invoke_signed_unchecked(instruction, account_infos, signers_seeds)}
The examples below make a CPI with PDA signers using Anchor and Native Rust. Each example includes a single instruction to transfer SOL from a PDA to a recipient account, using a CPI signed by the PDA.
Anchor
The following examples show three approaches to implementing CPIs in an Anchor program. The examples are functionally equivalent, but each demonstrates a different level of abstraction.
- Example 1: Uses Anchor's
CpiContextand helper function. - Example 2: Uses the
system_instruction::transferfunction fromsolana_programcrate. (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>,}
Rust
The example below makes a CPI with PDA signers from a program written in Native Rust. It includes a single instruction that transfers SOL from a PDA account to another. The CPI is signed by the PDA account. (The test file uses 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(())}}}
CPIs without PDA signers
When a CPI doesn't require PDA signers, the invoke
function is used. The invoke function calls the invoke_signed function with an empty signers_seeds array.
The empty signers array indicates that no PDAs are required for signing.
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {invoke_signed(instruction, account_infos, &[])}
The examples below make a CPI using Anchor and Native Rust. It includes a single instruction that transfers SOL from one account to another.
Anchor
The following examples show three approaches to implementing CPIs in an Anchor program. The examples are functionally equivalent, but each demonstrates a different level of abstraction.
- Example 1: Uses Anchor's
CpiContextand helper function. - Example 2: Uses the
system_instruction::transferfunction from thesolana_programcrate. - Example 3: Constructs the CPI instruction manually. This approach is useful when no crate exists to help build the instruction you want to invoke.
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>,}
Rust
The following example shows how to make a CPI from a program written in Native Rust. It includes a single instruction that transfers SOL from one account to another. (The test file uses 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(())}}}
Is this page helpful?