Cross-Program Invocations (CPIs)
A CPI is one program calling an instruction on another program, such as the Token Program or System Program. The caller passes the accounts the callee needs. When a PDA must authorise the call, the caller uses invoke_signed (or Anchor's CpiContext::new_with_signer) with the PDA's seeds, and the runtime treats the PDA as a signer for that call only.
Reference: Solana docs: Cross Program Invocation · Anchor docs: CPIs
Real code, line by line
6 excerpts from different projects. Each caption links the exact lines at the fork's pinned commit. The notes below each excerpt explain the lines that matter.
anchor/programs/valhalla/src/instructions/create.rs L231–256 @ 7aa9ddf · MIT · original to, authority, mint, }; let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); transfer_checked(cpi_ctx, amount, self.mint.decimals) } fn mint_governance_tokens(&self, bumps: &CreateVaultBumps) -> Result<()> { let signer_seeds: &[&[&[u8]]] = &[&[ constants::GOVERNANCE_TOKEN_MINT_SEED, &[bumps.governance_token_mint], ]]; let cpi_program = self.governance_token_program.to_account_info(); let cpi_accounts = MintTo { mint: self.governance_token_mint.to_account_info(), to: self.creator_governance_ata.to_account_info(), authority: self.governance_token_mint.to_account_info(), }; let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds); mint_to(cpi_context, self.config.governance_token_amount) }}- Line 235
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);: Makes a cross-program invocation into another program. - Line 237
transfer_checked(cpi_ctx, amount, self.mint.decimals): A checked token transfer, which also verifies the mint and decimals. - Line 247
let cpi_accounts = MintTo {: Mints new tokens. The mint authority must sign. - Line 252
let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
programs/kryptpay/src/instructions/swap_exact_tokens_for_tokens.rs L73–98 @ a528c51 · MIT · original ]; let signer_seeds = &[&authority_seeds[..]]; if swap_a { token::transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.trader_account_a.to_account_info(), to: ctx.accounts.pool_account_a.to_account_info(), authority: ctx.accounts.trader.to_account_info(), }, ), input, )?; token::transfer( CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.pool_account_b.to_account_info(), to: ctx.accounts.trader_account_b.to_account_info(), authority: ctx.accounts.pool_authority.to_account_info(), }, signer_seeds, ), output, )?;- Line 74
let signer_seeds = &[&authority_seeds[..]];: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 76
token::transfer(: An SPL token transfer through the Token Program. - Line 77
CpiContext::new(: Makes a cross-program invocation into another program. - Line 88
CpiContext::new_with_signer(: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
programs/poe/src/contexts/register_user.rs L44–64 @ 29a7c77 · MIT · original } pub fn mint_tokens(&mut self, bumps: &RegisterUserBumps) -> Result<()> { mint_to( CpiContext::new_with_signer( self.token_program.to_account_info(), MintTo { authority: self.mint.to_account_info(), to: self.token_account.to_account_info(), mint: self.mint.to_account_info(), }, &[&["poeken_mint".as_bytes(), &[bumps.mint]]], ), 1000 * 1000000000, )?; msg!("Minted tokens"); Ok(()) }}- Line 46
pub fn mint_tokens(&mut self, bumps: &RegisterUserBumps) -> Result<()> {: Mints new tokens. The mint authority must sign. - Line 48
CpiContext::new_with_signer(: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address. - Line 60
msg!("Minted tokens");: Logs to the program log, which is visible in explorers and the test output.
escrow/programs/mm_escrow/src/instructions/accept.rs L67–92 @ ca496be · Apache-2.0 · original &[self.escrow.bump], ]]; transfer( CpiContext::new_with_signer( self.token_program.to_account_info(), cpi_accounts, escrow_signer_seeds, ), x_requested, ) } pub fn transfer_y_to_seller_if_needed(&self, x_requested: u64, y_amount: u64) -> Result<()> { require!( check_price(x_requested, y_amount, self.escrow.y_price), Errors::PriceMismatch, ); let ixs = self.instructions.to_account_info(); let current_index = load_current_index_checked(&ixs)? as usize; if current_index == 0 { // wallet payment case self.transfer_y_to_seller(y_amount) } else { match load_instruction_at_checked(current_index - 1, &ixs) {- Line 67
&[self.escrow.bump],: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 71
CpiContext::new_with_signer(: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address. - Line 81
require!(: An explicit validation that aborts the instruction with an error.
anchor/programs/gree/src/lib.rs L52–77 @ 7ef8c89 · MIT · original payer: ctx.accounts.signer.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), rent: ctx.accounts.rent.to_account_info(), }; let cpi_context = CpiContext::new(ctx.accounts.token_metadata_program.to_account_info(), cpi_accounts); create_metadata_accounts_v3(cpi_context, data_v2, false, true, None)?; } // Create master edition with minimized context creation { let cpi_accounts = CreateMasterEditionV3 { edition: ctx.accounts.master_edition_account.to_account_info(), mint: ctx.accounts.mint.to_account_info(), update_authority: ctx.accounts.signer.to_account_info(), mint_authority: ctx.accounts.signer.to_account_info(), payer: ctx.accounts.signer.to_account_info(), metadata: ctx.accounts.metadata_account.to_account_info(), token_program: ctx.accounts.token_program.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), rent: ctx.accounts.rent.to_account_info(), }; let cpi_context = CpiContext::new(ctx.accounts.token_metadata_program.to_account_info(), cpi_accounts); create_master_edition_v3(cpi_context, None)?; } Ok(())- Line 56
let cpi_context = CpiContext::new(ctx.accounts.token_metadata_program.to_account_info(), c: Makes a cross-program invocation into another program.
anchor/programs/mons-dot-rehab/src/lib.rs L22–47 @ 0359070 · CC0-1.0 · original ctx.accounts.host.to_account_info().key, ctx.accounts.game.to_account_info().key, GAME_COST, ); invoke( &transfer_instruction, &[ ctx.accounts.host.to_account_info(), ctx.accounts.game.to_account_info(), ctx.accounts.system_program.to_account_info(), ], )?; Ok(()) } pub fn join_game(ctx: Context<JoinGame>, game_id: u64) -> Result<()> { let game = &mut ctx.accounts.game; require!(game.game_id == game_id, ErrorCode::GameNotFound); require!(game.guest_id == Pubkey::default(), ErrorCode::GameAlreadyJoined); game.guest_id = ctx.accounts.guest.key(); let transfer_instruction = system_instruction::transfer( ctx.accounts.guest.to_account_info().key, ctx.accounts.game.to_account_info().key,- Line 26
invoke(: Makes a cross-program invocation into another program. - Line 40
require!(game.game_id == game_id, ErrorCode::GameNotFound);: An explicit validation that aborts the instruction with an error.
Go further
- valhalla: explore the full project
- kryptpay-solana-contract: explore the full project
- poe-solana: explore the full project
- peer-pay: explore the full project
- gree: explore the full project
- mons-dot-rehab: explore the full project
- Next lesson: SPL Tokens: mints, accounts and transfers