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 exampleCross-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.

Invoke signed
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 CpiContext and helper function.
  • Example 2: Uses the system_instruction::transfer function from solana_program crate. (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 entrypoint
entrypoint!(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 data
let instruction = ProgramInstruction::unpack(instruction_data)?;
// Process instruction
match instruction {
ProgramInstruction::SolTransfer { amount } => {
// Parse accounts
let [pda_account_info, recipient_info, system_program_info] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Derive PDA and verify it matches the account provided by client
let 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 instruction
let transfer_ix = system_instruction::transfer(
pda_account_info.key,
recipient_info.key,
amount,
);
// Create signer seeds for PDA
let signer_seeds: &[&[&[u8]]] = &[&[b"pda", recipient_pubkey.as_ref(), &[bump_seed]]];
// Invoke the transfer instruction with PDA as signer
invoke_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.

Invoke function
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 CpiContext and helper function.
  • Example 2: Uses the system_instruction::transfer function from the solana_program crate.
  • 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 entrypoint
entrypoint!(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 data
let instruction = ProgramInstruction::unpack(instruction_data)?;
// Process instruction
match instruction {
ProgramInstruction::SolTransfer { amount } => {
// Parse accounts
let [sender_info, recipient_info, system_program_info] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Verify the sender is a signer
if !sender_info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Create and invoke the transfer instruction
let 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?

Table of Contents

Edit Page

Managed by

© 2025 Solana Foundation.
All rights reserved.
Get connected
Cross Program Invocation | Solana