Soldev

Cross Program Invocation (CPI)

Last updated:

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.