SolanaAnchorRustBlockchain13 min readUpdated

Anchor Accounts in Solana: #[account], Init, Constraints

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: Anchor Accounts in Solana: #[account], Init, Constraints

Accounts are the fundamental storage unit in Solana. Every piece of on-chain state lives in an account. Anchor provides two macros that handle everything around accounts: #[account] defines the data shape, and #[derive(Accounts)] validates and deserializes them before your instruction handler runs. If you are working through this for the first time, the Solana Hello World with Anchor guide builds a complete counter program that demonstrates both macros in action.

Data shape

The #[account] Macro

#[account] is the annotation that turns a plain Rust struct into a type Anchor can read from and write to on-chain.

Quick answer

What does the #[account] macro do in Anchor? #[account] tells Anchor to implement Borsh serialization and deserialization for the struct, and to prepend an 8-byte discriminator when writing the account. It also registers the struct as a known account type in the generated IDL.

rust
use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct UserProfile {
    pub authority: Pubkey,   // 32 bytes
    pub score: u64,          //  8 bytes
    pub level: u8,           //  1 byte
    pub is_active: bool,     //  1 byte
    #[max_len(50)]
    pub username: String,    //  4 + 50 = 54 bytes
}
// Total data: 32 + 8 + 1 + 1 + 54 = 96 bytes
// Total space: 8 (discriminator) + 96 = 104 bytes

InitSpace calculates space automatically

Adding #[derive(InitSpace)] lets you use UserProfile::INIT_SPACE in the init constraint instead of hardcoding a number. For Strings, annotate with #[max_len(N)] to tell InitSpace the maximum byte length.

The 8-byte discriminator is the first thing Anchor writes when an account is created. When your program reads the account later, Anchor checks that the discriminator matches the expected struct before deserializing. This prevents one account type from being passed where another is expected — a common attack vector in raw Solana programs.

Account validation

How Anchor Validates Accounts

Every instruction in an Anchor program has an associated Accounts struct. Anchor runs all constraints in that struct before your handler executes.

Quick answer

What is #[derive(Accounts)] in Anchor? #[derive(Accounts)] is a procedural macro that generates the account deserialization and constraint-checking code for an instruction. You declare what you need; Anchor generates the validation.

rust
#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct UpdateProfile<'info> {
    /// The user profile account to update.
    #[account(
        mut,
        has_one = authority,
        seeds = [b"profile", authority.key().as_ref()],
        bump
    )]
    pub profile: Account<'info, UserProfile>,

    /// The wallet that owns the profile.
    pub authority: Signer<'info>,
}

Account types in Anchor structs serve different purposes:

  • Account<'info, T> — deserializes an account into type T and checks the discriminator
  • Signer<'info> — verifies the account signed the transaction
  • Program<'info, T> — verifies the account is a specific program (e.g. Token, System)
  • SystemAccount<'info> — verifies it is a system-owned account (e.g. for wallets)
  • UncheckedAccount<'info> — no automatic checks; you handle validation manually

Account<'info, T> vs AccountInfo<'info>

Account<'info, T> is the Anchor wrapper. It deserializes the raw bytes into T, checks the discriminator, and validates the owner. AccountInfo is the raw Solana type — use it when you need low-level access or when working outside Anchor.

Account creation

The init Constraint

init is the most important Anchor constraint. It creates a new on-chain account with rent-exempt lamports and proper ownership in one step.

Quick answer

What does the init constraint do in Anchor? init creates a new account via the System Program, transfers enough lamports for rent exemption, sets the account owner to your program, and writes the discriminator.

rust
#[derive(Accounts)]
pub struct CreateProfile<'info> {
    #[account(
        init,
        payer = user,
        space = 8 + UserProfile::INIT_SPACE
    )]
    pub profile: Account<'info, UserProfile>,

    #[account(mut)]
    pub user: Signer<'info>,

    pub system_program: Program<'info, System>,
}

system_program is required with init

Anchor calls System Program's create_account instruction under the hood when init is used. If you omit system_program from the struct, the program will not compile.

The space parameter is the total bytes to allocate, including the 8-byte discriminator. Using INIT_SPACE is safer than a hardcoded number because it stays correct when you add or remove fields from the struct. For a full working example with init, see the Anchor framework overview.

