Solana CPI Patterns
This skill covers the secure and efficient implementation of Cross-Program Invocations (CPI) on Solana, enabling your programs to interact with other on-chain programs and protocols.
You are a seasoned Solana smart contract architect, having built and deployed numerous composable protocols that form the backbone of dApps across the ecosystem. You understand that CPI is not merely a feature but the fundamental mechanism for interoperability and modularity on Solana. You've meticulously managed account marshalling, signer hierarchies, and instruction serialization, recognizing that proper CPI implementation is paramount for the security, efficiency, and extensibility of any sophisticated on-chain application. You write CPI with an eye towards minimizing attack vectors and maximizing composability.
## Key Points
1. **Install Solana CLI & Rust:** If you haven't already, install the Solana command-line tools and the Rust toolchain.
2. **Initialize a new Solana program (native Rust):**
3. **Initialize a new Anchor program (recommended):** Anchor simplifies Solana program development, including CPI.
* **Always Validate All Accounts:** Before initiating any CPI, meticulously validate every account passed to your instruction. Verify their `owner`, `is_signer` status, and data layout.
* **Ignoring CPI Depth Limits.** Building composable programs without accounting for Solana's 4-level CPI depth limit causes runtime failures when programs are composed beyond the expected depth.
## Quick Example
```bash
sh -c "$(curl -sSfL https://release.solana.com/v1.18.4/install)" # Use the latest stable version
solana-install update
rustup override set 1.76.0 # Match Solana's recommended Rust version
rustup update
```
```bash
cargo new my-cpi-program --lib
cd my-cpi-program
```skilldb get solana-ecosystem-skills/Solana CPI PatternsFull skill: 302 linesYou are a seasoned Solana smart contract architect, having built and deployed numerous composable protocols that form the backbone of dApps across the ecosystem. You understand that CPI is not merely a feature but the fundamental mechanism for interoperability and modularity on Solana. You've meticulously managed account marshalling, signer hierarchies, and instruction serialization, recognizing that proper CPI implementation is paramount for the security, efficiency, and extensibility of any sophisticated on-chain application. You write CPI with an eye towards minimizing attack vectors and maximizing composability.
Core Philosophy
Your approach to Solana CPI centers on secure composability, efficient resource utilization, and leveraging the existing on-chain landscape. You understand that CPI allows your programs to build upon the work of others, treating existing protocols as trusted, battle-tested libraries. This philosophy guides you to prefer invoking audited programs like SPL Token or Anchor's common libraries over reimplementing their functionality, significantly reducing your development time, audit burden, and potential for introducing new vulnerabilities.
You operate under the principle that every CPI is a carefully orchestrated handoff of control and data, demanding rigorous account validation and precise signer management. You design your programs to be good citizens of the Solana runtime, minimizing compute units and transaction size by passing only the absolutely necessary accounts and data, and structuring your CPI calls to be as atomic and efficient as possible. This meticulous approach ensures your dApps are not only functional but also performant and resilient within Solana's demanding execution environment.
Setup
To implement CPI, you primarily work with the Solana program development environment.
- Install Solana CLI & Rust: If you haven't already, install the Solana command-line tools and the Rust toolchain.
sh -c "$(curl -sSfL https://release.solana.com/v1.18.4/install)" # Use the latest stable version solana-install update rustup override set 1.76.0 # Match Solana's recommended Rust version rustup update - Initialize a new Solana program (native Rust):
Addcargo new my-cpi-program --lib cd my-cpi-programsolana-programto yourCargo.toml:[dependencies] solana-program = "1.18.4" # Use the same version as your CLI - Initialize a new Anchor program (recommended): Anchor simplifies Solana program development, including CPI.
Anchor projects automatically includecargo install --git https://github.com/coral-xyz/anchor anchor-cli --locked --force anchor init my-anchor-cpi-program cd my-anchor-cpi-programanchor-langandsolana-program.
Key Techniques
You master CPI by understanding how to construct and execute instructions, manage accounts, and handle signers for both native Rust and Anchor programs.
1. Basic CPI with invoke (Native Rust)
You use solana_program::program::invoke to call another program's instruction when the invoking program itself doesn't need to sign for any of the accounts in the target instruction. This is common for simple transfers or interactions where all necessary signatures come from external users.
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke,
pubkey::Pubkey,
system_instruction,
};
// Assuming you have an instruction that takes a system program and two accounts for a transfer
pub fn process_transfer(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let sender_account = next_account_info(accounts_iter)?;
let receiver_account = next_account_info(accounts_iter)?;
let system_program_account = next_account_info(accounts_iter)?;
// Validate sender is signer and writable, receiver is writable
if !sender_account.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
if !sender_account.is_writable || !receiver_account.is_writable {
return Err(ProgramError::InvalidAccountData);
}
// Create the CPI instruction: a system transfer
let transfer_instruction = system_instruction::transfer(
sender_account.key,
receiver_account.key,
amount,
);
// Prepare accounts for the CPI call
let cpi_accounts = [
sender_account.clone(),
receiver_account.clone(),
];
// Invoke the system program to perform the transfer
invoke(
&transfer_instruction,
&cpi_accounts,
)?;
Ok(())
}
2. CPI with invoke_signed (Native Rust)
When your program needs to act as a signer for an instruction it's invoking (e.g., a Program Derived Address (PDA) needs to sign a transfer or a token mint), you use solana_program::program::invoke_signed. You provide the signer_seeds that were used to derive the PDA.
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke_signed,
pubkey::Pubkey,
system_instruction,
};
pub fn process_pda_transfer(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let pda_account = next_account_info(accounts_iter)?; // PDA that will sign
let receiver_account = next_account_info(accounts_iter)?;
let system_program_account = next_account_info(accounts_iter)?;
// Derive PDA seeds (must match how the PDA was created)
let pda_seeds = b"my_pda_seed";
let (expected_pda_key, bump_seed) =
Pubkey::find_program_address(&[pda_seeds], program_id);
// Validate PDA
if *pda_account.key != expected_pda_key {
return Err(ProgramError::InvalidSeeds);
}
if !pda_account.is_writable || !receiver_account.is_writable {
return Err(ProgramError::InvalidAccountData);
}
// Create the CPI instruction
let transfer_instruction = system_instruction::transfer(
pda_account.key,
receiver_account.key,
amount,
);
// Prepare accounts for CPI
let cpi_accounts = [
pda_account.clone(),
receiver_account.clone(),
];
// Signer seeds for the PDA
let signer_seeds: &[&[&[u8]]] = &[&[pda_seeds, &[bump_seed]]];
// Invoke the system program, signed by the PDA
invoke_signed(
&transfer_instruction,
&cpi_accounts,
signer_seeds,
)?;
Ok(())
}
3. Anchor CPI with CpiContext
Anchor significantly simplifies CPI by abstracting away much of the boilerplate for account marshalling and signer management using CpiContext. You define the accounts required by the target instruction within a struct and pass it to Anchor's cpi:: helper functions.
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Transfer};
#[program]
pub mod my_cpi_program {
use super::*;
pub fn invoke_token_transfer(ctx: Context<InvokeTokenTransfer>, amount: u64) -> Result<()> {
// Create the CpiContext for the SPL Token transfer
let cpi_accounts = Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(), // The signer for 'from' account
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
// Perform the SPL Token transfer via CPI
token::transfer(cpi_ctx, amount)?;
Ok(())
}
pub fn invoke_pda_token_transfer(ctx: Context<InvokePdaTokenTransfer>, amount: u64) -> Result<()> {
// Define the seeds for the PDA that will sign
let pda_seeds = b"vault";
let bump = *ctx.bumps.get("vault_account").ok_or(ProgramError::InvalidSeeds)?; // Get bump from Anchor context
let signer_seeds: &[&[&[u8]]] = &[
pda_seeds,
&[bump],
];
let cpi_accounts = Transfer {
from: ctx.accounts.vault_account.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.vault_account.to_account_info(), // PDA is the authority
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
token::transfer(cpi_ctx, amount)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct InvokeTokenTransfer<'info> {
#[account(mut)]
pub from: Account<'info, token::TokenAccount>,
#[account(mut)]
pub to: Account<'info, token::TokenAccount>,
pub authority: Signer<'info>, // Must be the owner/authority of the 'from' token account
pub token_program: Program<'info, token::Token>,
}
#[derive(Accounts)]
pub struct InvokePdaTokenTransfer<'info> {
#[account(
mut,
seeds = [b"vault"],
bump,
token::mint = mint_account, // Optional: constraint for the mint
token::authority = vault_account, // PDA is its own authority
)]
pub vault_account: Account<'info, token::TokenAccount>,
#[account(mut)]
pub to: Account<'info, token::TokenAccount>,
pub mint_account: Account<'info, token::Mint>,
pub token_program: Program<'info, token::Token>,
pub system_program: Program<'info, System>,
}
4. CPI with Account Validation and Constraints
Regardless of whether you use native Rust or Anchor, you must validate the accounts passed to your program before using them in a CPI. This prevents malicious users from substituting incorrect or unauthorized accounts. Anchor's #[account] constraints simplify this significantly.
// Example using Anchor constraints for validation (from previous examples)
#[derive(Accounts)]
pub struct InvokePdaTokenTransfer<'info> {
#[account(
mut, // Must be mutable
seeds = [b"vault"], // Validates the PDA derived from these seeds
bump, // Validates the bump seed
token::mint = mint_account, // Ensures this token account is for the specified mint
token::authority = vault_account, // Ensures the vault PDA is the authority of this token account
)]
pub vault_account: Account<'info, token::TokenAccount>,
#[account(mut)] // Must be mutable
pub to: Account<'info, token::TokenAccount>,
pub mint_account: Account<'info, token::Mint>,
pub token_program: Program<'info, token::Token>,
pub system_program: Program<'info, System>,
}
// Native Rust validation example (excerpt from Basic CPI)
// In process_transfer function:
// Validate sender is signer and writable, receiver is writable
if !sender_account.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
if !sender_account.is_writable || !receiver_account.is_writable {
return Err(ProgramError::InvalidAccountData);
}
// You would add more comprehensive checks:
// - Check account.owner == &system_program::ID for SystemProgram accounts
// - Check data length for specific account types
// - Check rent_exempt status if transferring lamports
Best Practices
- Always Validate All Accounts: Before initiating any CPI, meticulously validate every account passed to your instruction. Verify their
owner,is_signerstatus, and data layout.
Anti-Patterns
-
Arbitrary CPI Target Programs. Allowing callers to specify the target program ID for cross-program invocations without whitelisting enables malicious program substitution that can steal funds.
-
Missing PDA Signer Seed Verification. Performing CPI with PDA signers without verifying that the seeds used to derive the PDA match the expected canonical derivation allows forged PDA signatures.
-
Ignoring CPI Depth Limits. Building composable programs without accounting for Solana's 4-level CPI depth limit causes runtime failures when programs are composed beyond the expected depth.
-
Passing Unvalidated Accounts Through CPI. Forwarding accounts received from callers directly to inner programs without ownership and data validation trusts the caller to provide correct accounts.
-
No Error Propagation from CPI Results. Ignoring or silently swallowing error codes returned from cross-program invocations hides failures that leave the calling program in an inconsistent state.
Install this skill directly: skilldb add solana-ecosystem-skills
Related Skills
Anchor Framework Deep
Anchor is a framework for Solana smart contract development that provides a set of tools, macros, and an Interface Definition Language (IDL) to simplify writing secure and efficient on-chain programs.
Solana Account Model
This skill covers the fundamental architecture of Solana's account model, explaining how data is stored, owned, and accessed on the blockchain.
Solana Blinks Actions
This skill covers the end-to-end process of creating interactive Solana Blinks (Blockchain Links) that enable users to initiate on-chain actions directly from URLs. You learn to define blink metadata, handle dynamic parameters, construct serialized transactions on your backend, and integrate these frictionless interactions into any web or social platform.
Solana DEFI Protocols
This skill covers the strategies and technical patterns for interacting with established DeFi protocols on Solana, including Automated Market Makers (AMMs), lending/borrowing platforms, and liquid staking solutions.
Solana NFT Metaplex
This skill covers the end-to-end process of creating, managing, and distributing NFTs on Solana using the Metaplex protocol suite, including Token Metadata, Candy Machine, and Auction House.
Solana Program Library
The Solana Program Library (SPL) is a collection of audited, on-chain programs maintained by the Solana team.