Skip to main content

How to Use Account Constraints in Your Solana Anchor Program

Created on
Updated on
Nov 26, 2024

4 min read

Overview

Anchor is a framework that accelerates building secure Rust programs on Solana. It provides a set of tools that allow you to write, test, and deploy your programs quickly and easily. The Accounts struct is a part of the Anchor framework in Solana programming that allows you to define the accounts that your program interacts with. When creating the Accounts struct, developers have the option to define certain constraints on the accounts to enforce certain conditions on the accounts being used (e.g., specify account ownership). This guide will explore how to define such constraints to help improve your program's security and code readability.

What You Will Need

Overview of Anchor Account Struct

Before digging into constraints, let's take a look at a simple example of Anchor's Account struct (src: anchor-lang.com):

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
mod hello_anchor {
use super::*;
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
ctx.accounts.my_account.data = data;
Ok(())
}
}

#[account]
#[derive(Default)]
pub struct MyAccount {
data: u64
}

#[derive(Accounts)]
pub struct SetData<'info> {
#[account(mut)]
pub my_account: Account<'info, MyAccount>
}

Account is a wrapper around AccountInfo that verifies program ownership and deserializes underlying data into a Rust type.

In the example code, #[derive(Accounts)] is a procedural macro that automatically generates a struct definition for the SetData struct, representing the accounts the set_data function requires as input. The #[derive] attribute in Rust is a language feature that allows you to automatically generate code based on the definition of a struct or enum. In this case, the #[derive(Accounts)] macro is provided by the anchor-lang crate, and it generates code for the SetData struct based on the attributes of its fields.

The SetData struct defines the accounts required by the set_data function. In this case, it has a single field called my_account, an Account struct that represents an instance of the MyAccount type defined below. The #[account(mut)] attribute on the my_account field indicates that it is a mutable account, meaning the function will modify its state.

Constraints

When building Solana programs, security is critical to your program's viability. Anchor's Accounts struct provides a handy built-in feature to simplify common security checks. Constraints allow you to verify that the accounts you are passing in (or their underlying data) match some predefined requirements. To add constraints, use the following format:

#[account(<constraints>)]
pub account: AccountType

If you have done any building in Anchor, you have probably used constraints without knowing it. For example, the mut constraint is used to ensure that the specified account must be mutable:

#[account(mut)]
pub my_account: Account<'info, MyAccount>

In the example above, the program would throw an error if a user tried to pass an immutable account as my_account.

Let's look at a few other common examples of constraints that can be added to your Accounts struct.

AttributeExample
Implementation
Description
mut
#[account(mut)] 
pub mutable_account: Account<'info, MyData>,

Verifies the account is mutable
signer
#[account(signer)] 
pub signer_account: Account<'info, MyData>,
Verifies the account signed the transaction
init,
payer,
space
#[account(init, payer = bank, space = 200)]
pub new_account: Account<'info, MyData>,
#[account(mut)]
pub bank: Signer<'info>,
pub new_account: Account<'info, MyData>,
system_program: Program<'info, System>,
Initializes a new account.
init must be used with payer (a reference to another account who will pay rent for the new account) and space (number of bytes to allocate to the new account).

Note: you must pass the System Program in your accounts as system_program to create a new account.
init_if_needed can be used in lieu of init to create an account if it doesn't exist yet; however, Anchor warns that "When using init_if_needed, you need to make sure you properly protect yourself against re-initialization attacks. You need to include checks in your code that check that the initialized account cannot be reset to its initial settings after the first time it was initialized (unless that it what you want)." init_if_needed can be enabled by importing anchor-lang with the init-if-needed cargo feature.
seeds,
bump
#[account(seeds = [
b"example",
user.key().as_ref()
], bump)]
pub user_ex_pda: Account<'info, MyData>,
pub user: Signer<'info>,

Verifies that the passed account is derived from the currently executing program and the defined seeds.