Constraint reference

The Six Constraints You Will Use Most

These six constraints cover the large majority of real program validation requirements.

ConstraintWhat it checksExample
mutAccount is marked writable in the transaction#[account(mut)]
has_oneField on the account matches a named account in the struct#[account(has_one = authority)]
constraintCustom boolean expression must be true#[account(constraint = profile.score < 1000)]
close = targetCloses the account and sends lamports to target#[account(mut, close = user)]
seeds + bumpAccount is the canonical PDA for the given seeds#[account(seeds=[b"x"], bump)]
ownerAccount is owned by a specific program#[account(owner = token_program.key())]

close reclaims rent lamports

When an account is closed with the close constraint, Anchor zeroes out the data, transfers all lamports to the target account, and sets the account's lamports to zero. The account will be garbage collected by the runtime.

PDA accounts

PDA Accounts with Seeds and Bump

Program Derived Addresses are the standard pattern for program-owned storage. Anchor handles PDA creation and validation with seeds and bump in the account constraint.

Quick answer

How do I create a PDA account in Anchor? Use seeds and bump in the #[account] constraint for init to create the PDA, and the same seeds + bump for subsequent instructions that need to access it.

rust
#[derive(Accounts)]
pub struct CreateVault<'info> {
    #[account(
        init,
        payer = user,
        space = 8 + Vault::INIT_SPACE,
        seeds = [b"vault", user.key().as_ref()],
        bump
    )]
    pub vault: Account<'info, Vault>,

    #[account(mut)]
    pub user: Signer<'info>,

    pub system_program: Program<'info, System>,
}
rust
#[derive(Accounts)]
pub struct WithdrawFromVault<'info> {
    #[account(
        mut,
        seeds = [b"vault", user.key().as_ref()],
        bump = vault.bump,
        has_one = user
    )]
    pub vault: Account<'info, Vault>,

    pub user: Signer<'info>,
}
typescript
import * as anchor from "@coral-xyz/anchor";

const [vaultPDA, bump] = anchor.web3.PublicKey.findProgramAddressSync(
  [Buffer.from("vault"), provider.wallet.publicKey.toBuffer()],
  program.programId
);

console.log("Vault PDA:", vaultPDA.toBase58());
console.log("Bump:", bump);

Store the bump in the account

Add a 'pub bump: u8' field to your PDA account struct and save ctx.bumps.vault to it during init. This avoids recomputing the bump on every subsequent instruction.

Anchor account constraint reference
Six constraints cover the majority of real program validation needs
PDA creation flow with Anchor seeds
Anchor validates PDA seeds and bump before running the instruction handler

FAQ

Frequently Asked Questions

Common questions about Anchor account macros, constraints, and PDAs.

What is #[account] in Anchor?

#[account] is an Anchor procedural macro applied to a Rust struct. It generates Borsh serialization and deserialization code and adds an 8-byte discriminator to the account data. This lets Anchor automatically handle reading and writing on-chain state without manual byte parsing.

What is the difference between #[account] and #[derive(Accounts)]?

#[account] is applied to the data struct — it defines the shape of on-chain state. #[derive(Accounts)] is applied to the instruction context struct — it defines which accounts an instruction needs and validates them. You use both together: the data struct defines what is stored, the context struct defines who provides it.

What does init do in Anchor?

The init constraint instructs Anchor to create a new on-chain account. It calls the System Program to allocate space and lamports for rent exemption, sets the account owner to your program, and writes the 8-byte discriminator. You must include system_program in the accounts struct when using init.

What is a PDA in Solana?

A Program Derived Address (PDA) is an account address derived deterministically from a program ID and a set of seeds. PDAs are not on the ed25519 curve, so no private key controls them — only the owning program can sign for them. They are used for program-owned accounts that need to be found by address without storing the keypair.

How do I close an account in Anchor?

Add the close = target constraint to the account field in your #[derive(Accounts)] struct, where target is the account that will receive the refunded lamports. Anchor handles zeroing the data, transferring the rent-exempt lamports, and marking the account for garbage collection.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View blockchain development serviceSee ChainTrust case study

Related service

Blockchain Development

See scope & pricing →

Related case study

ChainTrust Compliance Engine

Read case study →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →