Anchor v2 alpha is here! Up to 95% smaller binaries, 3.0 to 50.4× fewer CU
Anchor Docs
A lone boy hauls an anchor on the shore, evoking the weight of maritime destiny and Homer's mastery of atmosphere and technique.
Boy with Anchor, Winslow Homer, 1873
Overview

Fundamentals

Handlers, accounts, validation, PDAs, IDL, and CPI in Anchor programs.

Program structure

Learn about the structure of Anchor programs, including declare_id!(), #[program], #[derive(Accounts)], and #[account].

The Anchor framework uses Rust macros to reduce boilerplate and enforce the checks every Solana program needs. Four macros do most of the work.

  • declare_id!() sets the program’s on-chain address.
  • #[program] annotates the module containing the program’s instruction handlers.
  • #[derive(Accounts)] declares the accounts an instruction requires.
  • #[account] defines a custom account data type owned by the program.

The default scaffold splits these pieces across a few files:

  • programs/
    • counter/
      • src/
        • instructions/One file per instruction
          • initialize.rs
        • constants.rs
        • error.rs
        • instructions.rs Re-exports instruction modules
        • lib.rs Declares the program module
        • state.rs Program-owned account data

For a small program, the pieces read more clearly as a single file. The example below exposes one initialize() instruction that creates a counter account and stores an authority in it.

