Skip to content

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,
}
  1. Click "Build" to compile
  2. Click "Deploy" to deploy to devnet
  3. Use the "Test" tab to call initialize with a message
  4. Call update to 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


Further Reading