2. Account State Design

Learn how to define on-chain data structures with Anchor’s #[account] macro.


Basic Account Structure

Source: state/config.rs

use anchor_lang::prelude::*;

#[account]
pub struct ProgramConfig {
    pub admin: Pubkey,           // 32 bytes
    pub fee_destination: Pubkey, // 32 bytes
    pub fee_basis_points: u64,   // 8 bytes
    pub paused: bool,            // 1 byte
    pub bump: u8,                // 1 byte
}

impl ProgramConfig {
    // 8 (discriminator) + 32 + 32 + 8 + 1 + 1 = 82 bytes
    pub const LEN: usize = 8 + 32 + 32 + 8 + 1 + 1;
}

Key Points

  • #[account] macro generates serialization/deserialization code
  • Discriminator: First 8 bytes identify the account type
  • Size calculation: Always include the 8-byte discriminator
  • Bump storage: Store PDA bump for efficient validation

Account with Timestamps

Source: state/user.rs

#[account]
pub struct UserAccount {
    pub authority: Pubkey,  // Owner of this account
    pub points: u64,        // User's points balance
    pub created_at: i64,    // Unix timestamp
    pub updated_at: i64,    // Last update timestamp
    pub bump: u8,           // PDA bump seed
}

impl UserAccount {
    pub const LEN: usize = 8 + 32 + 8 + 8 + 8 + 1;
}

Getting Current Timestamp

use anchor_lang::prelude::*;

pub fn create_user_account_handler(ctx: Context<CreateUserAccount>) -> Result<()> {
    let user = &mut ctx.accounts.user_account;
    let clock = Clock::get()?;
    
    user.authority = ctx.accounts.authority.key();
    user.points = 0;
    user.created_at = clock.unix_timestamp;
    user.updated_at = clock.unix_timestamp;
    user.bump = ctx.bumps.user_account;
    
    Ok(())
}

Account with Methods

Source: state/role.rs

#[account]
pub struct Role {
    pub authority: Pubkey,
    pub role_type: RoleType,
    pub permissions: u8,      // Bitmask for permissions
    pub assigned_by: Pubkey,
    pub assigned_at: i64,
    pub updated_at: i64,
    pub bump: u8,
}

impl Role {
    pub const LEN: usize = 8 + 32 + 1 + 1 + 32 + 8 + 8 + 1;

    // Check if role has a specific permission
    pub fn has_permission(&self, permission: u8) -> bool {
        (self.permissions & permission) != 0
    }

    // Add permission using bitwise OR
    pub fn add_permission(&mut self, permission: u8) {
        self.permissions |= permission;
    }

    // Remove permission using bitwise AND with NOT
    pub fn remove_permission(&mut self, permission: u8) {
        self.permissions &= !permission;
    }
}

Usage Example

pub fn check_permission_handler(ctx: Context<CheckPermission>) -> Result<()> {
    let role = &ctx.accounts.role;
    
    require!(
        role.has_permission(permissions::MANAGE_TOKENS),
        ErrorCode::InsufficientPermissions
    );
    
    // Permission granted, proceed...
    Ok(())
}

Enum State

#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq)]
pub enum RoleType {
    Admin,
    Moderator,
    User,
}

impl RoleType {
    pub fn default_permissions(&self) -> u8 {
        match self {
            RoleType::Admin => 0xFF,     // All permissions
            RoleType::Moderator => 0x06, // Limited permissions
            RoleType::User => 0x00,      // No special permissions
        }
    }
}

Enum Size

Enums in Anchor are 1 byte + size of largest variant:

pub enum Status {
    Active,           // 1 byte (discriminant only)
    Paused,           // 1 byte
    Closed,           // 1 byte
}

pub enum ComplexStatus {
    Active,                    // 1 + 0 = 1 byte
    Locked { until: i64 },     // 1 + 8 = 9 bytes
    Banned { reason: String }, // 1 + 4 + len = variable
}