programs/counter/src/lib.rs
use anchor_lang_v2::prelude::*;
declare_id!("3ynNB373Q3VAzKp7m4x238po36hjAGFXFJB4ybN2iTyg");
#[program]
pub mod counter {
use super::*;
pub fn initialize(ctx: &mut Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = *ctx.accounts.payer.address();
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {
#[account(mut)]
pub payer: Signer,
#[account(init, payer = payer)]
pub counter: Account<Counter>,
pub system_program: Program<System>,
}
#[account]
pub struct Counter {
pub count: u64,
pub authority: Address,
}

From those ordinary Rust items, the framework produces the Solana entrypoint, account parser, IDL metadata, and client-facing instruction builders.

declare_id!()#

The declare_id!() macro embeds the program’s on-chain address, known as the program ID, into the compiled binary.

declare_id!("3ynNB373Q3VAzKp7m4x238po36hjAGFXFJB4ybN2iTyg");

By default, the program ID is the public key of the keypair at target/deploy/<program_name>-keypair.json. To rewrite the declare_id!() string to match the on-disk keypair, run:

Terminal window
anchor keys sync
Tip (When to run $ anchor keys sync)

Run anchor keys sync after cloning a repository or regenerating a program keypair. Syncing prevents a mismatch between the declared ID and the program keypair used during deployment.

#[program]#

The #[program] attribute annotates the module that contains the program’s instruction handlers. Every public function inside this module becomes an instruction callable from clients.

#[program]
pub mod counter {
use super::*;
pub fn initialize(ctx: &mut Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = *ctx.accounts.payer.address();
Ok(())
}
}

The first parameter of every instruction handler is a Context<T>, where T is a struct that implements account loading and validation. Any additional parameters become instruction arguments supplied by the client.

pub fn set_count(ctx: &mut Context<SetCount>, count: u64) -> Result<()> {
ctx.accounts.counter.count = count;
Ok(())
}

#[program] also generates the instruction dispatcher. By default, each handler gets the first 8 bytes of sha256("global:" + handler_name) as its discriminator. CU-sensitive programs can use compact one-byte discriminators:

#[program]
pub mod vault {
use super::*;
#[discrim = 0]
pub fn deposit(ctx: &mut Context<Deposit>, amount: u64) -> Result<()> {
Ok(())
}
#[discrim = 1]
pub fn withdraw(ctx: &mut Context<Withdraw>, amount: u64) -> Result<()> {
Ok(())
}
}

If one handler in the module uses #[discrim = N], every handler in that module must declare a unique byte discriminator.

Context<T>#

Context<T> exposes everything the handler needs from the runtime beyond its own instruction arguments.

Field or methodWhat it contains
ctx.program_idThe program id passed to the entrypoint.
ctx.accountsThe validated T accounts struct.
ctx.bumpsA typed bumps struct generated from PDA fields.
ctx.remaining_accounts()Lazily materialized extra AccountView values.

Access each account through ctx.accounts.<field_name>. The field has already been loaded and validated before the handler starts.

#[derive(Accounts)]#

The #[derive(Accounts)] macro generates account loading and validation for an instruction.

#[derive(Accounts)]
pub struct Initialize {
#[account(mut)]
pub payer: Signer,
#[account(init, payer = payer)]
pub counter: Account<Counter>,
pub system_program: Program<System>,
}

Each field maps to one account the instruction requires. Field names are arbitrary, but they should describe the role the account plays in the instruction.

Account validation is the first line of defense against malicious input. Anchor enforces two complementary mechanisms that are usually used together.

  • Account constraints declared in the #[account(...)] attribute enforce runtime conditions on each account. Common constraints include init, mut, seeds, bump, and constraint.
  • Account types specified in the field’s type enforce static expectations about what kind of account the client must pass. Examples include Account<T>, BorshAccount<T>, Signer, Program<T>, and UncheckedAccount.

Once validation succeeds, the handler accesses accounts through ctx.accounts.<field_name>.

#[account]#

The #[account] attribute turns a plain struct into an Anchor-managed account type.

#[account]
pub struct Counter {
pub count: u64,
pub authority: Address,
}

In v2, the default account form is fixed-size and zero-copy. #[account] gives Counter a stable #[repr(C)] layout, derives the required Pod traits, adds an Anchor discriminator, and implements ownership metadata for the declaring program.

For a normal program-owned account, the on-chain layout is:

[8-byte discriminator][repr(C) Counter]

Use Account<T> for fixed-size Pod-backed state. Use BorshAccount<T> when the account needs variable-length fields such as Vec, String, or payload enums.

Important (Choosing the account model)

Account<T> is zero-copy and Pod-backed by default. For variable-length data, mark the type with #[account(borsh)] and use BorshAccount<T> in the accounts struct.

Generated modules#

The program macro emits generated surfaces that clients and tests rely on:

  • program::instruction::* structs that implement InstructionData
  • program::accounts::* structs that implement ToAccountMetas
  • typed Bumps for each accounts struct
  • IDL print hooks behind idl-build
  • program::cpi::* wrappers behind the cpi feature

These modules are compile-time output from the macros, not source files checked into the program. Tests and CPI callers can still import them as normal Rust modules because the macro expands them into the crate.

#![no_std] by default#

anchor-lang-v2 is #![no_std]. The default alloc feature is on, so user programs can still use Vec, String, and any generated macro code that needs allocation. Disabling alloc is possible, but most programs should keep the default feature set.

Accounts and context

Context<T>, typed bumps, remaining accounts, and account wrapper access.

Handlers do not receive raw Solana accounts directly. Anchor first loads the account list into the struct named in Context<T>. If validation succeeds, the handler receives that typed context and can use fields by name.

Every handler receives a mutable context:

pub fn initialize(ctx: &mut Context<Initialize>) -> Result<()> {
ctx.accounts.counter.bump = ctx.bumps.counter;
Ok(())
}

The mutable reference lets validation hooks and account-exit logic share state with the handler. Account wrappers load from pinocchio account views that are scoped at the handler boundary.

In the example above, Initialize is the #[derive(Accounts)] struct for the instruction. ctx.accounts.counter is already loaded as an Account<Counter>, so the handler can update count without manually checking the owner, discriminator, or writable bit.

Context fields#

Context<T> gives the handler:

Field or methodWhat it contains
ctx.program_idThe program id passed to the entrypoint.
ctx.accountsThe validated T accounts struct.
ctx.bumpsA typed bumps struct generated from PDA fields.
ctx.remaining_accounts()Lazily materialized extra AccountView values.

PDA bumps are generated as typed fields:

let bump = ctx.bumps.vault;

Optional accounts produce optional bumps:

#[derive(Accounts)]
pub struct MaybeProfile {
pub user: Signer,
#[account(seeds = [b"profile", user.address().as_ref()], bump)]
pub profile: Option<Account<Profile>>,
}
pub fn handle(ctx: &mut Context<MaybeProfile>) -> Result<()> {
if let Some(bump) = ctx.bumps.profile {
msg!("profile present");
}
Ok(())
}

Remaining accounts#

ctx.remaining_accounts() returns a Vec<AccountView>. It walks the cursor only on first access and caches the result for later calls in the same instruction.

Use remaining accounts for variable-length account lists, such as multisig signers. Prefer declared fields whenever the list length is fixed, because declared fields get generated IDL metadata, account resolution, and duplicate-mut checks.

Remaining accounts are intentionally less structured. Reach for them when the number of accounts is part of the instruction data or when the list is truly open-ended. If a client can know the account at build time, make it a named field instead.

Address access#

Most wrappers expose address() directly:

let signer = ctx.accounts.authority.address();

For raw pinocchio views, use account().address():

let account = ctx.accounts.data.account();
let address = account.address();

Use wrapper accessors when possible so code stays tied to the validated account type.

Account validation

How #[derive(Accounts)] loads accounts, runs constraints, and rejects duplicate mutable aliases.

#[derive(Accounts)] is where Anchor validation happens. It describes the accounts an instruction expects and the checks Anchor should run before the handler starts.

It is also what keeps handler code small. Instead of opening AccountInfo values by hand and checking owners, signers, PDAs, and account data manually, you write those requirements on fields:

#[derive(Accounts)]
pub struct Create {
#[account(mut)]
pub payer: Signer,
#[account(init, payer = payer, seeds = [b"config", payer.address().as_ref()], bump)]
pub config: Account<Config>,
pub system_program: Program<System>,
}

For each accounts struct, the macro emits:

  • a TryAccounts impl that walks account views and loads wrappers
  • typed bumps
  • a MUT_MASK for duplicate mutable account detection
  • account-meta builders for clients and tests
  • IDL account metadata under idl-build

Field order matters. Clients build the instruction in the order declared, and Anchor validates fields in that same order, stores PDA bumps in ctx.bumps, then calls the handler with the loaded account struct. If a constraint expression references a sibling field, put that sibling earlier in the struct so it is already loaded.

Built-in constraints#

Common constraints include:

ConstraintEffect
mutRequires the account to be writable before loading it mutably.
signerRequires is_signer. Usually prefer the Signer wrapper.
initCreates and initializes the account. Requires payer.
init_if_neededCreates only when the account is absent.
payer = accountFunds init or init_if_needed.
space = exprSize to allocate for account creation.
seeds = [...], bumpVerifies a PDA and records the bump.
bump = exprVerifies with an explicit stored bump.
address = exprRequires the account address to equal an expression.
owner = exprRequires the account owner to equal an expression.
close = destTransfers lamports and zeros data on successful exit.
realloc = exprResizes account data when account-resize is enabled.

has_one is still accepted but deprecated. Put address = parent.field on the sibling account instead.

Custom constraints#

Namespaced constraints are dispatched through AccountConstraint<A>. A downstream crate can implement a marker such as token::mint or my_ns::min_value without changing the core derive.

The lifecycle has four hooks:

HookWhen it runs
initAfter account creation.
checkDuring normal validation.
updateInside an update(...) constraint clause.
exitDuring successful account exit.

See Extensibility and Account constraints for the trait shape.

Duplicate mutable protection#

Anchor rejects duplicate mutable account aliases by default. The derive generates a 256-bit mutable-field mask and combines it with a runtime bitvec as accounts are loaded. If two mutable fields point at the same address, validation returns ConstraintDuplicateMutableAccount.

Opt out with unsafe(dup) only when the handler is written so it never holds conflicting mutable references:

#[derive(Accounts)]
pub struct TouchTwice {
#[account(mut, unsafe(dup))]
pub first: Account<Data>,
#[account(mut, unsafe(dup))]
pub second: Account<Data>,
}

The opt-out must be deliberate because aliasing mutable account data is a common source of handler bugs.

PDAs and resolution

Learn how to use Program Derived Addresses, PDA constraints, generated bumps, resolved accounts, and IDL seed metadata.

Definition (Program Derived Address)

A Program Derived Address (PDA) is a Solana account address derived deterministically from a list of seeds and a program ID. PDAs are off the Ed25519 curve, so they have no associated private key. The owning program can sign for them through CPI signer seeds.

Program Derived Addresses give programs a way to create accounts whose addresses are predictable from program-defined inputs. Anchor expresses PDA derivation through account constraints.

Anchor PDA constraints#

The seeds constraint lists the values used to derive the PDA. Seeds can be static byte strings, account addresses, account data, or instruction arguments. The bump constraint finds the bump seed that pushes the address off the Ed25519 curve.

#[derive(Accounts)]
pub struct Initialize {
#[account(mut)]
pub payer: Signer,
#[account(init, payer = payer, seeds = [b"counter", payer.address().as_ref()], bump)]
pub counter: Account<Counter>,
pub system_program: Program<System>,
}
Note

The seeds and bump constraints are used together for normal PDA validation.

The generated bumps struct stores the canonical bump as a typed field:

ctx.accounts.counter.bump = ctx.bumps.counter;

Seed examples#

Use an empty array to define a PDA without optional seeds:

#[derive(Accounts)]
pub struct ReadGlobal {
#[account(seeds = [], bump)]
pub global: Account<Global>,
}

Pass a single byte literal to derive a PDA from a fixed value:

#[derive(Accounts)]
pub struct ReadConfig {
#[account(seeds = [b"config"], bump)]
pub config: Account<Config>,
}

Use multiple seeds when the PDA should depend on a user, mint, market, or instruction-specific value:

#[derive(Accounts)]
pub struct ReadProfile {
pub user: Signer,
#[account(seeds = [b"profile", user.address().as_ref()], bump)]
pub profile: Account<Profile>,
}

The example above uses both a static seed (b"profile") and a dynamic seed (the user’s address).

If all seeds are byte literals, Anchor can run PDA discovery at macro expansion time and bake the result into generated code:

#[account(seeds = [b"global-config"], bump)]
pub config: Account<Config>,

Seeds that depend on runtime accounts or instruction arguments still use the runtime path.

Explicit bumps#

When bump is used without a value, Anchor finds the canonical bump during validation. That is the simplest form and is the right default while creating an account.

#[account(seeds = [b"counter", payer.address().as_ref()], bump)]
pub counter: Account<Counter>,

An account can also store its bump and use that value later:

#[account(seeds = [b"counter"], bump = counter.bump)]
pub counter: Account<Counter>,

By storing the bump value in account data, the program avoids a runtime search when reloading the account.

PDA creation#

The init constraint is commonly used with seeds and bump to create a new account whose address is a PDA. Under the hood, init invokes the System Program to create the account.

#[derive(Accounts)]
pub struct CreateProfile {
#[account(mut)]
pub payer: Signer,
#[account(init, payer = payer, seeds = [b"profile", payer.address().as_ref()], bump)]
pub profile: Account<Profile>,
pub system_program: Program<System>,
}

payer specifies who funds the account creation. For fixed-size Account<T> data, v2 can derive the allocation size from the account wrapper’s Space implementation.

seeds::program#

The seeds::program constraint derives a PDA against another program ID. Use it when an instruction needs to validate PDA accounts created by another program.

#[derive(Accounts)]
pub struct ReadExternal {
#[account(
seeds = [b"external"],
bump,
seeds::program = other_program.address(),
)]
pub external: UncheckedAccount,
pub other_program: Program<OtherProgram>,
}

