Accounts and Validation
Account types, zero-copy mechanics, the #[account] macro, and #[derive(Accounts)].
#[derive(Accounts)] generates all parsing and validation at compile time. Every account is zero-copy -- fields are read directly from account memory with no deserialization or heap allocation.
No Duplicates by Default
Quasar contexts reject duplicate accounts by default. This enables a faster entrypoint -- the runtime can skip borrow-state tracking for most accounts, which saves CUs on every instruction. Most instructions don't need the same account twice.
When you do need a duplicate (rare), opt in with #[account(dup)] and a /// CHECK: comment explaining why:
/// CHECK: Same authority used as both source and destination signer.
#[account(dup)]
pub authority_alias: &'info Signer,Account Types
| Type | What it does |
|---|---|
Account<T> | Program-owned data account. Validates owner + discriminator. Deref/DerefMut to the zero-copy type. |
Signer | Must have the is_signer flag set. |
UncheckedAccount | No validation. For accounts passed through to CPIs or validated manually. |
SystemAccount | Owned by the System program. For SOL-holding accounts with no program data. |
Program<T> | Validates the executable flag and address against T::ID. |
Interface<T> | Validates address against a user-defined set of program IDs. See Interfaces. |
InterfaceAccount<T> | Like Account<T>, but accepts accounts owned by multiple programs. Supports resolve(). |
Sysvar<T> | Validates the sysvar address. Deref to the inner type. |
Any type can be wrapped in Option -- an all-zero address is treated as None:
pub optional_authority: Option<&'info Signer>,Mutability
Two ways to mark an account as mutable:
// 1. In the type (preferred) -- &mut means writable
pub vault: &'info mut Account<Vault>,
// 2. Via attribute
#[account(mut)]
pub vault: &'info Account<Vault>,Read-only is &'info T. If an account is mutable but the transaction doesn't pass it as writable, parsing fails with ProgramError::Immutable.
Interfaces
Interface<T> and InterfaceAccount<T> are a generic abstraction for accounts that can belong to multiple programs. Unlike Anchor where interface types are hardcoded to SPL Token/Token-2022, Quasar lets you define interfaces for any set of programs.
Defining an interface
Implement ProgramInterface to tell Quasar which program IDs are valid:
pub struct TokenInterface;
impl ProgramInterface for TokenInterface {
fn matches(address: &Address) -> bool {
*address == SPL_TOKEN_ID || *address == TOKEN_2022_ID
}
}TokenInterface is built-in, but you can create interfaces for anything -- oracles, governance programs, lending protocols.
Using interfaces
Interface<T> wraps a program account (validates address against T::matches()). InterfaceAccount<T> wraps a data account owned by any matching program:
#[derive(Accounts)]
pub struct Transfer<'info> {
pub authority: &'info Signer,
pub from: &'info mut InterfaceAccount<Token>,
pub to: &'info mut InterfaceAccount<Token>,
pub token_program: &'info Interface<TokenInterface>,
}CPI methods work identically -- the SVM routes to whichever program was passed in the transaction.
Runtime dispatch with resolve()
InterfaceAccount<T> supports resolve() for dispatching to different zero-copy layouts based on the runtime owner. Implement InterfaceResolve on your marker type:
match ctx.accounts.oracle.resolve()? {
OraclePrice::Pyth(price) => { /* read Pyth-specific fields */ }
OraclePrice::Switchboard(price) => { /* read Switchboard fields */ }
}The owner check runs once during account parsing. resolve() is a second pointer cast -- no re-validation, no allocation. This gives you tagged-union-style dispatch over accounts owned by entirely different programs, all zero-copy.
Defining Account Types
The #[account] macro transforms a struct into a zero-copy on-chain type. You write normal Rust types; the macro generates a companion struct with alignment-1 Pod fields, and Deref/DerefMut impls that make the conversion invisible.
How zero-copy works
Solana account data arrives as raw &[u8]. Quasar pointer-casts directly into it -- no deserialization, no copying. The catch: u64 has alignment 8, but account data has no alignment guarantees. A misaligned cast is undefined behavior.
The #[account] macro solves this by generating a companion #[repr(C)] struct where every field is replaced with its alignment-1 Pod equivalent:
| You write | Generated companion uses | Size |
|---|---|---|
u64 | PodU64 | 8 bytes |
u32 | PodU32 | 4 bytes |
u16 | PodU16 | 2 bytes |
i64 | PodI64 | 8 bytes |
bool | PodBool | 1 byte |
u8 / i8 | u8 / i8 (already alignment 1) | 1 byte |
Address | Address (already [u8; 32]) | 32 bytes |
Pod types implement all arithmetic operators (+, -, *, /, %, +=, etc.) with both Pod and native operands, plus From/Into conversions. So when you read escrow.receive (a PodU64) and compare it to a u64 argument, it just works:
// escrow.receive is PodU64 under the hood, but this compiles naturally:
require!(escrow.receive > 0, MyError::ZeroReceiveAmount);
let total = escrow.receive + deposit;Arithmetic uses wrapping semantics in release builds (saves CUs) and panics on overflow in debug builds -- matching Rust's native integer behavior. Use checked_add, checked_sub, etc. when overflow must be detected.
Static accounts
For fixed-size fields:
#[account(discriminator = 1)]
pub struct Escrow {
pub maker: Address,
pub mint_a: Address,
pub mint_b: Address,
pub maker_ta_b: Address,
pub receive: u64,
pub bump: u8,
}The macro generates:
- A companion
EscrowZcstruct with Pod fields and#[repr(C)]layout (alignment 1) Deref/DerefMutimpls that pointer-cast directly into account dataDiscriminator,Owner,Spacetrait impls- A
set_inner()method that takes the native types you wrote
set_inner() accepts the types you declared (Address, u64, u8) and converts to Pod internally:
self.escrow.set_inner(
*self.maker.address(), // Address
*self.mint_a.address(), // Address
*self.mint_b.address(), // Address
*self.maker_ta_b.address(), // Address
receive, // u64 -> PodU64 internally
bumps.escrow, // u8
);Dynamic accounts
Variable-length fields use String<'a, MAX> and Vec<'a, T, MAX> -- stored inline in account data with a length prefix, accessed as &str and &[T] slices:
#[account(discriminator = 1)]
pub struct MultisigConfig<'a> {
pub creator: Address,
pub threshold: u8,
pub bump: u8,
pub label: String<'a, 32>, // max 32 bytes
pub signers: Vec<'a, Address, 10>, // max 10 elements
}Each dynamic field generates a getter (label() -> &str), a setter (set_label(&mut self, payer, value)), and a raw accessor (label_raw() -> RawEncoded) for CPI pass-through. Vec fields also get a mutable accessor (signers_mut() -> &mut [Address]).
Tail fields
If you have one dynamic field and it's the last field in the struct, use a bare &'a str or &'a [u8] instead. No length prefix -- the field consumes all remaining bytes, which is slightly more efficient:
#[account(discriminator = 1)]
pub struct Note<'a> {
pub author: Address,
pub content: &'a str,
}Discriminator rules
- Must be non-zero (prevents uninitialized accounts from passing validation)
- Cannot start with
0xFF(reserved for the event protocol)
Account Constraints
Field-level #[account(...)] attributes on a #[derive(Accounts)] struct generate validation code that runs during account parsing. Constraints can be combined freely on a single field.
init / init_if_needed
init creates the account via a CPI to the System program -- it allocates space, pays rent, writes the discriminator, and assigns ownership to your program in one step. init_if_needed does the same but skips creation if the account already exists (discriminator is already set).
Both need a payer. If your struct has a field named payer, it's auto-detected. Otherwise specify it explicitly with payer = <field>. Space is auto-computed from the account type; override with space = <expr> for dynamic accounts or custom sizing:
// payer auto-detected from the `payer` field
#[account(init, seeds = [b"escrow", payer], bump)]
pub escrow: &'info mut Account<Escrow>,
pub payer: &'info mut Signer,
// explicit payer when the field has a different name
#[account(init, payer = maker, seeds = [b"escrow", maker], bump)]
pub escrow: &'info mut Account<Escrow>,
pub maker: &'info mut Signer,
// init_if_needed for token accounts
#[account(init_if_needed, payer = taker, token::mint = mint_a, token::authority = taker)]
pub taker_ta_a: &'info mut Account<Token>,seeds / bump
PDA derivation and verification. seeds defines the seed array, bump controls how the bump seed is resolved.
Use bump alone on init to find the canonical bump (calls find_program_address). The discovered bump is stored in ctx.bumps.<field_name> so you can save it in account data. On subsequent instructions, use bump = <expr> with the stored bump to verify the PDA without re-searching -- this saves ~2000 CUs:
// First time: find and store the bump
#[account(init, payer = maker, seeds = [b"escrow", maker], bump)]
pub escrow: &'info mut Account<Escrow>,
// Later: use the stored bump (cheaper)
#[account(seeds = [b"escrow", maker], bump = escrow.bump)]
pub escrow: &'info mut Account<Escrow>,Seed expressions that reference account fields are automatically converted to their address bytes -- write maker not maker.address().as_ref(). You can also use byte literals (b"prefix"), integer bytes (&vault_id.to_le_bytes()), or any &[u8] expression.
has_one
Checks that a field stored in the account's data matches the address of another account in the struct. The field names must match -- if your account has a maker: Address field, then has_one = maker checks escrow.maker == maker.address():
#[account(has_one = maker, has_one = mint)]
pub escrow: &'info mut Account<Escrow>,
pub maker: &'info Signer,
pub mint: &'info Account<Mint>,constraint
Arbitrary boolean expression evaluated after the account is constructed. If it returns false, the instruction fails. Useful for business logic checks that don't fit the other constraint types:
#[account(constraint = escrow.receive > 0)] // reject zero-value escrows
#[account(constraint = clock.slot.get() > vault.unlock_slot.get())] // timelockaddress
Validates that the account's address matches a constant or expression exactly. Good for admin keys, well-known program addresses, or canonical accounts:
const ADMIN: Address = address!("Admin111111111111111111111111111111111111111");
#[account(address = ADMIN)]
pub admin: &'info Signer,Custom errors with @
has_one, constraint, and address all accept @ ErrorVariant to replace the default error with your own. This makes error messages much more useful for debugging and for clients:
#[account(
has_one = maker @ MyError::InvalidMaker,
constraint = escrow.receive > 0 @ MyError::ZeroReceiveAmount,
address = ADMIN @ MyError::Unauthorized,
)]
pub escrow: &'info mut Account<Escrow>,Without @, the defaults are HasOneMismatch, ConstraintViolation, and AddressMismatch.
close
Closes an account after the instruction handler returns. Zeros the discriminator (prevents replay), drains all lamports to the destination account, and reassigns ownership to the System program.
Because close runs in the epilogue (after your handler), you can still read the account's data during execution:
#[account(close = taker, seeds = [b"escrow", maker], bump = escrow.bump)]
pub escrow: &'info mut Account<Escrow>,realloc
Resizes account data at runtime. If growing, lamports are taken from the payer to maintain rent exemption. If shrinking, excess lamports are returned. The realloc payer falls back to the init payer, then to a field named payer:
#[account(realloc = new_size, realloc::payer = authority)]
pub config: Account<MultisigConfig<'info>>,Token / Mint / ATA
The quasar-spl crate provides specialized init attributes for SPL token accounts, mints, and associated token accounts. These handle the token-program-specific CPI setup automatically:
// Token account -- initialized with a specific mint and authority
#[account(init, payer = payer, token::mint = mint, token::authority = authority)]
pub vault: &'info mut Account<Token>,
// Mint -- initialized with decimals and mint authority
#[account(init, payer = payer, mint::decimals = 6, mint::authority = authority)]
pub mint: &'info mut Account<Mint>,
// Associated token account -- derived from mint + owner
#[account(init, payer = payer, associated_token::mint = mint, associated_token::authority = owner)]
pub ata: &'info mut Account<Token>,See the Account Constraints Reference for the full list including mint::freeze_authority, metadata::*, and master_edition::* attributes.
The Bumps Struct
#[derive(Accounts)] generates a bumps struct with one u8 field per PDA and *_seeds() methods for CPI signing:
// Auto-generated for Take
pub struct TakeBumps {
pub escrow: u8,
}
impl TakeBumps {
pub fn escrow_seeds(&self) -> [Seed; 3] { /* [b"escrow", maker, &[bump]] */ }
}Use it in impl blocks:
pub fn withdraw_tokens_and_close(&mut self, bumps: &TakeBumps) -> Result<(), ProgramError> {
let seeds = bumps.escrow_seeds();
self.token_program
.transfer(self.vault_ta_a, self.taker_ta_a, self.escrow, self.vault_ta_a.amount())
.invoke_signed(&seeds)?;
self.vault_ta_a
.close(self.token_program, self.taker, self.escrow)
.invoke_signed(&seeds)
}Validation Pipeline
When an instruction is invoked:
- Header validation -- signer, writable, executable, and duplicate flags are checked against compile-time expectations in a single constant comparison per account.
- Typed construction -- each field is constructed from its
AccountView. - PDA verification and initialization --
init/init_if_neededaccounts are created, PDA addresses are verified. - Constraint evaluation --
has_one,constraint, andaddresschecks run in declaration order. - Epilogue -- after the handler returns,
closeoperations execute.
Next Steps
- Instructions -- the
#[instruction]macro and argument handling - Program Derived Addresses -- PDA seeds, bumps, and verification
- Cross-Program Invocations -- using accounts in CPI calls
- Account Constraints Reference -- full attribute reference
