| name | anchor-cpis |
| description | Cross-Program Invocation patterns for safely calling other programs. Load when implementing SPL token transfers, system program calls, or any inter-program communication. |
Anchor CPIs
Cross-Program Invocations (CPIs) allow programs to call other programs. This skill covers CPI setup, privilege extension, program signing with PDAs, and security best practices.
Quick Reference
Basic CPI Pattern
let cpi_program = ctx.accounts.target_program.to_account_info();
let cpi_accounts = TargetInstruction {
account1: ctx.accounts.account1.to_account_info(),
account2: ctx.accounts.account2.to_account_info(),
};
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
target_program::cpi::instruction_name(cpi_ctx, args)?;
CPI with PDA Signer
let seeds = &[
b"authority".as_ref(),
&[authority_bump],
];
let signer_seeds = &[&seeds[..]];
token::transfer(
ctx.accounts.transfer_ctx().with_signer(signer_seeds),
amount
)?;
Organizing CPI Code
impl<'info> MyAccounts<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = Transfer {
from: self.source.to_account_info(),
to: self.destination.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}
CPI Fundamentals
Source: Anchor Book - Cross-Program Invocations
What are CPIs?
Cross-Program Invocations allow one program to invoke instructions on another program. This enables:
- Token transfers (SPL Token program)
- Account creation (System program)
- Associated token account creation (Associated Token program)
- Composable program interactions
Key Concepts
- Privilege Extension: Signatures and mutability extend from caller to callee
- Program Validation: Target program must be verified to prevent arbitrary CPIs
- Account Passing: Accounts passed to CPI must be included in caller's accounts
- Signer Seeds: PDAs can sign CPIs using their derivation seeds
Basic CPI Setup
Example Programs: Puppet and Puppet Master
Puppet program (target of CPI):
use anchor_lang::prelude::*;
#[program]
pub mod puppet {
use super::*;
pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
let puppet = &mut ctx.accounts.puppet;
puppet.data = data;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = user, space = 8 + 8)]
pub puppet: Account<'info, Data>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct SetData<'info> {
#[account(mut)]
pub puppet: Account<'info, Data>,
}
#[account]
pub struct Data {
pub data: u64,
}
Puppet Master program (makes CPI):
use anchor_lang::prelude::*;
use puppet::cpi::accounts::SetData;
use puppet::program::Puppet;
use puppet::{self, Data};
#[program]
mod puppet_master {
use super::*;
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<()> {
let cpi_program = ctx.accounts.puppet_program.to_account_info();
let cpi_accounts = SetData {
puppet: ctx.accounts.puppet.to_account_info(),
};
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
puppet::cpi::set_data(cpi_ctx, data)
}
}
#[derive(Accounts)]
pub struct PullStrings<'info> {
#[account(mut)]
pub puppet: Account<'info, Data>,
pub puppet_program: Program<'info, Puppet>,
}
Complete working example source: Anchor Book - CPI Setup
Enabling CPI Features
In the caller's Cargo.toml, import the target program with cpi feature:
[dependencies]
puppet = { path = "../puppet", features = ["cpi"] }
This generates:
puppet::cpi module with CPI helper functions
puppet::cpi::accounts with instruction builder structs
- Type-safe CPI invocation
CPI Components
- CPI Program: The program being called
- CPI Accounts: Accounts required by target instruction
- CPI Context: Combines program and accounts
- CPI Function: Generated helper for invoking instruction
Breaking Down the CPI
let cpi_program = ctx.accounts.puppet_program.to_account_info();
let cpi_accounts = SetData {
puppet: ctx.accounts.puppet.to_account_info(),
};
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
puppet::cpi::set_data(cpi_ctx, data)?;
The puppet::cpi::set_data function:
- Takes same arguments as
puppet::set_data handler
- Except uses
CpiContext instead of Context
- Handles low-level Solana syscalls
Privilege Extension
Critical concept: CPIs extend the caller's privileges to the callee.
Source: Anchor Book - Privilege Extension
Mutability Extension
If an account is mutable in the caller, it remains mutable in the callee:
#[derive(Accounts)]
pub struct PullStrings<'info> {
#[account(mut)]
pub puppet: Account<'info, Data>,
}
#[derive(Accounts)]
pub struct SetData<'info> {
#[account(mut)]
pub puppet: Account<'info, Data>,
}
Behavior: Changes made in callee persist after CPI returns.
Signature Extension
If an account signed the caller's transaction, it's also a signer in the callee:
Example with authority:
#[derive(Accounts)]
pub struct SetData<'info> {
#[account(mut, has_one = authority)]
pub puppet: Account<'info, Data>,
pub authority: Signer<'info>,
}
#[account]
pub struct Data {
pub data: u64,
pub authority: Pubkey,
}
#[derive(Accounts)]
pub struct PullStrings<'info> {
#[account(mut)]
pub puppet: Account<'info, Data>,
pub puppet_program: Program<'info, Puppet>,
pub authority: Signer<'info>,
}
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<()> {
puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)
}
impl<'info> PullStrings<'info> {
pub fn set_data_ctx(&self) -> CpiContext<'_, '_, '_, 'info, SetData<'info>> {
let cpi_program = self.puppet_program.to_account_info();
let cpi_accounts = SetData {
puppet: self.puppet.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(cpi_program, cpi_accounts)
}
}
Behavior: Authority's signature from caller transaction extends to callee's check.
Complete working example source: Anchor Book - Privilege Extension Example
Security Implications
Privilege extension is powerful but dangerous:
✅ Benefit: Enables seamless composability
⚠️ Risk: CPI to malicious program grants it all caller's privileges
Protection: Always validate CPI target program
pub puppet_program: Program<'info, Puppet>,
See: anchor-security skill - Arbitrary CPI pattern
CPI with PDA Signers
Source: Anchor Book - Programs as Signers
PDAs allow programs to sign CPIs, enabling programs to have authority over assets.
Basic Pattern
pub fn pull_strings(ctx: Context<PullStrings>, bump: u8, data: u64) -> Result<()> {
let seeds = &[&[bump]];
let signer_seeds = &[&seeds[..]];
puppet::cpi::set_data(
ctx.accounts.set_data_ctx().with_signer(signer_seeds),
data
)?;
Ok(())
}
#[derive(Accounts)]
pub struct PullStrings<'info> {
#[account(mut)]
pub puppet: Account<'info, Data>,
pub puppet_program: Program<'info, Puppet>,
pub authority: UncheckedAccount<'info>,
}
Note: Authority is UncheckedAccount because it's not a signer when passed in. The program adds the signature via with_signer.
Seed Format for Signing
Seeds must be in specific nested format:
let seeds = &[
seed1,
seed2,
&[bump],
];
let signer_seeds = &[&seeds[..]];
Common Patterns
Simple bump-only PDA:
let seeds = &[&[bump]];
let signer_seeds = &[&seeds[..]];
With static seed:
let seeds = &[
b"authority".as_ref(),
&[bump],
];
let signer_seeds = &[&seeds[..]];
With dynamic seeds:
let seeds = &[
b"vault".as_ref(),
escrow.key().as_ref(),
mint.key().as_ref(),
&[vault_bump],
];
let signer_seeds = &[&seeds[..]];
Using stored fields:
let seeds = &[
ctx.accounts.pool.withdraw_destination.as_ref(),
&[ctx.accounts.pool.bump],
];
let signer_seeds = &[&seeds[..]];
Complete Token Transfer Example
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
pub fn withdraw_tokens(ctx: Context<WithdrawTokens>) -> Result<()> {
let amount = ctx.accounts.vault.amount;
let seeds = &[
ctx.accounts.pool.withdraw_destination.as_ref(),
&[ctx.accounts.pool.bump],
];
let signer_seeds = &[&seeds[..]];
token::transfer(
ctx.accounts.transfer_ctx().with_signer(signer_seeds),
amount
)?;
Ok(())
}
#[derive(Accounts)]
pub struct WithdrawTokens<'info> {
#[account(
has_one = vault,
has_one = withdraw_destination,
seeds = [withdraw_destination.key().as_ref()],
bump = pool.bump,
)]
pub pool: Account<'info, TokenPool>,
#[account(mut)]
pub vault: Account<'info, TokenAccount>,
#[account(mut)]
pub withdraw_destination: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
impl<'info> WithdrawTokens<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = Transfer {
from: self.vault.to_account_info(),
to: self.withdraw_destination.to_account_info(),
authority: self.pool.to_account_info(),
};
CpiContext::new(program, accounts)
}
}
#[account]
pub struct TokenPool {
pub vault: Pubkey,
pub mint: Pubkey,
pub withdraw_destination: Pubkey,
pub bump: u8,
}
Source: Sealevel Attacks - PDA Sharing Recommended
Organizing CPI Code
Best practice: Move CPI setup to impl blocks for cleaner handler code.
Source: Anchor Book - Organizing CPI Code
Before (inline CPI setup)
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<()> {
let cpi_program = ctx.accounts.puppet_program.to_account_info();
let cpi_accounts = SetData {
puppet: ctx.accounts.puppet.to_account_info(),
};
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
puppet::cpi::set_data(cpi_ctx, data)?;
Ok(())
}
After (organized into impl)
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<()> {
puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)
}
impl<'info> PullStrings<'info> {
pub fn set_data_ctx(&self) -> CpiContext<'_, '_, '_, 'info, SetData<'info>> {
let cpi_program = self.puppet_program.to_account_info();
let cpi_accounts = SetData {
puppet: self.puppet.to_account_info(),
};
CpiContext::new(cpi_program, cpi_accounts)
}
}
Benefits:
- Handler focuses on business logic
- CPI setup reusable if needed multiple times
- Clearer code organization
- Type signature documents CPI structure
SPL Token CPIs
Common pattern for token operations in escrow and vault programs.
Token Transfer
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
token::transfer(ctx.accounts.transfer_ctx(), amount)?;
Ok(())
}
#[derive(Accounts)]
pub struct TransferTokens<'info> {
#[account(mut)]
pub from: Account<'info, TokenAccount>,
#[account(mut)]
pub to: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
impl<'info> TransferTokens<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = Transfer {
from: self.from.to_account_info(),
to: self.to.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}
Token Transfer with PDA Authority
pub fn withdraw_from_vault(ctx: Context<WithdrawFromVault>, amount: u64) -> Result<()> {
let seeds = &[
b"vault".as_ref(),
ctx.accounts.escrow.key().as_ref(),
&[ctx.accounts.vault_bump],
];
let signer_seeds = &[&seeds[..]];
token::transfer(
ctx.accounts.transfer_ctx().with_signer(signer_seeds),
amount
)?;
Ok(())
}
Token Account Initialization
use anchor_spl::token::{self, Mint, Token, TokenAccount, InitializeAccount};
pub fn initialize_token_account(ctx: Context<InitTokenAccount>) -> Result<()> {
token::initialize_account(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
InitializeAccount {
account: ctx.accounts.token_account.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
),
)?;
Ok(())
}
Note: Usually use Anchor's init + token::* constraints instead of manual CPIs.
System Program CPIs
Creating Accounts
use anchor_lang::system_program::{self, CreateAccount};
pub fn create_account_manual(
ctx: Context<CreateAccountManual>,
lamports: u64,
space: u64,
) -> Result<()> {
system_program::create_account(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.new_account.to_account_info(),
},
),
lamports,
space,
&ctx.accounts.program_id,
)?;
Ok(())
}
Note: Usually use Anchor's init constraint instead of manual account creation.
Transferring SOL
use anchor_lang::system_program::{self, Transfer};
pub fn transfer_sol(ctx: Context<TransferSol>, amount: u64) -> Result<()> {
system_program::transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
},
),
amount,
)?;
Ok(())
}
#[derive(Accounts)]
pub struct TransferSol<'info> {
#[account(mut)]
pub from: Signer<'info>,
#[account(mut)]
pub to: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
Account Reloading
Source: Anchor Book - Reloading an Account
The Problem
When a CPI modifies an account, the caller's deserialized copy doesn't auto-update:
pub fn pull_strings_and_check(ctx: Context<PullStrings>, data: u64) -> Result<()> {
puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)?;
if ctx.accounts.puppet.data != 42 {
panic!();
}
Ok(())
}
Why? Account<'info, T> deserializes once at instruction start. CPI changes underlying account data, but not the deserialized struct.
The Solution: reload()
pub fn pull_strings_and_check(ctx: Context<PullStrings>, data: u64) -> Result<()> {
puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)?;
ctx.accounts.puppet.reload()?;
if ctx.accounts.puppet.data != 42 {
panic!();
}
Ok(())
}
What reload() does:
- Re-reads account data from memory
- Re-deserializes into struct
- Updates the
Account<'info, T> value
When to Use reload()
Call reload() when you need to:
- Read account data modified by a CPI
- Verify CPI results
- Use updated values in subsequent logic
- Check state changes made by called program
Return Values from CPIs
Source: Anchor Book - Returning Values
Solana's set_return_data / get_return_data syscalls enable CPIs to return data.
Defining Return Values
Change handler return type from Result<()> to Result<T>:
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<u64> {
let puppet = &mut ctx.accounts.puppet;
puppet.data = data;
Ok(data)
}
Receiving Return Values
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<()> {
let result = puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)?;
let return_data = result.get();
msg!("Puppet returned: {}", return_data);
Ok(())
}
Note: .get() calls sol_get_return syscall and deserializes result.
Return Type Requirements
Types must implement AnchorSerialize and AnchorDeserialize:
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct StructReturn {
pub value: u64,
pub success: bool,
}
pub fn complex_operation(ctx: Context<Op>) -> Result<StructReturn> {
Ok(StructReturn {
value: 42,
success: true,
})
}
Size Limit
Return data limited to 1024 bytes. For larger data, use account state instead.
CPI Security Checklist
When making CPIs:
See: anchor-security skill - Arbitrary CPI pattern
Common Patterns for Escrow
Transferring Tokens to Merchant
pub fn finalize_settlement(ctx: Context<Finalize>) -> Result<()> {
let amount = ctx.accounts.pending.amount;
let seeds = &[
b"vault".as_ref(),
ctx.accounts.escrow.key().as_ref(),
ctx.accounts.mint.key().as_ref(),
&[ctx.accounts.vault_bump],
];
let signer_seeds = &[&seeds[..]];
token::transfer(
ctx.accounts.transfer_ctx().with_signer(signer_seeds),
amount
)?;
Ok(())
}
impl<'info> Finalize<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = Transfer {
from: self.vault.to_account_info(),
to: self.recipient.to_account_info(),
authority: self.vault.to_account_info(),
};
CpiContext::new(program, accounts)
}
}
Closing Accounts with Rent Return
Already handled by Anchor's close constraint, but manual pattern:
pub fn close_pending_manual(ctx: Context<ClosePending>) -> Result<()> {
let dest_starting_lamports = ctx.accounts.facilitator.lamports();
**ctx.accounts.facilitator.lamports.borrow_mut() = dest_starting_lamports
.checked_add(ctx.accounts.pending.to_account_info().lamports())
.unwrap();
**ctx.accounts.pending.to_account_info().lamports.borrow_mut() = 0;
ctx.accounts.pending.to_account_info().assign(&system_program::ID);
ctx.accounts.pending.to_account_info().realloc(0, false)?;
Ok(())
}
But prefer using close constraint:
#[account(mut, close = facilitator)]
pub pending: Account<'info, PendingSettlement>,
Skill Loading Guidance
Always Load With
- anchor-core - Core patterns prerequisite
- anchor-security - CPI security (arbitrary CPI pattern)
Commonly Paired With
- anchor-pdas - PDA signing in CPIs
- anchor-token-operations - Token transfer CPIs
Load This Skill When
- Implementing token transfers
- Making cross-program calls
- Working with program-controlled assets
- Implementing vault or escrow logic
- Creating or closing accounts via CPI
Related Skills
- anchor-core - For CpiContext and basic patterns
- anchor-security - For CPI security (pattern 6: Arbitrary CPI)
- anchor-pdas - For PDA signing patterns
- anchor-token-operations - For token-specific CPI patterns
Reference Links
Official Documentation
Source Material
Acknowledgment
At the start of a session, after reviewing this skill, state: "I have reviewed the anchor-cpis skill and understand how to safely make cross-program invocations."