Anchor
Introduction to Anchor

Introduction to Anchor

Accounts

We saw the #[account] macro, but naturally on solana there are different type of accounts. For this reason is worth taking a moment to see how generally accounts on Solana work, but more in depth, how they work with Anchor.

General Overview

On Solana, every piece of state lives in an account; picture the ledger as one giant table where each row shares the same base schema:

pub struct Account {
    /// lamports in the account
    pub lamports: u64,
    /// data held in this account
    #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
    pub data: Vec<u8>,
    /// the program that owns this account and can mutate its lamports or data.
    pub owner: Pubkey,
    /// `true` if the account is a program; `false` if it merely belongs to one
    pub executable: bool,
    /// the epoch at which this account will next owe rent (currently deprecated and is set to `0`)
    pub rent_epoch: Epoch,
}

All accounts on Solana share the same base layout. What sets them apart is:

  1. The owner: The program that has exclusive rights to modify the account's data and lamports.
  2. The data: Used by the owner program to distinguish between different account types.

When we talk about Token Program Accounts, what we mean is an account where the owner is the Token Program. Unlike a System Account whose data field is empty, a Token Program Account can be either a Mint or a Token account. We use discriminators to distinguish between them.

Just as the Token Program can own accounts, so can any other program even our own.

Program Accounts

Program accounts are the foundation of state management in Anchor programs. They allow you to create custom data structures that are owned by your program. Let's explore how to work with them effectively.

Account Structure and Discriminators

Every program account in Anchor needs a way to identify its type. This is handled through discriminators, which can be either:

  1. Default Discriminators: An 8-byte prefix generated using sha256("account:<StructName>")[0..8] for accounts, or sha256("global:<instruction_name>")[0..8] for instructions. The seeds use PascalCase for accounts and snake_case for instructions.
Anchor Discriminator Calculator
Account
sha256("account:" + PascalCase(seed))[0..8]
[0, 0, 0, 0, 0, 0, 0, 0]
  1. Custom Discriminators: Starting with Anchor v0.31.0, you can specify your own discriminator:
#[account(discriminator = 1)]              // single-byte
pub struct Escrow { … }

Important Notes about Discriminators:

  • They must be unique across your program
  • Using [1] prevents using [1, 2, …] as these also start with 1
  • [0] cannot be used as it conflicts with uninitialized accounts

Creating Program Accounts

To create a program account, you first define your data structure:

use anchor_lang::prelude::*;
 
#[derive(InitSpace)]
#[account(discriminator = 1)]
pub struct CustomAccountType {
    data: u64,
}

Key points about program accounts:

  • Maximum size is 10,240 bytes (10 KiB)
  • For larger accounts, you'll need zero_copy and chunked writes
  • The InitSpace derive macro automatically calculates the required space
  • Total space = INIT_SPACE + DISCRIMINATOR.len()

The total space in bytes needed for the account is the sum of INIT_SPACE (size of all the fields combined) and the discriminator size (DISCRIMINATOR.len()).

Solana accounts require a rent deposit in lamports, which depends on the size of the account. Knowing the size helps us calculate how many lamports we need to deposit to make the account open.

Here's how we're going to initiate the account in our Account struct:

#[account(
    init,
    payer = <target_account>,
    space = <num_bytes>                 // CustomAccountType::INIT_SPACE + CustomAccountType::DISCRIMINATOR.len(),
)]
pub account: Account<'info, CustomAccountType>,

Here are some of the fields used in the #[account] macro, beyond the seeds and bump fields that we have already covered, and what they do:

  • init: tells Anchor to create the account
  • payer: which signer funds the rent (here, the maker)
  • space: how many bytes to allocate. This is where the rent calculation happens as well

After creation, you can modify the account's data. If you need to change its size, use reallocation:

#[account(
    mut,                       // Mark as mutable
    realloc = <space>,         // New size
    realloc::payer = <target>, // Who pays for the change
    realloc::zero = <bool>     // Whether to zero new space
)]

Note: When reducing account size, set realloc::zero = true to ensure old data is properly cleared.

Lastly, when the account is no longer needed, we can close it to recover rent:

#[account(
    mut,                       // Mark as mutable
    close = <target_account>,  // Where to send remaining lamports
)]
pub account: Account<'info, CustomAccountType>,

We then can add PDAs, deterministic addresses derived from seeds and a program ID that are particularly useful for creating predictable account addresses, into these constraints like this:

#[account(
    seeds = <seeds>,            // Seeds for derivation
    bump                        // Standard bump seed
)]
pub account: Account<'info, CustomAccountType>,

Note: that PDAs are deterministic: same seeds + program + bump always produce the same address and that the bump ensures the address is off the ed25519 curve

Since the calculating the bump can "burn" a lot of CUs, it's always good to save it into the account or pass it into the instruction and validate it without having to calculate like this:

#[account(
    seeds = <seeds>,
    bump = <expr>
)]
pub account: Account<'info, CustomAccountType>,

