Hello! Welcome back to our exploration of the Anchor framework.
In our last lesson, you learned how to define the schema for your program's on-chain data using the #[account] macro. We covered how this defines the structure of accounts your program owns, how Anchor uses an 8-byte discriminator for type safety, and how to calculate the required space for these accounts.
This lesson addresses the next logical step: how do we tell a specific instruction which accounts it needs and what rules those accounts must follow? This is the heart of Anchor's security model and the focus of today's learning outcome: to specify and validate instruction accounts using an Accounts struct.
Think of the #[account] struct from last lesson as defining a "table schema" in a database. Today, you'll learn to write the "function signature" that specifies which tables (accounts) a particular operation can access and what permissions (e.g., read-only, write) it has.
From Manual Iteration to Declarative Validation
In native Solana development, you had to manually iterate through a flat list of AccountInfo objects passed to your program. You were responsible for checking that you received the correct number of accounts, that they were in the right order, that they were writable if needed, and that the transaction was signed by the right parties. This is tedious and a major source of security vulnerabilities.
Anchor replaces this imperative, error-prone process with a clean, declarative model. You simply declare what you expect, and Anchor generates the validation code for you.
Solana Smart Contract Tutorial: Using the Anchor Framework
To see the stark difference, let's watch this comparison from Josh's DevBox. It contrasts the manual checks in native Solana with Anchor's automated approach, which will make the value of the Accounts struct immediately clear.
Watch the clip from 00:42:30 to 00:45:23. Notice how the native code is filled with manual parsing and checks, while the Anchor code is clean, with each function clearly defining its purpose and required accounts.
This declarative approach is the purpose of the #[derive(Accounts)] macro. You apply it to a struct where each field represents an account required by your instruction. This struct then becomes the T in the Context<T> parameter of your instruction handler.
// This struct defines all the accounts our `initialize` instruction needs.
#[derive(Accounts)]
pub struct Initialize<'info> {
// ... fields representing accounts go here ...
}
// Our instruction handler receives the validated accounts in the context.
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// We can now safely access the accounts via ctx.accounts
// e.g., ctx.accounts.some_account
Ok(())
}
Let's consult the official Anchor documentation for a formal introduction to this concept.
The Anchor documentation provides the definitive explanation of the #[derive(Accounts)] macro and its role in account validation.
Read the sections titled "#[derive(Accounts)] macro" and "Account Validation". Focus on how the macro is used and the two primary ways Anchor validates accounts: Account Constraints and Account Types.
Defining the Players: Anchor's Account Types
As the documentation explains, the fields in your Accounts struct are not just any Rust types. They are specific Anchor types that encapsulate validation logic. Here are the three most fundamental ones you'll use constantly:
-
Account<'info, T>: Represents a data account owned by your program. TheTis a struct you've defined with#[account](like we did in the last lesson). When Anchor processes this type, it automatically performs several critical checks:- Ownership Check: Verifies the account is owned by the currently executing program.
- Discriminator Check: Verifies the first 8 bytes of the account's data match the discriminator for the struct
T. - Deserialization: If both checks pass, it deserializes the account data into the
Tstruct for you to use.
-
Signer<'info>: Represents an account that must have signed the transaction. This is the primary way you authorize actions. Anchor simply checks theis_signerflag on the underlyingAccountInfo. The data of this account is not read or trusted. -
Program<'info, T>: Represents another program on the blockchain (e.g., theSystemProgramor theTokenProgram). Anchor verifies that the public key of the account passed in matches the expected program ID forTand that the account is marked as executable.
Anchor Basics [Solana Development Course M5P1] - Feb 22nd '23
Let's watch a segment from Solandy's "Anchor Basics" video that explains these different account types and introduces the constraints used to validate them.
Watch from 00:17:30 to 00:21:45. Pay close attention to the descriptions of Account, Signer, and Program, and see how constraints like init, payer, and space are introduced to build up a validation rule.
Setting the Rules: The #[account] Constraint Attribute
You've now seen the main types of accounts. The real power comes from applying constraints to them using the #[account(...)] attribute on each field. This is how you enforce the specific rules for your instruction.
Let's explore some of the most common constraints.
How to Use Account Constraints in Your Solana Anchor ...
This guide from QuickNode provides an excellent overview and a handy reference table for Anchor constraints. We'll start with the introduction and then use the table as our main reference.
First, read the section titled "Constraints" to understand the general syntax. Then, carefully review the large table of attributes. For this lesson, focus your attention on understanding mut, signer, init, payer, and space. We will return to this table for more advanced constraints in future lessons.
Putting it all Together: An Example
Let's analyze a canonical example that uses the types and constraints we've just learned about. This is the Accounts struct for a typical "initialize" instruction that creates a new data account.
use anchor_lang::prelude::*;
// This is our program-owned data account structure (from the previous lesson)
#[account]
pub struct MyAccount {
pub data: u64,
}
// This is the Accounts struct for our 'initialize' instruction
#[derive(Accounts)]
pub struct Initialize<'info> {
// 1. The 'init' constraint
#[account(
init, // Tells Anchor to create this account
payer = user, // The 'user' account below will pay for rent
space = 8 + 8 // The space to allocate (8 for discriminator + 8 for u64)
)]
pub my_account: Account<'info, MyAccount>,
// 2. The 'mut' constraint on the Signer
#[account(mut)]
pub user: Signer<'info>,
// 3. The System Program
pub system_program: Program<'info, System>,
}
Let's break down what's happening here when this instruction is called:
-
my_account:init: This is the key constraint. Anchor will make a Cross-Program Invocation (CPI) to the System Program to create a new account.payer = user: It specifies that theuseraccount (defined below) must pay the lamports required for rent. Anchor ensuresuserhas sufficient funds.space = 8 + 8: It tells the System Program to allocate 16 bytes for the new account. This connects directly to what you learned about space calculation in the last lesson.- The type
Account<'info, MyAccount>ensures that after creation, the account will be owned by our program and its data structure will be prepared for aMyAccountstruct.
-
user:Signer<'info>: Anchor validates that this account signed the transaction. This is our authority check.#[account(mut)]: Themutconstraint is crucial here. Since this account is thepayer, its lamport balance will be debited. Therefore, it must be mutable. If a client submits a transaction where theuseraccount isn't marked as writable, Anchor will reject it.
-
system_program:Program<'info, System>: Theinitconstraint requires the System Program to create the account. We must pass it in so Anchor can use it for the CPI. TheProgramtype validates that the account provided at this position is indeed the official Solana System Program.
This small, declarative struct packs an incredible amount of validation that you would otherwise have to write manually.
Test your understanding!
You are building a simple counter program. You need to define the Accounts struct for an increment instruction. This instruction needs two accounts:
- The
counterdata account, which should be mutable so you can increase its value. - The
authorityaccount, which must be the signer of the transaction to authorize the change.
Based on what you've learned, write the Increment<'info> struct. Assume you have already defined #[account] pub struct Counter { ... }.
Show answer
#[derive(Accounts)]
pub struct Increment<'info> {
// The counter account must be mutable to be changed.
// The `Account` type will check ownership and the discriminator.
#[account(mut)]
pub counter: Account<'info, Counter>,
// The authority must sign the transaction.
pub authority: Signer<'info>,
}
(In the next lesson, you'll learn how to add a has_one = authority constraint to the counter account to ensure that the authority signer here matches a public key stored inside the counter account, completing the security model.)
What Happens When Validation Fails?
The beauty of this system is the automatic error handling. If a client tries to call an instruction with the wrong accounts or without meeting the constraints, the transaction doesn't just fail cryptically. Anchor returns a specific error code that tells you exactly what went wrong.
Anchor Basics [Solana Development Course M5P1] - Feb 22nd '23
Let's see this in action. The end of Solandy's video demonstrates what happens when an instruction is called with an account that has the wrong owner or the wrong discriminator.
Watch from 00:45:19 to 00:47:45. Observe the different error codes Anchor produces for an ownership failure versus a discriminator mismatch. This is the framework protecting your program from invalid state changes.
Conclusion
In this lesson, you've learned to use one of Anchor's most powerful features: the Accounts struct. By moving from manual, imperative checks to a declarative validation model, you eliminate huge amounts of boilerplate code and dramatically improve the security and readability of your programs.
Here are the key takeaways:
- The
#[derive(Accounts)]macro is used on a struct to define the set of accounts an instruction requires. - Each field in the struct uses a specific Anchor Type (
Account,Signer,Program) to perform baseline validation. - The
#[account(...)]attribute is used to apply constraints (mut,init,payer,space, etc.) that enforce the rules and logic for your instruction. - This declarative system provides robust, automatic security checks before your instruction logic even runs.
You now have the tools to define both the data structures (#[account]) and the instruction access patterns (#[derive(Accounts)]).
In the next lesson, we will deepen our understanding of security by exploring more advanced constraints. You'll learn how to implement security checks by applying constraints like signer, mut, and has_one to accounts. We will focus particularly on has_one, which is essential for creating relationships between accounts and ensuring that the signer of a transaction is the authorized user for a specific piece of data.
Can't find a good explanation? Sign up and we'll make it for you
Sign up