Fundamentals
Learn how to use the Anchor framework to build secure Solana programs.
Before diving into Anchor, it’s recommended to have a basic understanding of Solana’s core concepts.
Program structure
IDL
Program derived addresses
Cross-program invocation
Program structure
Learn about the structure of Anchor programs, including key macros and their roles in simplifying Solana program development.
The Anchor framework uses Rust macros to reduce boilerplate and enforce the security checks every Solana program needs. Four macros form the backbone of any Anchor program.
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.
Example program#
The following program exposes a single initialize() instruction that creates a NewAccount and stores a u64 value in it. Every macro discussed in this page appears in the file:
use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");
#[program]mod hello_anchor { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.new_account.data = data; msg!("Changed data to: {}!", data); Ok(()) }}
#[derive(Accounts)]pub struct Initialize<'info> { #[account(init, payer = signer, space = 8 + 8)] pub new_account: Account<'info, NewAccount>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>,}
#[account]pub struct NewAccount { data: u64,}declare_id!()#
The declare_id!() macro embeds the program’s on-chain address, known as the program ID, into the compiled binary:
use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");24 collapsed lines
#[program]mod hello_anchor { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.new_account.data = data; msg!("Changed data to: {}!", data); Ok(()) }}
#[derive(Accounts)]pub struct Initialize<'info> { #[account(init, payer = signer, space = 8 + 8)] pub new_account: Account<'info, NewAccount>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>,}
#[account]pub struct NewAccount { data: u64,}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 anchor keys sync.
Tip (When to run anchor keys sync)
Run anchor keys sync after cloning a repository. The committed program ID will
not match the keypair generated by anchor build on your machine, so syncing
prevents a mismatch between the declared ID and the deployed binary.
#[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:
4 collapsed lines
use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");
#[program]mod hello_anchor { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.new_account.data = data; msg!("Changed data to: {}!", data); Ok(()) }}14 collapsed lines
#[derive(Accounts)]pub struct Initialize<'info> { #[account(init, payer = signer, space = 8 + 8)] pub new_account: Account<'info, NewAccount>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>,}
#[account]pub struct NewAccount { data: u64,}Instruction context#
The first parameter of every instruction handler is a Context<T>, where T is a struct that implements the Accounts trait. The context exposes everything the handler needs from the runtime beyond its own arguments:
pub struct Context<'a, 'b, 'c, 'info, T: Bumps> { /// Currently executing program id. pub program_id: &'a Pubkey, /// Deserialized accounts. pub accounts: &'b mut T, /// Remaining accounts given but not deserialized or validated. /// Be very careful when using this directly. pub remaining_accounts: &'c [AccountInfo<'info>], /// Bump seeds found during constraint validation. pub bumps: T::Bumps,}Access each field on the ctx parameter using dot notation:
ctx.accountscontains the deserialized, validated accounts declared byT.ctx.program_idis the public key of the currently executing program.ctx.remaining_accountsholds any accounts passed beyond theAccountsstruct. These are neither deserialized nor validated, so use with caution.ctx.bumpsexposes bump seeds discovered during PDA constraint validation.
Any additional parameters on the handler function become instruction arguments supplied by the client at invocation time. In the example, data: u64 is an argument:
7 collapsed lines
use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");
#[program]mod hello_anchor { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.new_account.data = data; msg!("Changed data to: {}!", data); Ok(()) }15 collapsed lines
}
#[derive(Accounts)]pub struct Initialize<'info> { #[account(init, payer = signer, space = 8 + 8)] pub new_account: Account<'info, NewAccount>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>,}
#[account]pub struct NewAccount { data: u64,}#[derive(Accounts)]#
The #[derive(Accounts)] macro generates the Accounts trait implementation for a struct, letting Anchor deserialize and validate every account passed into an instruction:
14 collapsed lines
use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");
#[program]mod hello_anchor { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.new_account.data = data; msg!("Changed data to: {}!", data); Ok(()) }}
#[derive(Accounts)]pub struct Initialize<'info> { #[account(init, payer = signer, space = 8 + 8)] pub new_account: Account<'info, NewAccount>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>,}5 collapsed lines
#[account]pub struct NewAccount { data: u64,}Each field maps to one account the instruction requires. Field names are arbitrary but should describe the role the account plays in the instruction.
Account validation#
Account validation is the first line of defense against malicious input. Anchor enforces two complementary mechanisms, typically used together.
- Account constraints declared in the
#[account(...)]attribute enforce runtime conditions on each account. Common constraints includeinit,mut,has_one,seeds, andconstraint. - Account types specified in the field’s type enforce static expectations about what kind of account the client must pass. Examples include
Account<T>,Signer,Program<T>, andUncheckedAccount.
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. The macro derives implementations for several traits, summarized in the full reference:
23 collapsed lines
use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");
#[program]mod hello_anchor { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.new_account.data = data; msg!("Changed data to: {}!", data); Ok(()) }}
#[derive(Accounts)]pub struct Initialize<'info> { #[account(init, payer = signer, space = 8 + 8)] pub new_account: Account<'info, NewAccount>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>,}
#[account]pub struct NewAccount { data: u64,}Accounts created with this type are owned by the program identified in declare_id!(). Anchor writes an 8-byte discriminator to the front of the account at initialization and checks it on every read, and serializes and deserializes the account’s data through Borsh.
Account discriminator#
The discriminator is the first 8 bytes of SHA256("account:<StructName>"), stored as the leading 8 bytes of every account created with this type (source). It serves two purposes.
- On initialization, Anchor writes the discriminator as the first 8 bytes of the account’s data.
- On deserialization, Anchor compares the stored discriminator against the one expected by the target type. A mismatch fails the instruction before the handler runs, preventing the client from substituting an account of the wrong type.
Important (Reserving discriminator space)
Every init constraint must allocate 8 extra bytes of space for the discriminator in addition to the account’s own fields. In the example below, space = 8 + 8 covers 8 bytes for the discriminator plus 8 bytes for the data: u64 field.
#[account(init, payer = signer, space = 8 + 8)]pub new_account: Account<'info, NewAccount>,IDL
Learn about the Interface Description Language (IDL) file in Anchor, its purpose, benefits, and how it simplifies program-client interactions.
An Interface Description Language (IDL) file for an Anchor program is a standardized JSON file describing the program’s instructions and accounts. It gives client code generators a consistent format to consume so they can produce typed bindings for the program without reading its Rust source.
Tip (Where the IDL lives)
The anchor build command writes the IDL to /target/idl/<program-name>.json.
The sections below show how the same program corresponds to its IDL and to a generated client.
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 below exposes a single initialize() instruction along with the Initialize accounts struct it expects:
use anchor_lang::prelude::*;
declare_id!("BYFW1vhC1ohxwRbYoLbAWs86STa25i9sD5uEusVjTYNd");
#[program]mod hello_anchor { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.new_account.data = data; msg!("Changed data to: {}!", data); Ok(()) }}
#[derive(Accounts)]pub struct Initialize<'info> { #[account(init, payer = signer, space = 8 + 8)] pub new_account: Account<'info, NewAccount>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>,}5 collapsed lines
#[account]pub struct NewAccount { data: u64,}The IDL captures that instruction in JSON, including its name, 8-byte discriminator, account list, and argument list:
{ "address": "BYFW1vhC1ohxwRbYoLbAWs86STa25i9sD5uEusVjTYNd",6 collapsed lines
"metadata": { "name": "hello_anchor", "version": "0.1.0", "spec": "0.1.0", "description": "Created with Anchor" }, "instructions": [ { "name": "initialize", "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], "accounts": [ { "name": "new_account", "writable": true, "signer": true }, { "name": "signer", "writable": true, "signer": true }, { "name": "system_program", "address": "11111111111111111111111111111111" } ], "args": [ { "name": "data", "type": "u64" } ] } ],20 collapsed lines
"accounts": [ { "name": "NewAccount", "discriminator": [176, 95, 4, 118, 91, 177, 125, 232] } ], "types": [ { "name": "NewAccount", "type": { "kind": "struct", "fields": [ { "name": "data", "type": "u64" } ] } } ]}A generated client uses the IDL to build and send the transaction. The fluent builder mirrors the IDL fields, with .initialize(data) carrying the args and .accounts({...}) supplying the account list:
import * as anchor from '@anchor-lang/core'import { Program, BN } from '@anchor-lang/core'import { HelloAnchor } from '../target/types/hello_anchor'import { Keypair } from '@solana/web3.js'import assert from 'assert'
describe('hello_anchor', () => { const provider = anchor.AnchorProvider.env() anchor.setProvider(provider) const wallet = provider.wallet as anchor.Wallet const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>
it('initialize', async () => { // Generate keypair for the new account const newAccountKp = new Keypair()
// Send transaction const data = new BN(42) const transactionSignature = await program.methods .initialize(data) .accounts({ newAccount: newAccountKp.publicKey, signer: wallet.publicKey, }) .signers([newAccountKp]) .rpc()
// Fetch the created account const newAccount = await program.account.newAccount.fetch(newAccountKp.publicKey)
console.log('Transaction signature: ', transactionSignature) console.log('On-chain data is:', newAccount.data.toString()) assert(data.eq(newAccount.data)) })})Program accounts#
The accounts array in the IDL maps to every struct annotated with #[account]. These structs define the data layout for accounts created and owned by the program.
The program defines a NewAccount struct with a single data: u64 field:
use anchor_lang::prelude::*;
declare_id!("BYFW1vhC1ohxwRbYoLbAWs86STa25i9sD5uEusVjTYNd");20 collapsed lines
#[program]mod hello_anchor { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.new_account.data = data; msg!("Changed data to: {}!", data); Ok(()) }}
#[derive(Accounts)]pub struct Initialize<'info> { #[account(init, payer = signer, space = 8 + 8)] pub new_account: Account<'info, NewAccount>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>,}
#[account]pub struct NewAccount { data: u64,}The IDL records the account’s name and 8-byte discriminator under accounts, and its field layout under types:
{ "address": "BYFW1vhC1ohxwRbYoLbAWs86STa25i9sD5uEusVjTYNd",34 collapsed lines
"metadata": { "name": "hello_anchor", "version": "0.1.0", "spec": "0.1.0", "description": "Created with Anchor" }, "instructions": [ { "name": "initialize", "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], "accounts": [ { "name": "new_account", "writable": true, "signer": true }, { "name": "signer", "writable": true, "signer": true }, { "name": "system_program", "address": "11111111111111111111111111111111" } ], "args": [ { "name": "data", "type": "u64" } ] } ], "accounts": [ { "name": "NewAccount", "discriminator": [176, 95, 4, 118, 91, 177, 125, 232] } ], "types": [ { "name": "NewAccount", "type": { "kind": "struct", "fields": [ { "name": "data", "type": "u64" } ] } } ]}The client fetches the account by public key and deserializes it into the typed shape declared in the IDL:
import * as anchor from '@anchor-lang/core'import { Program, BN } from '@anchor-lang/core'import { HelloAnchor } from '../target/types/hello_anchor'import { Keypair } from '@solana/web3.js'import assert from 'assert'
describe('hello_anchor', () => { const provider = anchor.AnchorProvider.env() anchor.setProvider(provider) const wallet = provider.wallet as anchor.Wallet const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>
it('initialize', async () => { // Generate keypair for the new account const newAccountKp = new Keypair()
// Send transaction const data = new BN(42) const transactionSignature = await program.methods .initialize(data) .accounts({ newAccount: newAccountKp.publicKey, signer: wallet.publicKey, }) .signers([newAccountKp]) .rpc()
// Fetch the created account const newAccount = await program.account.newAccount.fetch(newAccountKp.publicKey)
console.log('Transaction signature: ', transactionSignature) console.log('On-chain data is:', newAccount.data.toString()) assert(data.eq(newAccount.data)) })})Discriminators#
Anchor assigns a unique 8-byte discriminator to every instruction and every 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.
A discriminator is the first 8 bytes of SHA256("<prefix>:<name>"). The prefix is global for instructions and account for accounts. Since Anchor v0.30, every discriminator is included in the generated IDL.
Remark
You won’t normally interact with discriminators directly. Anchor inserts them on the wire and verifies them on read. This section is here for reference.
The instruction discriminator tells the program which handler to invoke. The Anchor client prepends it as the first 8 bytes of the instruction data:
"instructions": [ { "name": "initialize", "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], ... }]The discriminator is the first 8 bytes of SHA256("global:<InstructionName>"). For initialize:
sha256("global:initialize")The full hash, in hex:
af af 6d 1f 0d 98 9b ed d4 6a 95 07 32 81 ad c2 1b b5 e0 e1 d7 73 b2 fb bd 7a b5 04 cd d4 aa 30The leading 8 bytes become the discriminator:
af = 175af = 1756d = 1091f = 310d = 1398 = 1529b = 155ed = 237See the gen_discriminator source for the implementation.
The account discriminator identifies the account type during deserialization. Anchor writes it to the first 8 bytes of account data when the account is created and checks it on every read:
"accounts": [ { "name": "NewAccount", "discriminator": [176, 95, 4, 118, 91, 177, 125, 232] }]The discriminator is the first 8 bytes of SHA256("account:<StructName>"). For NewAccount:
sha256("account:NewAccount")The full hash, in hex:
b0 5f 04 76 5b b1 7d e8 a1 93 57 2a d3 5e b1 ae e5 f0 69 e2 09 7e 5c d2 64 56 55 2a cb 4a e9 57The leading 8 bytes become the discriminator:
b0 = 1765f = 9504 = 476 = 1185b = 91b1 = 1777d = 125e8 = 232See the account discriminator source for the implementation.
Note
Two programs that define accounts with the same struct name produce identical discriminators. Anchor avoids confusion at deserialization time by also verifying that the account is owned by the expected program.
Program derived addresses
Learn how to use Program Derived Addresses (PDAs) in Anchor programs to create deterministic account addresses.
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, which is what allows the owning program to sign for them through invoke_signed rather than a user-provided keypair.
Program Derived Addresses give programs a way to create accounts whose addresses are predictable from program-defined inputs.
Anchor PDA constraints#
PDA derivation in Anchor is expressed through account constraints. These constraints serve as security checks that ensure the correct address is derived.
The seeds constraint takes an array of optional seeds used to derive the PDA, which can be static values or dynamic references to account data. The bump constraint provides the bump seed that pushes the address off the Ed25519 curve so it’s a valid PDA. The optional seeds::program constraint specifies the program ID used for derivation and is only needed when deriving a PDA for a program other than the current one.
Note
The seeds and bump constraints are required to be used together.
Usage examples#
The seeds constraint specifies the optional values used to derive the PDA.
No optional seeds#
Use an empty array [] to define a PDA without optional seeds:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { #[account( seeds = [], bump, )] pub pda_account: SystemAccount<'info>,}Single static seed#
Pass a single byte literal to derive a PDA from a fixed value:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { #[account( seeds = [b"hello_world"], bump, )] pub pda_account: SystemAccount<'info>,}Multiple seeds and account references#
The seeds constraint accepts multiple values and can reference other account addresses or account data:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub signer: Signer<'info>, #[account( seeds = [b"hello_world", signer.key().as_ref()], bump, )] pub pda_account: SystemAccount<'info>,}The example above uses both a static seed (b"hello_world") and a dynamic seed (the signer’s public key).
The bump constraint specifies the bump seed used to derive the PDA.
Automatic bump calculation#
When bump is used without a value, the bump is automatically calculated each time the instruction is invoked:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { #[account( seeds = [b"hello_world"], bump, )] pub pda_account: SystemAccount<'info>,}Specify bump value#
The bump value can be provided explicitly, which is useful for optimizing compute unit usage. This assumes that the PDA account has been created and the bump seed is stored as a field on an existing account:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { #[account( seeds = [b"hello_world"], bump = pda_account.bump_seed, )] pub pda_account: Account<'info, CustomAccount>,}
#[account]pub struct CustomAccount { pub bump_seed: u8,}By storing the bump value in the account’s data, the program doesn’t need to recalculate it, saving compute units. The saved bump value can live on the account itself or on another account.
The seeds::program constraint specifies the program ID used to derive the PDA. Use it when an instruction needs to interact with PDA accounts created by another program:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { #[account( seeds = [b"hello_world"], bump, seeds::program = other_program.key(), )] pub pda_account: SystemAccount<'info>, pub other_program: Program<'info, OtherProgram>,}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 InstructionAccounts<'info> { #[account(mut)] pub signer: Signer<'info>, #[account( init, payer = signer, space = 8 + 1, seeds = [b"hello_world", signer.key().as_ref()], bump, )] pub pda_account: Account<'info, CustomAccount>, pub system_program: Program<'info, System>,}
#[account]pub struct CustomAccount { pub bump_seed: u8,}Note
The init constraint must be used with payer and space. The payer specifies the account that will pay for the account creation. The space specifies the bytes to allocate for the account (including the 8-byte discriminator).
PDA seeds in the IDL#
PDA seeds defined in the seeds constraint are included in the program’s IDL file. This allows the Anchor client to automatically resolve account addresses using these seeds when constructing instructions.
The program below defines a pda_account using a static seed (b"hello_world") and the signer’s public key as a dynamic seed:
use anchor_lang::prelude::*;
declare_id!("BZLiJ62bzRryYp9mRobz47uA66WDgtfTXhhgM25tJyx5");
#[program]mod hello_anchor { use super::*; pub fn test_instruction(ctx: Context<InstructionAccounts>) -> Result<()> { msg!("PDA: {}", ctx.accounts.pda_account.key()); Ok(()) }}
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub signer: Signer<'info>, #[account( seeds = [b"hello_world", signer.key().as_ref()], bump, )] pub pda_account: SystemAccount<'info>,}The IDL records the PDA seeds declared on the account. The static seed b"hello_world" is converted to byte values, and the dynamic seed is included as a reference to the signer account:
{ "address": "BZLiJ62bzRryYp9mRobz47uA66WDgtfTXhhgM25tJyx5",6 collapsed lines
"metadata": { "name": "hello_anchor", "version": "0.1.0", "spec": "0.1.0", "description": "Created with Anchor" }, "instructions": [ { "name": "test_instruction", "discriminator": [33, 223, 61, 208, 32, 193, 201, 79], "accounts": [ { "name": "signer", "signer": true }, { "name": "pda_account", "pda": { "seeds": [ { "kind": "const", "value": [104, 101, 108, 108, 111, 95, 119, 111, 114, 108, 100] }, { "kind": "account", "path": "signer" } ] } } ], "args": [] } ]}The Anchor client uses the IDL to automatically resolve the PDA address. The provider wallet acts as the signer, and its public key becomes the dynamic seed for derivation, so the client can call the instruction without explicitly deriving the PDA:
import * as anchor from '@anchor-lang/core'import { Program } from '@anchor-lang/core'import { HelloAnchor } from '../target/types/hello_anchor'
describe('hello_anchor', () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env())
const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>
it('Is initialized!', async () => { // Account address is automatically resolved using the IDL const tx = await program.methods.testInstruction().rpc() console.log('Your transaction signature', tx) })})When the instruction is invoked, the PDA is printed to program logs:
Program BZLiJ62bzRryYp9mRobz47uA66WDgtfTXhhgM25tJyx5 invoke [1]Program log: Instruction: TestInstructionProgram log: PDA: 3Hikt5mpKaSS4UNA5Du1TZJ8tp4o8VC8YWW6X9vtfVnJProgram BZLiJ62bzRryYp9mRobz47uA66WDgtfTXhhgM25tJyx5 consumed 18505 of 200000 compute unitsProgram BZLiJ62bzRryYp9mRobz47uA66WDgtfTXhhgM25tJyx5 successCross-program invocation
Learn how to implement Cross Program Invocations (CPIs) in Anchor programs to enable composability between different Solana programs.
A Cross Program Invocation (CPI) is one program calling an instruction on another program. This is how composability works on Solana: programs build on top of each other rather than reimplementing the same logic. The example below uses a SOL transfer to illustrate the mechanics, but the same pattern applies to any instruction on any program.
Cross program invocations#
The lib.rs file below defines a single sol_transfer() instruction. When invoked, it issues a CPI to the System Program’s transfer() instruction:
use anchor_lang::prelude::*;use anchor_lang::system_program::{transfer, Transfer};
declare_id!("9AvUNHjxscdkiKQ8tUn12QCMXtcnbR9BVGq3ULNzFMRi");
#[program]pub mod cpi { use super::*;
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> { let from_pubkey = ctx.accounts.sender.to_account_info(); let to_pubkey = ctx.accounts.recipient.to_account_info(); let program_id = ctx.accounts.system_program.to_account_info();
let cpi_context = CpiContext::new( program_id, Transfer { from: from_pubkey, to: to_pubkey, }, ); transfer(cpi_context, amount)?; Ok(()) }}
#[derive(Accounts)]pub struct SolTransfer<'info> { #[account(mut)] sender: Signer<'info>, #[account(mut)] recipient: SystemAccount<'info>, system_program: Program<'info, System>,}The cpi.test.ts file invokes sol_transfer() and logs a link to the transaction details on SolanaFM:
it('SOL Transfer Anchor', async () => { const transactionSignature = await program.methods .solTransfer(new BN(transferAmount)) .accounts({ sender: sender.publicKey, recipient: recipient.publicKey, }) .rpc()
console.log( `\nTransaction Signature:` + `https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`, )})The transaction details show the Anchor program invoked first (instruction 1), which then invokes the System Program (instruction 1.1):

Example explanation#
A CPI is constructed the same way as any other Solana instruction. The caller must specify:
- The program ID of the program being called.
- The accounts required by the instruction.
- Any instruction data required as arguments.
The System Program’s transfer() instruction requires a from account that sends the SOL and a to account that receives it. The SolTransfer struct declares both accounts plus the System Program itself, since the CPI invokes that program:
26 collapsed lines
use anchor_lang::prelude::*;use anchor_lang::system_program::{transfer, Transfer};
declare_id!("9AvUNHjxscdkiKQ8tUn12QCMXtcnbR9BVGq3ULNzFMRi");
#[program]pub mod cpi { use super::*;
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> { let from_pubkey = ctx.accounts.sender.to_account_info(); let to_pubkey = ctx.accounts.recipient.to_account_info(); let program_id = ctx.accounts.system_program.to_account_info();
let cpi_context = CpiContext::new( program_id, Transfer { from: from_pubkey, to: to_pubkey, }, ); transfer(cpi_context, amount)?; Ok(()) }}
#[derive(Accounts)]pub struct SolTransfer<'info> { #[account(mut)] sender: Signer<'info>, #[account(mut)] recipient: SystemAccount<'info>, system_program: Program<'info, System>,}The tabs below present three approaches to implementing the same CPI, each at a different level of abstraction:
The first approach uses Anchor’s CpiContext, which bundles the program_id and accounts required by the instruction. The cpi_context is then passed to the transfer() helper to invoke the instruction:
9 collapsed lines
use anchor_lang::prelude::*;use anchor_lang::system_program::{transfer, Transfer};
declare_id!("9AvUNHjxscdkiKQ8tUn12QCMXtcnbR9BVGq3ULNzFMRi");
#[program]pub mod cpi { use super::*;
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> { let from_pubkey = ctx.accounts.sender.to_account_info(); let to_pubkey = ctx.accounts.recipient.to_account_info(); let program_id = ctx.accounts.system_program.to_account_info();
let cpi_context = CpiContext::new( program_id, Transfer { from: from_pubkey, to: to_pubkey, }, ); transfer(cpi_context, amount)?; Ok(())11 collapsed lines
}}
#[derive(Accounts)]pub struct SolTransfer<'info> { #[account(mut)] sender: Signer<'info>, #[account(mut)] recipient: SystemAccount<'info>, system_program: Program<'info, System>,}The cpi_context variable specifies the program ID (System Program) and the accounts (sender and recipient) required by the transfer() instruction. The cpi_context and amount are then passed into transfer() to execute the CPI.
The next approach uses invoke() directly with system_instruction::transfer(), the pattern typically seen in native Rust programs. The first approach is an abstraction over this one:
use anchor_lang::solana_program::{program::invoke, system_instruction};
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> { let from_pubkey = ctx.accounts.sender.to_account_info(); let to_pubkey = ctx.accounts.recipient.to_account_info(); let program_id = ctx.accounts.system_program.to_account_info();
let instruction = &system_instruction::transfer(&from_pubkey.key(), &to_pubkey.key(), amount);
invoke(instruction, &[from_pubkey, to_pubkey, program_id])?; Ok(())}The instruction can also be built manually and passed into invoke(). This is useful when no helper crate exists for the target program. The caller must specify the AccountMeta entries and construct the instruction data buffer:
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> { let from_pubkey = ctx.accounts.sender.to_account_info(); let to_pubkey = ctx.accounts.recipient.to_account_info(); let program_id = ctx.accounts.system_program.to_account_info();
// Prepare instruction AccountMetas let account_metas = vec![ AccountMeta::new(from_pubkey.key(), true), AccountMeta::new(to_pubkey.key(), false), ];
// SOL transfer instruction discriminator let instruction_discriminator: u32 = 2;
// Prepare instruction data let mut instruction_data = Vec::with_capacity(4 + 8); instruction_data.extend_from_slice(&instruction_discriminator.to_le_bytes()); instruction_data.extend_from_slice(&amount.to_le_bytes());
// Create instruction let instruction = Instruction { program_id: program_id.key(), accounts: account_metas, data: instruction_data, };
// Invoke instruction invoke(&instruction, &[from_pubkey, to_pubkey, program_id])?; Ok(())}When building an instruction in Rust, use the following syntax to specify the AccountMeta for each account:
AccountMeta::new(account1_pubkey, true), // writable, signerAccountMeta::new(account2_pubkey, false), // writable, not signerAccountMeta::new_readonly(account3_pubkey, false), // not writable, not signerAccountMeta::new_readonly(account4_pubkey, true), // not writable, signerA reference program containing all three approaches is available on Solana Playground.
Cross program invocations with PDA signers#
A program can sign a CPI for a Program Derived Address (PDA) derived from its own program ID. The example below transfers SOL from a PDA to a recipient by passing the PDA’s seeds into the CPI.
The lib.rs file declares a single sol_transfer() instruction. The sender is a pda_account derived from the seeds b"pda" and the recipient’s address:
use anchor_lang::prelude::*;use anchor_lang::system_program::{transfer, Transfer};
declare_id!("3455LkCS85a4aYmSeNbRrJsduNQfYRY82A7eCD3yQfyR");
#[program]pub mod cpi { use super::*;
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> { let from_pubkey = ctx.accounts.pda_account.to_account_info(); let to_pubkey = ctx.accounts.recipient.to_account_info(); let program_id = ctx.accounts.system_program.to_account_info();
let seed = to_pubkey.key(); let bump_seed = ctx.bumps.pda_account; let signer_seeds: &[&[&[u8]]] = &[&[b"pda", seed.as_ref(), &[bump_seed]]];
let cpi_context = CpiContext::new( program_id, Transfer { from: from_pubkey, to: to_pubkey, }, ) .with_signer(signer_seeds);
transfer(cpi_context, amount)?; Ok(()) }}
#[derive(Accounts)]pub struct SolTransfer<'info> { #[account( mut, seeds = [b"pda", recipient.key().as_ref()], bump, )] pda_account: SystemAccount<'info>, #[account(mut)] recipient: SystemAccount<'info>, system_program: Program<'info, System>,}The cpi.test.ts file derives the PDA on the client, funds it, then invokes sol_transfer() to move SOL back to the wallet via a CPI signed by the program:
const [PDA] = PublicKey.findProgramAddressSync( [Buffer.from('pda'), wallet.publicKey.toBuffer()], program.programId,)
it('Fund PDA with SOL', async () => { const transferInstruction = SystemProgram.transfer({ fromPubkey: wallet.publicKey, toPubkey: PDA, lamports: transferAmount, })
const transaction = new Transaction().add(transferInstruction)
const transactionSignature = await sendAndConfirmTransaction( connection, transaction, [wallet.payer], // signer )
console.log( `\nTransaction Signature:` + `https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`, )})
it('SOL Transfer with PDA signer', async () => { const transactionSignature = await program.methods .solTransfer(new BN(transferAmount)) .accounts({ pdaAccount: PDA, recipient: wallet.publicKey, }) .rpc()
console.log( `\nTransaction Signature: https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`, )})The transaction details show the custom program invoked first (instruction 1), which then invokes the System Program (instruction 1.1):

Example explanation#
The sender is a PDA the program signs for. The seeds for the pda_account are the hardcoded string b"pda" and the address of the recipient, so each recipient gets a unique PDA:
32 collapsed lines
use anchor_lang::prelude::*;use anchor_lang::system_program::{transfer, Transfer};
declare_id!("3455LkCS85a4aYmSeNbRrJsduNQfYRY82A7eCD3yQfyR");
#[program]pub mod cpi { use super::*;
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> { let from_pubkey = ctx.accounts.pda_account.to_account_info(); let to_pubkey = ctx.accounts.recipient.to_account_info(); let program_id = ctx.accounts.system_program.to_account_info();
let seed = to_pubkey.key(); let bump_seed = ctx.bumps.pda_account; let signer_seeds: &[&[&[u8]]] = &[&[b"pda", seed.as_ref(), &[bump_seed]]];
let cpi_context = CpiContext::new( program_id, Transfer { from: from_pubkey, to: to_pubkey, }, ) .with_signer(signer_seeds);
transfer(cpi_context, amount)?; Ok(()) }}
#[derive(Accounts)]pub struct SolTransfer<'info> { #[account( mut, seeds = [b"pda", recipient.key().as_ref()], bump, )] pda_account: SystemAccount<'info>, #[account(mut)] recipient: SystemAccount<'info>, system_program: Program<'info, System>,}The tabs below present two approaches to implementing the same PDA-signed CPI:
The first approach uses CpiContext, then attaches the signer seeds via with_signer():
9 collapsed lines
use anchor_lang::prelude::*;use anchor_lang::system_program::{transfer, Transfer};
declare_id!("3455LkCS85a4aYmSeNbRrJsduNQfYRY82A7eCD3yQfyR");
#[program]pub mod cpi { use super::*;
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> { let from_pubkey = ctx.accounts.pda_account.to_account_info(); let to_pubkey = ctx.accounts.recipient.to_account_info(); let program_id = ctx.accounts.system_program.to_account_info();
let seed = to_pubkey.key(); let bump_seed = ctx.bumps.pda_account; let signer_seeds: &[&[&[u8]]] = &[&[b"pda", seed.as_ref(), &[bump_seed]]];
let cpi_context = CpiContext::new( program_id, Transfer { from: from_pubkey, to: to_pubkey, }, ) .with_signer(signer_seeds);
transfer(cpi_context, amount)?; Ok(())15 collapsed lines
}}
#[derive(Accounts)]pub struct SolTransfer<'info> { #[account( mut, seeds = [b"pda", recipient.key().as_ref()], bump, )] pda_account: SystemAccount<'info>, #[account(mut)] recipient: SystemAccount<'info>, system_program: Program<'info, System>,}The signer_seeds slice contains the seeds plus the bump_seed. The bump seed for a PDA validated by the seeds constraint is available via ctx.bumps followed by the field name.
When the CPI executes, the Solana runtime validates that the supplied seeds and the caller program ID derive a valid PDA, then adds that PDA as a signer on the invocation. This is the mechanism programs use to sign for PDAs derived from their program ID.
The first approach is a wrapper around invoke_signed() paired with system_instruction::transfer(). Calling invoke_signed() directly produces an equivalent CPI:
use anchor_lang::solana_program::{program::invoke_signed, system_instruction};
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> { let from_pubkey = ctx.accounts.pda_account.to_account_info(); let to_pubkey = ctx.accounts.recipient.to_account_info(); let program_id = ctx.accounts.system_program.to_account_info();
let seed = to_pubkey.key(); let bump_seed = ctx.bumps.pda_account;
let signer_seeds: &[&[&[u8]]] = &[&[b"pda", seed.as_ref(), &[bump_seed]]];
let instruction = &system_instruction::transfer(&from_pubkey.key(), &to_pubkey.key(), amount);
invoke_signed(instruction, &[from_pubkey, to_pubkey, program_id], signer_seeds)?; Ok(())}The signer_seeds are passed into invoke_signed() as the third argument.
A reference program containing both approaches is available on Solana Playground.