Module 4: Anchor Framework Foundation¶
Anchor is the dominant framework for Solana program development. It provides high-level abstractions, automatic serialization, and security checks that make building programs faster and safer.
Learning Objectives¶
By the end of this module, you will understand:
- Why Anchor vs native Solana development
- Project structure and CLI commands
- Account types and constraints
- Instruction handlers and contexts
- Error handling with
error_code! - Events and logging
- Testing with
anchor test
1. Why Anchor?¶
Native vs Anchor Comparison¶
Native Solana (low-level):
// Native: Manual everything
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
program_error::ProgramError,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let account = next_account_info(accounts_iter)?;
// Manual validation
if account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
if !account.is_writable {
return Err(ProgramError::InvalidAccountData);
}
// Manual deserialization
let mut data = account.try_borrow_mut_data()?;
let mut counter = u64::from_le_bytes(data[0..8].try_into().unwrap());
counter += 1;
data[0..8].copy_from_slice(&counter.to_le_bytes());
Ok(())
}
Anchor (high-level):
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod counter {
use super::*;
pub fn increment(ctx: Context<Increment>) -> Result<()> {
ctx.accounts.counter.count += 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
}
#[account]
pub struct Counter {
pub count: u64,
}
What Anchor Provides¶
| Feature | Benefit |
|---|---|
#[account] macro |
Auto Borsh serialization + discriminator |
#[derive(Accounts)] |
Automatic account validation |
Constraints (#[account(mut, seeds = [...])]) |
Declarative security |
| Type-safe accounts | Compile-time checks |
| IDL generation | TypeScript client auto-generation |
| Built-in testing | anchor test command |
| CPI helpers | Easy cross-program calls |
2. Project Structure¶
Creating a New Project¶
# Create new Anchor workspace
anchor init my_program
cd my_program
# Project structure:
my_program/
├── Anchor.toml # Workspace config
├── Cargo.toml # Rust workspace
├── package.json # Node dependencies
├── programs/
│ └── my_program/
│ ├── Cargo.toml # Program dependencies
│ └── src/
│ └── lib.rs # Program code
├── tests/
│ └── my_program.ts # TypeScript tests
├── target/ # Build artifacts
│ ├── deploy/ # .so files
│ ├── idl/ # JSON interface
│ └── types/ # TypeScript types
└── migrations/
└── deploy.ts # Deploy scripts
Anchor.toml¶
[features]
seeds = false
skip-lint = false
[programs.localnet]
my_program = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
[programs.devnet]
my_program = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "Localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
CLI Commands¶
# Build program
anchor build
# Run tests
anchor test
# Deploy to configured cluster
anchor deploy
# Generate IDL only
anchor idl parse -f programs/my_program/src/lib.rs
# Start local validator with program
anchor localnet
# Upgrade existing program
anchor upgrade target/deploy/my_program.so --program-id <PROGRAM_ID>
3. Program Structure¶
Basic Program¶
use anchor_lang::prelude::*;
// Program ID declaration
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
// Program module - contains instruction handlers
#[program]
pub mod my_program {
use super::*;
// Instruction: Initialize a new counter
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.authority = ctx.accounts.authority.key();
counter.count = 0;
Ok(())
}
// Instruction: Increment the counter
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter.count.checked_add(1).unwrap();
Ok(())
}
// Instruction with arguments
pub fn set_count(ctx: Context<SetCount>, new_count: u64) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = new_count;
Ok(())
}
}
// Account validation structs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + Counter::INIT_SPACE
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
}
#[derive(Accounts)]
pub struct SetCount<'info> {
#[account(
mut,
has_one = authority
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
// Account data structure
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub authority: Pubkey, // 32 bytes
pub count: u64, // 8 bytes
}
The Discriminator¶
Every Anchor account has an 8-byte discriminator (hash of the account name):
┌────────────────────────────────────────────────┐
│ Account Data Layout │
├────────────────────────────────────────────────┤
│ [8 bytes] discriminator │
│ [32 bytes] authority (Pubkey) │
│ [8 bytes] count (u64) │
└────────────────────────────────────────────────┘
Total: 48 bytes
The discriminator ensures: 1. Account is the correct type 2. Account was created by this program 3. Protection against account confusion attacks
4. Account Types¶
Core Account Types¶
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct MyInstruction<'info> {
// Typed account - deserialized automatically
#[account(mut)]
pub my_account: Account<'info, MyData>,
// Signer - must sign the transaction
pub authority: Signer<'info>,
// System account (wallet) - no data
#[account(mut)]
pub payer: SystemAccount<'info>,
// Program - verifies it's the correct program
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
// Unchecked account - raw AccountInfo
/// CHECK: This account is validated manually
pub unchecked: UncheckedAccount<'info>,
// Account info - lowest level
/// CHECK: Validated in instruction
pub raw_account: AccountInfo<'info>,
}
Account Type Reference¶
| Type | Use Case | Validations |
|---|---|---|
Account<'info, T> |
Your program's accounts | Owner, discriminator, deserialization |
Signer<'info> |
Transaction signers | Must be signer |
SystemAccount<'info> |
SOL wallets | Owner is System Program |
Program<'info, T> |
Program accounts | Executable, correct program ID |
UncheckedAccount<'info> |
External accounts | None (document with /// CHECK:) |
AccountInfo<'info> |
Raw access | None |
Box<Account<'info, T>> |
Large accounts | Same as Account, on heap |
5. Account Constraints¶
Initialization¶
#[derive(Accounts)]
pub struct Initialize<'info> {
// Create new account
#[account(
init,
payer = payer,
space = 8 + MyAccount::INIT_SPACE
)]
pub my_account: Account<'info, MyAccount>,
// Alternative: init_if_needed
#[account(
init_if_needed,
payer = payer,
space = 8 + MyAccount::INIT_SPACE
)]
pub maybe_account: Account<'info, MyAccount>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
PDAs (Program Derived Addresses)¶
#[derive(Accounts)]
#[instruction(user_id: u64)] // Access instruction args
pub struct CreateUserAccount<'info> {
// PDA with seeds
#[account(
init,
payer = authority,
space = 8 + UserAccount::INIT_SPACE,
seeds = [b"user", authority.key().as_ref()],
bump
)]
pub user_account: Account<'info, UserAccount>,
// PDA with dynamic seed
#[account(
init,
payer = authority,
space = 8 + UserStats::INIT_SPACE,
seeds = [b"stats", user_id.to_le_bytes().as_ref()],
bump
)]
pub user_stats: Account<'info, UserStats>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
// Accessing existing PDA
#[derive(Accounts)]
pub struct UpdateUser<'info> {
#[account(
mut,
seeds = [b"user", authority.key().as_ref()],
bump = user_account.bump // Store bump in account
)]
pub user_account: Account<'info, UserAccount>,
pub authority: Signer<'info>,
}
Common Constraints¶
#[derive(Accounts)]
pub struct TransferTokens<'info> {
// Must be mutable
#[account(mut)]
pub source: Account<'info, TokenAccount>,
// Must have matching field
#[account(
mut,
has_one = authority, // source.authority == authority.key()
)]
pub source_checked: Account<'info, TokenAccount>,
// Custom constraint
#[account(
constraint = source.amount >= 100 @ ErrorCode::InsufficientFunds
)]
pub constrained: Account<'info, TokenAccount>,
// Address constraint
#[account(
address = ADMIN_PUBKEY @ ErrorCode::InvalidAdmin
)]
pub admin: Signer<'info>,
// Owner constraint
#[account(
owner = token_program.key()
)]
pub token_account: Account<'info, TokenAccount>,
// Close account (return rent)
#[account(
mut,
close = destination
)]
pub to_close: Account<'info, MyAccount>,
#[account(mut)]
pub destination: SystemAccount<'info>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
Constraint Reference¶
| Constraint | Purpose |
|---|---|
mut |
Account will be modified |
init |
Create and initialize account |
init_if_needed |
Create only if doesn't exist |
seeds = [...] |
PDA derivation seeds |
bump |
PDA bump seed |
payer = x |
Who pays for account creation |
space = n |
Account size in bytes |
has_one = field |
account.field == field.key() |
constraint = expr |
Custom boolean check |
address = pubkey |
Must match specific address |
owner = pubkey |
Account owner check |
close = dest |
Close account, send rent to dest |
realloc = size |
Resize account |
6. Instructions with Arguments¶
Passing Arguments¶
#[program]
pub mod my_program {
use super::*;
// Simple arguments
pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
// Use amount directly
Ok(())
}
// Multiple arguments
pub fn create_order(
ctx: Context<CreateOrder>,
price: u64,
quantity: u64,
side: OrderSide,
) -> Result<()> {
let order = &mut ctx.accounts.order;
order.price = price;
order.quantity = quantity;
order.side = side;
Ok(())
}
// Complex arguments
pub fn update_config(
ctx: Context<UpdateConfig>,
new_config: ConfigParams,
) -> Result<()> {
ctx.accounts.config.params = new_config;
Ok(())
}
}
// Enum for instruction arguments
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq)]
pub enum OrderSide {
Buy,
Sell,
}
// Struct for complex arguments
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct ConfigParams {
pub fee_rate: u16,
pub max_amount: u64,
pub enabled: bool,
}
Accessing Args in Accounts Struct¶
#[derive(Accounts)]
#[instruction(amount: u64, recipient: Pubkey)] // Declare which args to access
pub struct Transfer<'info> {
#[account(
mut,
// Use instruction arg in constraint
constraint = source.amount >= amount @ ErrorCode::InsufficientFunds
)]
pub source: Account<'info, TokenAccount>,
#[account(
mut,
// Use instruction arg in seeds
seeds = [b"vault", recipient.as_ref()],
bump
)]
pub recipient_vault: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
}
7. Error Handling¶
Defining Errors¶
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Insufficient funds for this operation")]
InsufficientFunds,
#[msg("Invalid authority")]
InvalidAuthority,
#[msg("Account already initialized")]
AlreadyInitialized,
#[msg("Arithmetic overflow")]
Overflow,
#[msg("Invalid amount: must be greater than zero")]
InvalidAmount,
#[msg("Order has expired")]
OrderExpired,
// Error with dynamic data (logged, not in message)
#[msg("Balance too low")]
BalanceTooLow,
}
Using Errors¶
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
let account = &mut ctx.accounts.user_account;
// Using require! macro
require!(amount > 0, ErrorCode::InvalidAmount);
require!(
account.balance >= amount,
ErrorCode::InsufficientFunds
);
// Using require_keys_eq! for pubkey comparison
require_keys_eq!(
account.authority,
ctx.accounts.authority.key(),
ErrorCode::InvalidAuthority
);
// Using require_gt!, require_gte!, require_eq!, etc.
require_gte!(account.balance, amount, ErrorCode::InsufficientFunds);
// Manual error return
if amount > account.max_withdrawal {
return Err(ErrorCode::InvalidAmount.into());
}
// Using ok_or for Option
let checked_amount = amount
.checked_sub(fee)
.ok_or(ErrorCode::Overflow)?;
account.balance -= amount;
Ok(())
}
Error in Constraints¶
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(
mut,
has_one = authority @ ErrorCode::InvalidAuthority,
constraint = user_account.balance > 0 @ ErrorCode::InsufficientFunds
)]
pub user_account: Account<'info, UserAccount>,
pub authority: Signer<'info>,
}
8. Events¶
Defining Events¶
use anchor_lang::prelude::*;
#[event]
pub struct DepositEvent {
pub user: Pubkey,
pub amount: u64,
pub timestamp: i64,
}
#[event]
pub struct TradeEvent {
pub maker: Pubkey,
pub taker: Pubkey,
pub price: u64,
pub quantity: u64,
#[index] // Indexed for faster queries
pub market: Pubkey,
}
Emitting Events¶
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
let user_account = &mut ctx.accounts.user_account;
user_account.balance += amount;
// Emit event
emit!(DepositEvent {
user: ctx.accounts.authority.key(),
amount,
timestamp: Clock::get()?.unix_timestamp,
});
Ok(())
}
Listening to Events (TypeScript)¶
// Subscribe to events
const listener = program.addEventListener("DepositEvent", (event, slot) => {
console.log("Deposit:", event.user.toString(), event.amount.toString());
});
// Later: remove listener
program.removeEventListener(listener);
// Parse events from transaction
const tx = await connection.getTransaction(signature);
const events = program.coder.events.parse(tx.meta.logMessages);
9. Cross-Program Invocations (CPI)¶
Basic CPI¶
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
// Create CPI context
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(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
// Execute CPI
token::transfer(cpi_ctx, amount)?;
Ok(())
}
#[derive(Accounts)]
pub struct TransferTokens<'info> {
#[account(mut)]
pub from: Account<'info, TokenAccount>,
#[account(mut)]
pub to: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
CPI with PDA Signer¶
pub fn transfer_from_vault(ctx: Context<TransferFromVault>, amount: u64) -> Result<()> {
let vault = &ctx.accounts.vault;
// Seeds for PDA signing
let seeds = &[
b"vault",
vault.mint.as_ref(),
&[vault.bump],
];
let signer_seeds = &[&seeds[..]];
// CPI with signer seeds
let cpi_accounts = Transfer {
from: ctx.accounts.vault_token.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
authority: ctx.accounts.vault.to_account_info(),
};
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(())
}
10. Testing with Anchor¶
Basic Test Structure¶
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { MyProgram } from "../target/types/my_program";
import { expect } from "chai";
describe("my_program", () => {
// Configure the client
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.MyProgram as Program<MyProgram>;
it("Initializes counter", async () => {
const counter = anchor.web3.Keypair.generate();
await program.methods
.initialize()
.accounts({
counter: counter.publicKey,
authority: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([counter])
.rpc();
const account = await program.account.counter.fetch(counter.publicKey);
expect(account.count.toNumber()).to.equal(0);
expect(account.authority.toString()).to.equal(
provider.wallet.publicKey.toString()
);
});
it("Increments counter", async () => {
const counter = anchor.web3.Keypair.generate();
// Initialize first
await program.methods
.initialize()
.accounts({
counter: counter.publicKey,
authority: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([counter])
.rpc();
// Increment
await program.methods
.increment()
.accounts({
counter: counter.publicKey,
})
.rpc();
const account = await program.account.counter.fetch(counter.publicKey);
expect(account.count.toNumber()).to.equal(1);
});
});
Testing with PDAs¶
it("Creates user account PDA", async () => {
const [userPda, bump] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("user"), provider.wallet.publicKey.toBuffer()],
program.programId
);
await program.methods
.createUser("Alice")
.accounts({
userAccount: userPda,
authority: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const account = await program.account.userAccount.fetch(userPda);
expect(account.name).to.equal("Alice");
expect(account.bump).to.equal(bump);
});
Testing Errors¶
it("Fails with insufficient funds", async () => {
try {
await program.methods
.withdraw(new anchor.BN(1000000))
.accounts({
userAccount: userPda,
authority: provider.wallet.publicKey,
})
.rpc();
expect.fail("Should have thrown");
} catch (err) {
expect(err.error.errorCode.code).to.equal("InsufficientFunds");
}
});
// Alternative: using chai assertion
import { assert } from "chai";
it("Fails with invalid authority", async () => {
const wrongAuthority = anchor.web3.Keypair.generate();
await assert.isRejected(
program.methods
.withdraw(new anchor.BN(100))
.accounts({
userAccount: userPda,
authority: wrongAuthority.publicKey,
})
.signers([wrongAuthority])
.rpc(),
/InvalidAuthority/
);
});
Running Tests¶
# Run all tests
anchor test
# Run specific test file
anchor test --skip-build tests/my_program.ts
# Run with logs
anchor test -- --features debug
# Run on devnet
anchor test --provider.cluster devnet
Interactive: Hello Solana¶
Exercise: Build Your First Anchor Program
Open Solana Playground and create a new project:
use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");
#[program]
pub mod hello_solana {
use super::*;
pub fn initialize(ctx: Context<Initialize>, message: String) -> Result<()> {
let greeting = &mut ctx.accounts.greeting;
greeting.message = message;
greeting.author = ctx.accounts.author.key();
msg!("Greeting created: {}", greeting.message);
Ok(())
}
pub fn update(ctx: Context<Update>, new_message: String) -> Result<()> {
let greeting = &mut ctx.accounts.greeting;
greeting.message = new_message;
msg!("Greeting updated: {}", greeting.message);
Ok(())
}
}
#[derive(Accounts)]
#[instruction(message: String)]
pub struct Initialize<'info> {
#[account(
init,
payer = author,
space = 8 + 32 + 4 + 280 // discriminator + pubkey + string len + max string
)]
pub greeting: Account<'info, Greeting>,
#[account(mut)]
pub author: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Update<'info> {
#[account(mut, has_one = author)]
pub greeting: Account<'info, Greeting>,
pub author: Signer<'info>,
}
#[account]
pub struct Greeting {
pub author: Pubkey,
pub message: String,
}
- Click "Build" to compile
- Click "Deploy" to deploy to devnet
- Use the "Test" tab to call
initializewith a message - Call
updateto change the message
Summary¶
| Concept | Key Pattern |
|---|---|
| Program | #[program] module with instruction handlers |
| Accounts | #[derive(Accounts)] struct with constraints |
| Account Data | #[account] struct with InitSpace |
| PDAs | seeds = [...], bump constraints |
| Errors | #[error_code] enum with require! |
| Events | #[event] struct with emit! |
| CPI | CpiContext::new() or new_with_signer() |
| Testing | TypeScript with program.methods builder |
Self-Assessment¶
What does the 8-byte discriminator protect against?
It prevents account confusion attacks where an attacker might try to pass one account type as another. The discriminator is a hash of the account type name, so each type has a unique identifier that's checked on deserialization.
When would you use init_if_needed vs init?
Use init when the account should always be new (fail if exists). Use init_if_needed when you want to create the account only if it doesn't exist - useful for patterns like "get or create". Be careful: init_if_needed can have security implications if not used carefully.
How do you make a PDA sign a CPI?
Use CpiContext::new_with_signer() and provide the seeds array (including the bump) as signer seeds. The runtime verifies that your program could derive the PDA from those seeds, allowing the "signature".
Next Steps¶
You now have the foundation for building Solana programs with Anchor. Let's build our first complete DApp:
Module 5: Token & Escrow System