And it's possible to derive a PDA that is derived from another program by passing the address of the program is derived from like this:

#[account(
    seeds = <seeds>,
    bump = <expr>,
    seeds::program = <expr>
)]
pub account: Account<'info, CustomAccountType>,

Token Accounts

The Token Program, part of the Solana Program Library (SPL), is the built-in toolkit for minting and moving any asset that isn't native SOL. It has instructions to create tokens, mint new supply, transfer balances, burn, freeze, and more.

This program owns two key account types:

  • Mint Account: stores the metadata for one specific token: supply, decimals, mint authority, freeze authority, and so on
  • Token Account: holds a balance of that mint for a particular owner. Only the owner can reduce the balance (transfer, burn, etc.), but anyone can send tokens to the account, increasing its balance

Token Accounts in Anchor

Natively, the core Anchor crate only bundles CPI and Accounts helpers for the System Program. If you want the same hand-holding for SPL tokens you pull in the anchor_spl crate.

anchor_spl adds:

  • Helper builders for every instruction in both the SPL Token and Token-2022 programs
  • Type wrappers that make it painless to verify and deserialize Mint and Token accounts

Let's look at how the Mint and Token accounts are structured:

#[account(
    mint::decimals     = <expr>,
    mint::authority    = <target_account>,
    mint::freeze_authority = <target_account>
    mint::token_program = <target_account>
)]
pub mint: Account<'info, Mint>,
 
#[account(
    mut,
    associated_token::mint       = <target_account>,
    associated_token::authority  = <target_account>,
    associated_token::token_program = <target_account>
)]
pub maker_ata_a: Account<'info, TokenAccount>,

Account<'info, Mint> and Account<'info, TokenAccount> tell Anchor to:

  • confirm the account really is a Mint or Token account
  • deserialize its data so you can read fields directly
  • enforce any extra constraints you specify (authority, decimals, mint, token_program, etc.)

These token-related accounts follow the same init pattern used earlier. Since Anchor knows their fixed byte size, we don't need to specify a space value, only the payer funding the account.

Anchor also offers init_if_needed macro: it checks whether the token account already exists and, if not, creates it. That shortcut isn't safe for every account type, but it's perfectly suited to token accounts, so we'll rely on it here.

As mentioned, anchor_spl creates helpers for both the Token and Token2022 programs, with the latter introducing Token Extensions. The main challenge is that even though these accounts achieve similar goals and have comparable structures, they can't be deserialized and checked the same way since they're owned by two different programs.

We could create more "advanced" logic to handle these different account types, but fortunately Anchor supports this scenario through InterfaceAccounts:

use anchor_spl::token_interface::{Mint, TokenAccount};
 
#[account(
    mint::decimals     = <expr>,
    mint::authority    = <target_account>,
    mint::freeze_authority = <target_account>
)]
pub mint: InterfaceAccounts<'info, Mint>,
 
#[account(
    mut,
    associated_token::mint = <target_account>,
    associated_token::authority = <target_account>,
    associated_token::token_program = <target_account>
)]
pub maker_ata_a: InterfaceAccounts<'info, TokenAccount>,

The key difference here is that we're using InterfaceAccounts instead of Account. This allows our program to work with both Token and Token2022 accounts without needing to handle the differences in their deserialization logic. The interface provides a common way to interact with both types of accounts while maintaining type safety and proper validation.

This approach is particularly useful when you want your program to be compatible with both token standards, as it eliminates the need to write separate logic for each program. The interface handles all the complexity of dealing with different account structures behind the scenes.

Additional Accounts Type

Naturally, System Accounts, Program Accounts and Token Accounts are not the only types of account that we can have in anchor. So we're going to see here other types of Account that we can have:

Signer

The Signer type is used when you need to verify that an account has signed a transaction. This is crucial for security as it ensures that only authorized accounts can perform certain actions. You'll use this type whenever you need to guarantee that a specific account has approved a transaction, such as when transferring funds or modifying account data that requires explicit permission. Here's how you can use it:

#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
    #[account(mut)]
    pub signer: Signer<'info>,
}

The Signer type automatically checks if the account has signed the transaction. If it hasn't, the transaction will fail. This is particularly useful when you need to ensure that only specific accounts can perform certain operations.

AccountInfo & UncheckedAccount

AccountInfo and UncheckedAccount are low-level account types that provide direct access to account data without automatic validation. They are identical in functionality, but UncheckedAccount is the preferred choice as its name better reflects its purpose.

These types are useful in three main scenarios:

  1. Working with accounts that lack a defined structure
  2. Implementing custom validation logic
  3. Interacting with accounts from other programs that don't have Anchor type definitions

Since these types bypass Anchor's safety checks, they are inherently unsafe and require explicit acknowledgment using the /// CHECK comment. This comment serves as documentation that you understand the risks and have implemented appropriate validation.

Here's an example of how to use them:

#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
    /// CHECK: This is an unchecked account
    pub account: UncheckedAccount<'info>,
    
    /// CHECK: This is an unchecked account
    pub account_info: AccountInfo<'info>,
}

Option

The Option type in Anchor allows you to make accounts optional in your instruction. When an account is wrapped in Option, it can either be provided or omitted in the transaction. This is particularly useful for:

  • Building flexible instructions that can work with or without certain accounts
  • Implementing optional parameters that may not always be needed
  • Creating backward-compatible instructions that can work with new or old account structures

When an Option account is set to None, Anchor will use the Program ID as the account address. This behavior is important to understand when working with optional accounts.

Here's how to implement it:

#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
    pub optional_account: Option<Account<'info, CustomAccountType>>,
}

Box

The Box type is used to store accounts on the heap rather than the stack. This is necessary in several scenarios:

  • When dealing with large account structures that would be inefficient to store on the stack
  • When working with recursive data structures
  • When you need to work with accounts that have a size that can't be determined at compile time

Using Box helps manage memory more efficiently in these cases by allocating the account data on the heap. Here's an example:

#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
    pub boxed_account: Box<Account<'info, LargeAccountType>>,
}

Program

The Program type is used to validate and interact with other Solana programs. Anchor can easily identify program accounts because they have their executable flag set to true. This type is particularly useful when:

  • You need to make Cross-Program Invocations (CPIs)
  • You want to ensure you're interacting with the correct program
  • You need to verify program ownership of accounts

There are two main ways to use the Program type:

  1. Using built-in program types (recommended when available):
use anchor_spl::token::Token;
 
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
}
  1. Using a custom program address when the program type isn't available:
// Address of the Program
const PROGRAM_ADDRESS: Pubkey = pubkey!("22222222222222222222222222222222222222222222")
 
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
    #[account(address = PROGRAM_ADDRESS)]
    /// CHECK: this is fine since we're checking the address
    pub program: UncheckedAccount<'info>,
}

Note: When working with token programs, you might need to support both the Legacy Token Program and Token-2022 Program. In such cases, use the Interface type instead of Program:

use anchor_spl::token_interface::TokenInterface;
 
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
    pub program: Interface<'info, TokenInterface>,
}

Custom Account Validation

Anchor provides a powerful set of constraints that can be applied directly in the #[account] attribute. These constraints help ensure account validity and enforce program rules at the account level, before your instruction logic runs. Here are the available constraints:

Address Constraint

The address constraint verifies that an account's public key matches a specific value. This is essential when you need to ensure you're interacting with a known account, such as a specific PDA or a program account:

#[account(
    address = <expr>,                    // Basic usage
    address = <expr> @ CustomError       // With custom error
)]
pub account: Account<'info, CustomAccountType>,

Owner Constraint

The owner constraint ensures that an account is owned by a specific program. This is a critical security check when working with program-owned accounts, as it prevents unauthorized access to accounts that should be managed by a particular program:

#[account(
    owner = <expr>,                      // Basic usage
    owner = <expr> @ CustomError         // With custom error
)]
pub account: Account<'info, CustomAccountType>,

Executable Constraint

The executable constraint verifies that an account is a program account (has its executable flag set to true). This is particularly useful when making Cross-Program Invocations (CPIs) to ensure you're interacting with a program rather than a data account:

#[account(executable)]
pub account: Account<'info, CustomAccountType>,

Mutable Constraint

The mut constraint marks an account as mutable, allowing its data to be modified during the instruction. This is required for any account that will be updated, as Anchor enforces immutability by default for safety:

#[account(
    mut,                                 // Basic usage
    mut @ CustomError                    // With custom error
)]
pub account: Account<'info, CustomAccountType>,

Signer Constraint

The signer constraint verifies that an account has signed the transaction. This is crucial for security when an account needs to authorize an action, such as transferring funds or modifying data. It's a more explicit way to require signatures compared to using the Signer type:

#[account(
    signer,                              // Basic usage
    signer @ CustomError                 // With custom error
)]
pub account: Account<'info, CustomAccountType>,

Has One Constraint

The has_one constraint verifies that a specific field on the account struct matches another account's public key. This is useful for maintaining relationships between accounts, such as ensuring a token account belongs to the correct owner:

#[account(
    has_one = data @ Error::InvalidField
)]
pub account: Account<'info, CustomAccountType>,

Custom Constraint

When the built-in constraints don't meet your needs, you can write a custom validation expression. This allows for complex validation logic that can't be expressed with other constraints, such as checking account data length or validating relationships between multiple fields:

#[account(
    constraint = data == account.data @ Error::InvalidField
)]
pub account: Account<'info, CustomAccountType>,

These constraints can be combined to create powerful validation rules for your accounts. By placing validation at the account level, you keep your security checks close to the account definitions and avoid scattering require!() calls throughout your instruction logic.

Contents
View Source
Blueshift © 2025Commit: cc5f933