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:
- The owner: The program that has exclusive rights to modify the account's data and lamports.
- 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:
- Default Discriminators: An 8-byte prefix generated using
sha256("account:<StructName>")[0..8]
for accounts, orsha256("global:<instruction_name>")[0..8]
for instructions. The seeds use PascalCase for accounts and snake_case for instructions.
- 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 with1
[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 accountpayer
: 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:
- Working with accounts that lack a defined structure
- Implementing custom validation logic
- 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:
- 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>,
}
- 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.