Resolved account structs#

For client and test code, #[derive(Accounts)] emits account-meta builders. Full structs include every account. Resolved structs include only fields the caller must provide while PDAs and well-known programs are derived or filled in.

let metas = multisig_v2::accounts::CreateResolved {
creator: creator.pubkey(),
}
.to_account_metas(None);

If Create contains a config PDA derived from creator and a system_program, the resolved form supplies only creator. The builder derives config and inserts the System Program account in the right position.

PDA seeds in the IDL#

Anchor emits PDA metadata into the IDL when it can inspect the seed list. Array-form seeds get the richest output:

#[account(seeds = [b"profile", user.address().as_ref(), slug.as_ref()], bump)]
pub profile: Account<Profile>,

For array-form seeds, the macro can classify literals as const seeds, sibling accounts as account seeds, and instruction args as arg seeds. Const-evaluable expressions such as constant paths are rendered through the IDL build helper when they implement AsRef<[u8]>.

Opaque expression seeds are valid for runtime validation, but client-side auto-derivation has less information:

#[account(seeds = Profile::seeds(user.address()), bump)]
pub profile: Account<Profile>,

Use array-form seeds for accounts that generated clients should auto-resolve. Use expression-form seeds when the runtime abstraction matters more than generated client metadata.

IDL

