Cross Program Invocation (CPI)
A cross program invocation is when a program invokes the instructions of another program.
This can only go to a max depth of 4 programs.
Accounts and permissions cascade across the invocations, meaning that the accounts and permissions given to program A, will cascade to the subsequent programs it calls.
Programs use the invoke
and invoke_signed
functions to trigger other programs behavior.
Invoke
You use invoke
when making any CPI that does not require any PDAs to act as signers. Your program will pass on the original signatures to the program being called.
This is how you sign for a regular account owned by the System Program.
The invoke
method actually just calls invoke_signed
with an empty signers_seed
array:
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {
invoke_signed(instruction, account_infos, &[])
}
Invoke signed
When you need to sign with a PDA you have to use invoke_signed
and provide the seeds required to derive the PDA of the calling program.
PDAs don't have private keys. The Solana runtime will call create_program_address
using the seeds
and program_id
of the calling program. It then compares the resulting PDA to the addresses supplied in the instruction. If it matches it is considered a valid signer.
This is how you sign for transactions on accounts owned by a program instead of the system (like a normal user account which just requires a regular private key signer)
A simple transfer
Here's an example of a program
written in Rust using Anchor that handles transfers:
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>,
}
This program takes a SolTransfer
instruction with an amount and ferries it to the next program, using the provided program_id
from the system_program
found on the SolTransfer
struct.
Token transfers
Token transfers are done through the anchor_spl
crate.
let transfer = anchor_spl::token::Transfer {
to: ctx.accounts.buyer_token_account.to_account_info(),
from: ctx.accounts.token_vault.to_account_info(),
authority: ctx.accounts.pool.to_account_info(),
};
// Transfer tokens to seller from pool
anchor_spl::token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
transfer,
signer_seeds,
),
usdc_amount,
)?;
PDA Transfers
When making transfers through a Program where the Accounts are program-owned, and found at PDAs, we use invoke_signed
and hand in the signers_seeds
alongside the instruction
and account_infos
array.