Size = 1 byte + largest variant size


Bitmask Permissions

Efficient permission storage using bit flags:

pub mod permissions {
    pub const MANAGE_CONFIG: u8 = 1 << 0;      // 0b00000001
    pub const MANAGE_USERS: u8 = 1 << 1;       // 0b00000010
    pub const MANAGE_TOKENS: u8 = 1 << 2;      // 0b00000100
    pub const PAUSE_PROGRAM: u8 = 1 << 3;      // 0b00001000
    pub const EMERGENCY_ACTIONS: u8 = 1 << 4;  // 0b00010000
    pub const MANAGE_TREASURY: u8 = 1 << 5;    // 0b00100000
    pub const MANAGE_ROLES: u8 = 1 << 6;       // 0b01000000
    pub const BATCH_OPERATIONS: u8 = 1 << 7;   // 0b10000000
}

Combining Permissions

// Check single permission
if role.has_permission(permissions::MANAGE_TOKENS) {
    // User can manage tokens
}

// Check multiple permissions (any)
let can_manage = role.has_permission(permissions::MANAGE_TOKENS)
    || role.has_permission(permissions::MANAGE_USERS);

// Assign multiple permissions at once
let admin_perms = permissions::MANAGE_CONFIG 
    | permissions::MANAGE_USERS 
    | permissions::MANAGE_TOKENS;

role.permissions = admin_perms;

Benefits:

  • Store up to 8 permissions in 1 byte
  • Fast bitwise operations
  • Memory efficient
  • Easy to extend (use u16/u32 for more permissions)

Size Calculation Reference

Type Size (bytes) Example
bool 1 is_active: bool
u8, i8 1 count: u8
u16, i16 2 id: u16
u32, i32 4 amount: u32
u64, i64 8 lamports: u64
u128, i128 16 large_number: u128
Pubkey 32 owner: Pubkey
String 4 + len name: String (max len)
Vec<T> 4 + (len * T::size) items: Vec<u64>
Option<T> 1 + T::size maybe: Option<u64>
Enum 1 + largest variant status: Status
Discriminator 8 Always added by Anchor

Example Calculation

#[account]
pub struct MyAccount {
    pub owner: Pubkey,        // 32
    pub balance: u64,         // 8
    pub created_at: i64,      // 8
    pub is_active: bool,      // 1
    pub status: Status,       // 1 (enum)
    pub bump: u8,             // 1
}

impl MyAccount {
    // 8 (discriminator) + 32 + 8 + 8 + 1 + 1 + 1 = 59 bytes
    pub const LEN: usize = 8 + 32 + 8 + 8 + 1 + 1 + 1;
}

Variable-Length Data

For dynamic data, calculate maximum size:

#[account]
pub struct NftMetadata {
    pub name: String,    // Max 32 chars
    pub symbol: String,  // Max 10 chars
    pub uri: String,     // Max 200 chars
    // ...
}

impl NftMetadata {
    pub const MAX_NAME_LENGTH: usize = 32;
    pub const MAX_SYMBOL_LENGTH: usize = 10;
    pub const MAX_URI_LENGTH: usize = 200;
    
    pub const LEN: usize = 8 
        + 4 + Self::MAX_NAME_LENGTH 
        + 4 + Self::MAX_SYMBOL_LENGTH 
        + 4 + Self::MAX_URI_LENGTH;
}

Validation:

require!(
    name.len() <= NftMetadata::MAX_NAME_LENGTH,
    ErrorCode::NameTooLong
);

Best Practices

Always store bump seeds - Avoids recomputation
Add timestamps - Track creation/update times
Use bitmasks - Efficient permission storage
Calculate LEN correctly - Include discriminator (8 bytes)
Validate variable data - Enforce maximum lengths
Add helper methods - Encapsulate business logic
Document sizes - Comment byte sizes inline


Next: PDA (Program Derived Address)