Learn how Anchor emits IDL metadata for instructions, accounts, types, events, constants, and account resolution.

An Interface Description Language (IDL) file for an Anchor program is a standardized JSON file describing the program’s instructions, accounts, types, events, constants, and errors. Client code generators consume it to produce typed bindings without reading the Rust source.

Tip (Where to find the IDL)

The anchor build command writes the IDL to target/idl/<program-name>.json and the generated TypeScript type file to target/types/<program-name>.ts.

Anchor builds the IDL from the same Rust items used by the program. The emission path is trait-driven. Account wrappers and type wrappers expose metadata through IdlAccountType and IdlType under the idl-build feature.

idl-build pulls in IDL assembly machinery and should not be shipped in production SBF binaries.

Program instructions#

The instructions array in the IDL maps directly to the public functions inside the #[program] module. Each entry records the accounts and arguments the instruction requires.

The program exposes an initialize() instruction with one argument and an Initialize accounts struct:

#[program]
pub mod counter {
use super::*;
pub fn initialize(ctx: &mut Context<Initialize>, start: u64) -> Result<()> {
ctx.accounts.counter.count = start;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {
#[account(mut)]
pub payer: Signer,
#[account(init, payer = payer)]
pub counter: Account<Counter>,
pub system_program: Program<System>,
}

The IDL captures the instruction name, discriminator, account list, and argument list:

{
"name": "initialize",
"discriminator": [175, 175, 109, 31, 13, 152, 155, 237],
"accounts": [
{ "name": "payer", "writable": true, "signer": true },
{ "name": "counter", "writable": true },
{ "name": "systemProgram", "address": "11111111111111111111111111111111" }
],
"args": [{ "name": "start", "type": "u64" }]
}

A generated client uses the IDL to build the transaction. The instruction name, args, and accounts in the builder mirror the IDL fields:

const signature = await program.methods
.initialize(new BN(42))
.accounts({
payer: wallet.publicKey,
counter: counter.publicKey,
})
.signers([counter])
.rpc()

Program accounts#

The accounts array in the IDL maps to structs annotated with #[account]. These structs define account data created and owned by the program.

The program defines a fixed-size Counter account:

#[account]
pub struct Counter {
pub count: u64,
pub authority: Address,
}

The IDL records the account name and discriminator under accounts, and its field layout under types:

{
"accounts": [
{
"name": "Counter",
"discriminator": [255, 176, 4, 245, 188, 253, 124, 25]
}
],
"types": [
{
"name": "Counter",
"type": {
"kind": "struct",
"fields": [
{ "name": "count", "type": "u64" },
{ "name": "authority", "type": "pubkey" }
]
}
}
]
}

The TypeScript client can fetch the account by public key and decode it using the IDL:

const counterAccount = await program.account.counter.fetch(counter)
console.log(counterAccount.count.toString())

Discriminators#

Anchor assigns a discriminator to every instruction and every Anchor account type. These bytes act as runtime identifiers so the program can dispatch the right instruction handler and so deserialization can verify the right account type.

By default, instruction discriminators are the first 8 bytes of sha256("global:" + instruction_name). Account discriminators are the first 8 bytes of sha256("account:" + struct_name).

CU-sensitive programs can opt into compact one-byte instruction discriminators with #[discrim = N]:

#[program]
pub mod vault {
use super::*;
#[discrim = 0]
pub fn deposit(ctx: &mut Context<Deposit>, amount: u64) -> Result<()> {
Ok(())
}
}

You will not normally interact with discriminators directly. Anchor inserts them on the wire and verifies them on read. The discriminator fields in the generated JSON come from these rules.

Account wrappers and IDL#

Core wrappers such as Signer, Program<T>, SystemAccount, UncheckedAccount, and Sysvar<T> surface account metadata through trait impls. SPL wrappers in anchor-spl-v2 do the same from the downstream crate.

Custom wrappers should implement the IDL traits so the generated account list remains meaningful to clients.

PDA seed metadata#

PDA seeds defined in seeds constraints can be included in the IDL. This lets generated clients auto-resolve accounts when the seed list is inspectable.

Array-form seeds get the richest output:

#[account(seeds = [b"vault", authority.address().as_ref()], bump)]
pub vault: Account<Vault>,

Opaque expression seeds still work at runtime, but the IDL cannot always describe how a client should reproduce them:

#[account(seeds = Vault::seeds(authority.address()), bump)]
pub vault: Account<Vault>,

Prefer array-form seeds for public instructions where generated clients should auto-derive PDAs.

TypeScript status#

The CLI builds IDL and TypeScript type files, but the TypeScript package is still alpha. Current scaffolds are pinned to @anchor-lang/core ^1.0.0 until a stable v2 package is published. Generated TypeScript client surfaces are active development, not a stable npm contract.

Cross-program invocation

Learn how to make CPIs with CpiContext, CpiHandle, generated CPI wrappers, and PDA signer seeds.

A Cross Program Invocation (CPI) is one program calling an instruction on another program. Composability on Solana is built on CPI. Token transfers, account creation through the System Program, and calls into another Anchor program all use it.

A CPI is constructed from the same three pieces as any Solana instruction:

  1. The program ID of the program being called.
  2. The accounts required by the instruction.
  3. Any instruction data required as arguments.

Anchor’s v2 CPI path keeps those pieces typed. CpiContext<T> bundles the target program address, typed CPI accounts, signer seeds, and remaining accounts. CpiHandle<'a> carries account handles into the CPI without exposing the raw account view for accidental re-borrows.

System program CPI#

anchor_lang_v2::system_program provides typed wrappers for every System Program instruction. The wrappers take a CpiContext whose accounts struct holds CpiHandle<'a> values, so the call stays inside Anchor’s borrow-tracked model.

use anchor_lang_v2::system_program;
#[derive(Accounts)]
pub struct Deposit {
#[account(mut)]
pub payer: Signer,
#[account(mut)]
pub vault: SystemAccount,
pub system_program: Program<System>,
}
pub fn deposit(ctx: &mut Context<Deposit>, lamports: u64) -> Result<()> {
let cpi = CpiContext::new(
ctx.accounts.system_program.address(),
system_program::Transfer {
from: ctx.accounts.payer.cpi_handle_mut(),
to: ctx.accounts.vault.cpi_handle_mut(),
},
);
system_program::transfer(cpi, lamports)
}

The module exposes typed wrappers for the System Program instructions:

WrappersUse for
transfer, create_account, allocate, assignOrdinary signed account creation and lamport movement.
*_with_seed variants of eachCases where the base address is owned by another program and the resulting account address is derived deterministically.
The nonce-account familyNonce account creation, withdrawal, authorization, and advance.

SPL token CPI#

anchor-spl-v2 exposes account structs and helper functions for common SPL Token instructions. The example below transfers tokens with transfer_checked.

The instruction needs a source token account, mint, destination token account, authority, and token program:

#[derive(Accounts)]
pub struct TransferTokens {
pub mint: Account<Mint>,
#[account(
mut,
token::mint = mint,
token::authority = authority,
)]
pub from: Account<TokenAccount>,
#[account(mut, token::mint = mint)]
pub to: Account<TokenAccount>,
pub authority: Signer,
pub token_program: Program<Token>,
}