Note: bump must be provided. If not set as an expression, Anchor uses the canonical bump. We discuss bump expressions in the Instruction Data section below.
To derive a PDA for a program other than the one currently executing, you may use seeds::program = other_program.key().
has_one
#[account(has_one = authority)]
pub data: Account<'info, MyData>,
pub authority: Signer<'info>,
Verifies the account specified has a field on the account equal to the key of a specified account of the same name.
In this example, data.authority must be the same as authority.key().
address
#[account(address = SOME_PUBKEY)]
pub authority: Signer<'info>,
Verifies the account key matches the passed pubkey (e.g., if you were restricting an authority to a specified SOME_PUBKEY).
owner
#[account(owner = token_program.key())]
pub data: Account<'info, MyData>,
pub token_program: Account<'info, Token>,
Verifies the account owner matches the passed pubkey (e.g., the example requires the data account to be owned by the Token Program).
executable
#[account(executable)]
pub some_program: AccountInfo<'info>,
Verifies the account is a program.
rent_exempt
#[account(rent_exempt = skip)]
pub skipped_account: Account<'info, MyData>,
#[account(rent_exempt = enforce)]
pub enforced_account: Account<'info, MyData>,
Skips or enforces rent exemption check normally done by other constraints (e.g., init).
zero
#[account(zero)]
pub zero_account: Account<'info, MyData>,
Checks that the account discriminator is zero. This is necessary for accounts larger than 10kb. This must be done via a 2-step process: create the account and then initialize it with zero.
close
#[account(close = receiver)]
pub closing: Account<'info, MyData>,
pub receiver: SystemAccount<'info>,
Marks the account as closed at the end of the instruction and sends its lamports to the specified account.
constraint
#[account(
constraint =
acct_one.age == acct_two.info.age
)]
pub acct_one: Account<'info, MyData>,
pub acct_two: Account<'info, OtherData>,
Verifies whether the given expression evaluates to true.
realloc
#[account(
mut,
realloc = 100,
realloc::payer = bank
realloc::zero = false
)]
pub update_acct: Account<'info, MyData>,
#[account(mut)]
pub bank: Signer<'info>,
system_program: Program<'info, System>,
Update the amount of space (bytes) allocated to an account.
Any cost associated with new space will be paid by realloc::payer
Any recovered lamports from reducing space will be received by realloc::payer
Use realloc::zero to determine whether the new memory should be zero-initialized after reallocation

SPL Token Constraints

In addition to the constraints above, the Anchor SPL library includes several constraints specific to the SPL Token Program:

AttributeExample
Implementation
Description
token::mint
token::authority
#[account(
init,
payer = payer,
token::mint = mint,
token::authority = payer,
)]
pub new_ata: Account<'info, TokenAccount>,
#[account(address = mint::USDC)]
pub mint: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>
Verifies the mint address and the authority of a TokenAccount

If using init, you must pass both mint and authority. You may use one of the two options if just checking an account.
mint::authority
mint::decimals
mint::freeze_authority
#[account(
init,
payer = payer,
mint::decimals = 9,
mint::authority = payer,
mint::freeze_authority = payer,
)]
pub new_ata: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>
Create or check a mint account with the given mint decimals and mint authority

If using init, you must pass both decimals and authority. freeze authority is optional. You may use one of the three options if just checking an account.
associated_token::
mint,
associated_token::
authority
#[account(
init,
payer = payer,
associated_token::mint = mint,
associated_token::authority = payer,
)]
pub new_ata: Account<'info, TokenAccount>,
#[account(address = mint::USDC)]
pub mint: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, Token>,
pub system_program: Program<'info, System>
Verifies the mint address and the authority of an associated token account.

If using init, you must pass both mint and authority. If just checking an account, you may use one of the two options.

Handling Constraint Errors

Anchor constraints make it easy to throw custom errors. You can call an error if a constraint is violated simply by adding @ after your constraint, like so:

#[account(mut @ MyError::AccountNotMutable)]
pub my_account: Account<'info, MyData>

The example above would throw your AccountNotMutable error if a user passed the my_account without marking it mutable. The @ error handler works for mut, signer, has_one, address, owner, and constraint.

Utilizing Instruction Data in Constraints

Sometimes you need data from your account instruction to pass into your constraints (for example, you might want to pass in a bump for security or derive a PDA based on a user input, e.g., restaurant_name for a restaurant review). To access the instruction's arguments, you must use the #[instruction(..)] attribute. Here's an example of how to use it:

#[derive(Accounts)]
#[instruction(entered_bump: u8, restaurant: String)]
pub struct InitializeReview<'info> {
#[account(
init,
payer = payer,
space = 100
seeds = [restaurant.as_bytes().as_ref(), signer.key().as_ref()],
bump = entered_bump
)]
pub pda_data_account: Account<'info, RestaurantReview>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}

Wrap Up

Getting the hang of constraints can be tricky, so do not get frustrated if it does not click at first. Like anything, it takes a little bit of practice. Feel free to bookmark this page and come back to it when you're defining new account contexts in your programs. Constraints and proper error handling can make your code more secure and readable!

If you're having trouble, have questions, or just want to talk about what you're building, drop us a line on Discord or Twitter!

We <3 Feedback!

If you have any feedback on this guide, let us know. We'd love to hear from you.

Share this guide