20 KiB
Cross-Program Invocation (CPI)
This reference provides comprehensive coverage of Cross-Program Invocation (CPI) for native Rust Solana program development, including invoke patterns, account privilege propagation, and security considerations.
Table of Contents
- What is CPI
- CPI Fundamentals
- invoke vs invoke_signed
- Account Privilege Propagation
- Common CPI Patterns
- CPI Limits and Constraints
- Security Considerations
- Best Practices
What is CPI
Cross-Program Invocation (CPI) is when one Solana program directly calls instructions on another program.
Conceptual Model
If you think of a Solana instruction as an API endpoint, a CPI is like one API endpoint internally calling another.
User Transaction
│
▼
┌────────────────────┐
│ Your Program │
│ │
│ ┌──────────────┐ │
│ │ Instruction │ │
│ │ Handler │ │
│ └──────┬───────┘ │
│ │ CPI │
└──────────┼─────────┘
│
▼
┌────────────────────┐
│ System Program │
│ create_account │
└────────────────────┘
Why CPI is Essential
Composability: Programs can leverage functionality from other programs without reimplementing it.
Common Use Cases:
- Create accounts (System Program CPI)
- Transfer tokens (Token Program CPI)
- Interact with DeFi protocols
- Call custom program logic
- Complex multi-step operations
CPI vs Direct Instruction
| Aspect | Direct Instruction | CPI |
|---|---|---|
| Who initiates | User wallet | Another program |
| Signer source | User's private key | Program or PDA |
| Call depth | 1 (top-level) | 2-5 (nested) |
| Use case | Entry point | Program-to-program |
CPI Fundamentals
The Two CPI Functions
Solana provides two functions for making CPIs:
use solana_program::program::{invoke, invoke_signed};
// 1. invoke: For regular account signers
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo],
) -> ProgramResult
// 2. invoke_signed: For PDA signers
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo],
signers_seeds: &[&[&[u8]]],
) -> ProgramResult
Required Imports
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
instruction::{AccountMeta, Instruction},
program::{invoke, invoke_signed},
pubkey::Pubkey,
};
Instruction Structure
Before making a CPI, you must construct an Instruction:
pub struct Instruction {
/// Program ID of the program being invoked
pub program_id: Pubkey,
/// Accounts required by the instruction
pub accounts: Vec<AccountMeta>,
/// Serialized instruction data
pub data: Vec<u8>,
}
pub struct AccountMeta {
/// Account public key
pub pubkey: Pubkey,
/// Is this account a signer?
pub is_signer: bool,
/// Is this account writable?
pub is_writable: bool,
}
invoke vs invoke_signed
invoke: Regular Signers
Use invoke when all required signers are regular accounts (not PDAs).
Example: User transfers SOL
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
pub fn user_transfer_sol(
_program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let sender = next_account_info(account_info_iter)?;
let recipient = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Verify sender signed the transaction
if !sender.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Create transfer instruction
let transfer_ix = system_instruction::transfer(
sender.key,
recipient.key,
amount,
);
// Execute CPI (sender already signed the transaction)
invoke(
&transfer_ix,
&[
sender.clone(),
recipient.clone(),
system_program.clone(),
],
)?;
Ok(())
}
Key Points:
sender.is_signermust be true (verified at transaction level)- No
signers_seedsneeded invokeinternally callsinvoke_signedwith empty seeds
invoke_signed: PDA Signers
Use invoke_signed when a PDA needs to sign the instruction.
Example: PDA transfers SOL
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
pub fn pda_transfer_sol(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let pda_account = next_account_info(account_info_iter)?;
let recipient = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Derive PDA and verify
let (pda, bump_seed) = Pubkey::find_program_address(
&[b"vault", recipient.key.as_ref()],
program_id,
);
if pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
// Create transfer instruction
let transfer_ix = system_instruction::transfer(
pda_account.key, // From PDA (needs signing!)
recipient.key,
amount,
);
// PDA signing seeds (must match derivation)
let signer_seeds: &[&[&[u8]]] = &[&[
b"vault",
recipient.key.as_ref(),
&[bump_seed], // Critical: bump must be included
]];
// Execute CPI with PDA signature
invoke_signed(
&transfer_ix,
&[
pda_account.clone(),
recipient.clone(),
system_program.clone(),
],
signer_seeds, // Runtime verifies and grants signing authority
)?;
Ok(())
}
How Runtime Handles PDA Signing:
- Runtime receives
signers_seeds - Calls
create_program_address(signers_seeds, calling_program_id) - Verifies derived PDA matches an account in the instruction
- Grants signing authority for that account
- Executes the CPI
Critical: Seeds must exactly match the PDA derivation, including the bump.
Account Privilege Propagation
Privilege Extension
When making a CPI, account privileges extend from the caller to the callee.
User Transaction
│ (provides: signer=true, writable=true)
▼
┌─────────────────────┐
│ Program A │
│ Receives accounts: │
│ - user (signer) │──┐ Privileges
│ - vault (writable) │ │ propagate
└─────────────────────┘ │
▼
┌─────────────────────┐
│ Program B (via CPI)│
│ Can use: │
│ - user (signer) │
│ - vault (writable) │
└─────────────────────┘
Propagation Rules
Rule 1: If an account is a signer in Program A, it remains a signer in Program B (via CPI)
Rule 2: If an account is writable in Program A, it remains writable in Program B (via CPI)
Rule 3: Programs can add PDA signers via invoke_signed
Rule 4: Programs cannot escalate privileges (can't make non-signer a signer without PDA derivation)
Example: Privilege Propagation Chain
// User calls Program A
// Accounts: [user (signer, writable), vault (writable), data_account]
// Program A → CPI to Program B
invoke(
&instruction_for_program_b,
&[user.clone(), vault.clone()], // Both retain privileges
)?;
// Program B → CPI to Program C
invoke(
&instruction_for_program_c,
&[user.clone()], // user still a signer!
)?;
Depth: Up to 4 levels of CPI (5 total stack height including initial transaction)
Common CPI Patterns
1. System Program: Create Account
Most common CPI: Creating new accounts.
use solana_program::{
program::invoke_signed,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
pub fn create_pda_account(
program_id: &Pubkey,
payer: &AccountInfo,
pda_account: &AccountInfo,
system_program: &AccountInfo,
space: usize,
seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
// Calculate rent
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(space);
// Create account instruction
let create_account_ix = system_instruction::create_account(
payer.key,
pda_account.key,
rent_lamports,
space as u64,
program_id,
);
// Prepare signer seeds
let mut full_seeds = seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
// Execute CPI
invoke_signed(
&create_account_ix,
&[payer.clone(), pda_account.clone(), system_program.clone()],
signer_seeds,
)?;
Ok(())
}
2. System Program: Transfer SOL
use solana_program::system_instruction;
// From regular account
let transfer_ix = system_instruction::transfer(from_key, to_key, lamports);
invoke(&transfer_ix, &[from_account, to_account, system_program])?;
// From PDA
let transfer_ix = system_instruction::transfer(pda_key, to_key, lamports);
let signer_seeds: &[&[&[u8]]] = &[&[seeds, &[bump]]];
invoke_signed(&transfer_ix, &[pda_account, to_account, system_program], signer_seeds)?;
3. Custom Program CPI
Calling another custom program:
use borsh::BorshSerialize;
#[derive(BorshSerialize)]
struct CustomInstructionData {
amount: u64,
memo: String,
}
pub fn call_custom_program(
custom_program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let user = next_account_info(account_info_iter)?;
let target_account = next_account_info(account_info_iter)?;
let custom_program = next_account_info(account_info_iter)?;
// Serialize instruction data
let instruction_data = CustomInstructionData {
amount,
memo: "Hello from CPI".to_string(),
};
let data = instruction_data.try_to_vec()?;
// Build instruction
let instruction = Instruction {
program_id: *custom_program_id,
accounts: vec![
AccountMeta::new(*user.key, true), // signer, writable
AccountMeta::new(*target_account.key, false), // writable
],
data,
};
// Execute CPI
invoke(
&instruction,
&[user.clone(), target_account.clone(), custom_program.clone()],
)?;
Ok(())
}
4. Multiple PDAs Signing
pub fn multi_pda_cpi(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let pda1_seeds = &[b"pda1", &[bump1]];
let pda2_seeds = &[b"pda2", &[bump2]];
// Multiple PDA signers
let signer_seeds: &[&[&[u8]]] = &[
pda1_seeds, // First PDA
pda2_seeds, // Second PDA
];
invoke_signed(&instruction, &accounts, signer_seeds)?;
Ok(())
}
5. Chained CPIs
Program A calls Program B, which calls Program C:
// In Program A
pub fn program_a_handler(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
// Call Program B
let instruction_for_b = build_program_b_instruction();
invoke(&instruction_for_b, accounts)?;
Ok(())
}
// In Program B
pub fn program_b_handler(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
// Call Program C
let instruction_for_c = build_program_c_instruction();
invoke(&instruction_for_c, accounts)?;
Ok(())
}
Depth tracking: User→A→B→C = stack depth 4 (within limit)
CPI Limits and Constraints
Stack Depth Limit
Maximum call depth: 5 (including initial transaction)
Depth 1: User Transaction
Depth 2: Program A (first CPI)
Depth 3: Program B (second CPI)
Depth 4: Program C (third CPI)
Depth 5: Program D (fourth CPI)
Depth 6: ❌ ERROR - MAX_INSTRUCTION_STACK_DEPTH exceeded
Constant:
// From agave source
pub const MAX_INSTRUCTION_STACK_DEPTH: usize = 5;
Error when exceeded:
Error: CallDepth(5)
Account Limits
- Max accounts per instruction: 256 (practical limit ~64 without ALTs)
- Max writable accounts: Limited by transaction size
- Duplicate accounts: Allowed but share state (mutations visible to all references)
Compute Unit Costs
CPI operations consume compute units:
| Operation | Approximate CU Cost |
|---|---|
invoke base cost |
~1,000 CU |
invoke_signed base cost |
~1,000 CU |
| Per account passed | ~50-100 CU |
| PDA derivation in runtime | ~1,500 CU |
| Actual callee logic | Variable |
Tip: Pre-derive PDAs and store bumps to save CU.
Data Size Limits
- Instruction data: No hard limit, but affects transaction size (1232 bytes max for non-ALT transactions)
- Account data modification: Accounts can be resized via
realloc(up to 10 MiB)
Security Considerations
1. Validate PDA Derivation Before CPI
❌ Vulnerable:
pub fn vulnerable_cpi(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let pda_account = &accounts[0];
// No validation!
let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[bump]]];
invoke_signed(&instruction, &[pda_account.clone()], signer_seeds)?;
Ok(())
}
✅ Secure:
pub fn secure_cpi(
program_id: &Pubkey,
accounts: &[AccountInfo],
bump: u8,
) -> ProgramResult {
let pda_account = &accounts[0];
// Validate PDA before CPI
let (expected_pda, _) = Pubkey::find_program_address(&[b"vault"], program_id);
if expected_pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[bump]]];
invoke_signed(&instruction, &[pda_account.clone()], signer_seeds)?;
Ok(())
}
2. Verify Signer Requirements
Always check is_signer before making CPIs that transfer value:
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let transfer_ix = system_instruction::transfer(user.key, vault.key, amount);
invoke(&transfer_ix, &[user.clone(), vault.clone(), system_program.clone()])?;
3. Program ID Verification
Verify the program being called is the expected program:
const EXPECTED_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
if token_program.key.to_string() != EXPECTED_PROGRAM {
return Err(ProgramError::IncorrectProgramId);
}
4. Privilege Leakage
Be careful about which accounts you pass in CPIs:
// ❌ Dangerous - passes admin with signer privilege
invoke(
&untrusted_program_instruction,
&[admin.clone(), user_data.clone()], // Admin is a signer!
)?;
// ✅ Safe - only pass necessary accounts
invoke(
&untrusted_program_instruction,
&[user_data.clone()], // Admin not included
)?;
5. Reent rancy Considerations
Solana programs are generally safe from reentrancy because:
- Accounts are locked during instruction execution
- Runtime prevents concurrent modifications
However, be cautious with:
- State assumptions across CPI boundaries
- Read-modify-write patterns split across CPIs
6. Error Handling
CPI errors propagate to the caller:
// If CPI fails, entire transaction reverts
match invoke(&instruction, &accounts) {
Ok(()) => msg!("CPI succeeded"),
Err(e) => {
msg!("CPI failed: {:?}", e);
return Err(e); // Propagate error
}
}
All state changes are atomic - if CPI fails, all changes rollback.
Best Practices
1. Derive PDAs Once
// ❌ Wasteful - derives multiple times
pub fn wasteful(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let (pda, bump) = Pubkey::find_program_address(&[b"data"], program_id);
// ... use pda
let (pda_again, bump_again) = Pubkey::find_program_address(&[b"data"], program_id);
// ... use pda_again (same as pda!)
}
// ✅ Efficient - derive once, reuse
pub fn efficient(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let (pda, bump) = Pubkey::find_program_address(&[b"data"], program_id);
// Reuse pda and bump
}
2. Store and Reuse Bumps
#[derive(BorshSerialize, BorshDeserialize)]
pub struct VaultData {
pub bump: u8, // Store on creation
// ... other fields
}
// On CPI: use stored bump
let vault_data = VaultData::try_from_slice(&vault_pda.data.borrow())?;
let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[vault_data.bump]]];
Benefit: Saves ~2,700 CU per operation.
3. Helper Functions for Common CPIs
pub mod cpi_helpers {
use super::*;
pub fn transfer_sol(
from: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
amount: u64,
) -> ProgramResult {
let ix = system_instruction::transfer(from.key, to.key, amount);
invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])
}
pub fn transfer_sol_from_pda(
from_pda: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
amount: u64,
signer_seeds: &[&[&[u8]]],
) -> ProgramResult {
let ix = system_instruction::transfer(from_pda.key, to.key, amount);
invoke_signed(&ix, &[from_pda.clone(), to.clone(), system_program.clone()], signer_seeds)
}
}
4. Validate All CPI Inputs
Checklist before CPI:
- ✅ Verify signer requirements (
is_signer) - ✅ Validate PDA derivation
- ✅ Check program IDs match expectations
- ✅ Verify account ownership
- ✅ Validate data integrity
5. Document CPI Dependencies
/// Transfers SOL from program vault to recipient.
///
/// # Accounts
/// 0. `[writable]` vault_pda - Program vault (PDA, signer)
/// 1. `[writable]` recipient - Receives SOL
/// 2. `[]` system_program - System Program (11111...)
///
/// # CPIs Made
/// - System Program: transfer (from vault to recipient)
pub fn withdraw_from_vault(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
// ...
}
6. Error Context
invoke(&instruction, &accounts).map_err(|e| {
msg!("CPI to System Program failed");
e
})?;
7. Minimize CPI Depth
Keep call chains shallow:
- Reduces compute units
- Easier to debug
- Lower risk of hitting stack limit
- Better user experience (simpler transactions)
Summary
Key Takeaways:
- CPI enables composability - programs call other programs
- Use
invokefor regular signers,invoke_signedfor PDAs - Privileges propagate - signers and writable flags extend through CPIs
- Maximum depth is 5 - including initial transaction
- Always validate PDAs before using in
invoke_signed - Verify signer requirements to prevent unauthorized operations
- Store bumps in account data to save compute units
- CPIs are atomic - failures rollback all changes
Security Checklist:
- ✅ Validate PDA derivation with canonical bump
- ✅ Verify
is_signerfor value transfers - ✅ Check program IDs match expectations
- ✅ Only pass necessary accounts (avoid privilege leakage)
- ✅ Handle CPI errors appropriately
Common Pattern:
// 1. Validate inputs
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// 2. Derive and validate PDA if needed
let (pda, bump) = Pubkey::find_program_address(&seeds, program_id);
if pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
// 3. Build instruction
let ix = build_instruction();
// 4. Execute CPI
invoke_signed(&ix, &accounts, &[&[seeds, &[bump]]])?;
CPI is the foundation of program composability on Solana. Master it to build powerful, modular programs that leverage the entire ecosystem.