The handler builds the CPI accounts, wraps them in CpiContext, then calls the SPL helper:

use anchor_spl_v2::token::{self, cpi as token_cpi};
pub fn transfer_tokens(
ctx: &mut Context<TransferTokens>,
amount: u64,
decimals: u8,
) -> Result<()> {
let accounts = token_cpi::accounts::TransferChecked {
from: ctx.accounts.from.cpi_handle_mut(),
mint: ctx.accounts.mint.cpi_handle(),
to: ctx.accounts.to.cpi_handle_mut(),
authority: ctx.accounts.authority.cpi_handle(),
};
let cpi = CpiContext::new(ctx.accounts.token_program.address(), accounts);
token_cpi::transfer_checked(cpi, amount, decimals)
}

The account constraints protect the CPI from mismatched accounts before the Token Program runs. token::mint = mint ensures both token accounts belong to the expected mint, and token::authority = authority ensures the signer controls the source account.

Generated Anchor CPI#

When one Anchor program calls another Anchor program, the callee can expose generated CPI wrappers behind the cpi feature. A caller can then use the callee’s program::cpi::accounts::* structs and wrapper functions.

let cpi_accounts = callee::cpi::accounts::SetData {
data: ctx.accounts.callee_data.cpi_handle_mut(),
authority: ctx.accounts.authority.cpi_handle(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.callee_program.address(), cpi_accounts);
callee::cpi::set_data(cpi_ctx, value)?;

The generated CPI account structs contain one CpiHandle<'a> per account and implement ToCpiAccounts<'a>. Wrapper functions pack instruction args and call CpiContext::invoke.

Note (Current caveat)

Generated CPI account wrappers currently skip accounts structs that contain Option<_> or Nested<_>. Those shapes need extra flattening and fallback logic. Runtime validation still supports them. This caveat is only about generated CPI wrappers.

PDA signer seeds#

A program can sign a CPI for a PDA derived from its own program ID. Program-owned vaults, authorities, and other accounts with no private key rely on this.

Attach signer seeds with with_signer() when a PDA authorizes the CPI:

let signer_seeds: &[&[&[u8]]] = &[&[
b"vault-authority",
ctx.accounts.mint.address().as_ref(),
&[ctx.bumps.vault_authority],
]];
let cpi = CpiContext::new(ctx.accounts.token_program.address(), accounts)
.with_signer(signer_seeds);

The outer slice can hold multiple signer PDAs. Each inner slice is the ordered seed list for one PDA, including its bump.

BorshAccount<T> re-entry#

If a CPI mutates an account that the current handler holds as BorshAccount<T>, release and reacquire the borrow around the call:

ctx.accounts.profile.release_borrow()?;
some_program::cpi::update_profile(cpi_ctx)?;
ctx.accounts.profile.reacquire_borrow_mut(ctx.program_id)?;

Account<T> does not need this because its zero-copy fields read from the underlying buffer directly.

CpiHandle<'a>#

CpiHandle<'a> is intentionally narrow:

  • account.cpi_handle() creates a read-only handle
  • account.cpi_handle_mut() creates a writable handle and requires the account to be marked mut
  • handle.address() returns the account address
  • the handle does not deref to AccountView

That missing Deref is intentional. It prevents accidental use with checked pinocchio CPI builders that would re-borrow the same account views.

CpiContext::invoke(data) uses pinocchio’s invoke_signed_unchecked. The path stays safe for ordinary typed CPI because CPI account structs hold CpiHandle<'a> values that borrow the typed account wrappers for the duration of the call.

Two layers catch aliasing:

  • Rust rejects concurrent typed mutable access at compile time.
  • pinocchio’s account borrow state catches stale raw-borrow cases after the CPI returns.
Esc

Start typing to search the docs.