Anchor Constraints Explained
Anchor constraints are the declarative rules that turn a list of transaction accounts into a typed, fail-closed context for your instruction. On Solana, every account is just a pubkey the client chose to pass. Without checks, an attacker can substitute a different mint, a different vault PDA, or a writable account they control. Anchor's #[derive(Accounts)] structs, wrapper types, and #[account(...)] attributes exist so those mistakes fail at validation time instead of in production funds movement.
This page is the umbrella for Accounts, Constraints & Validation. Sibling pages go deep on account types, validation order, seeds and bump, init / init_if_needed, space and rent, custom invariants, and token helpers. Here you get one coherent model so those pages fit as zooms, not separate stories.
Summary
- Anchor validates every field in your accounts struct before the handler runs: wrapper type checks (owner, signer, program id, deserialize), then attribute constraints (
mut,seeds,init,has_one,constraint, token/ATA helpers). - Insight: Solana security is mostly account validation. Constraints are how you encode "this must be our PDA," "this payer funds creation," "this ATA belongs to that mint and authority," and "this field relationship holds" without hand-rolling every check.
- Key Concepts: Account wrappers,
#[account]attributes, validation order, seeds / bump,init/init_if_needed, space / rent exemption, customconstraint, token and ATA constraints, discriminators. - When to Use: Every Anchor instruction. Prefer the strictest type plus the minimum attributes that prove ownership, identity, and mutability for that flow.
- Limitations/Trade-offs: Constraints cannot run CPIs or complex loops; they evaluate expressions over accounts and instruction args. Overusing
init_if_neededorUncheckedAccountwithout guards reintroduces classic reinit and substitution bugs. Wrongspacewastes rent or panics on serialize. - Related Topics: Account Types, Account Validation Order, Seeds & Bump Constraints, init & init_if_needed, Space & Rent, Custom Constraints.
Foundations
Solana programs are stateless. All durable state lives in accounts. The runtime enforces only coarse rules: signer flags, writable flags, and owner-writes (only the owner program may mutate an account's data). Everything else (correct mint, correct PDA, correct admin relationship) is your program's job.
Native Rust programs check those rules with explicit ifs and early returns. Anchor 0.32.1 generates the same class of checks from macros:
Client transaction accounts
|
v
#[derive(Accounts)] -- wrapper types (Signer, Account<T>, Program, ...)
|
v
#[account(...)] -- mut, seeds, bump, init, has_one, constraint, token helpers
|
v
Context<T> ready -- handler runs with typed, validated accountsTwo layers stack:
- Type layer. Choosing
Signer<'info>,Account<'info, Vault>,Program<'info, System>, orUncheckedAccount<'info>decides which automatic checks run (signature, owner + discriminator + Borsh load, program id, or none). - Attribute layer.
#[account(mut, seeds = ..., bump, has_one = authority, constraint = ...)]composes additional rules. All attributes on a field must pass.
Clients built with @solana/kit 7.0.0 (or Anchor's TS client) still assemble the full account list. Constraints do not fetch missing keys; they only accept or reject what the transaction already declared. Your IDL and client code must pass the same pubkeys, seeds, and programs the macros expect.
Mechanics & Interactions
Account types as the first filter
Pick the strictest wrapper that matches the role:
| Type | Automatic checks |
|---|---|
Signer<'info> | is_signer |
Account<'info, T> | Owner is this program, 8-byte discriminator, deserialize T |
InterfaceAccount<'info, T> | Token-2022 / interface-compatible owners |
Program<'info, T> | Matches program id for T |
SystemAccount<'info> | Owned by the System Program |
UncheckedAccount<'info> | None (you must document and enforce checks) |
AccountLoader is for zero-copy layouts; Interface / TokenInterface matter when supporting both SPL Token and Token-2022. Using UncheckedAccount for funds or authority paths is a deliberate escape hatch, not a default. Details: Account Types.
Validation order
Anchor does not "guess" based on struct field order alone. It applies a deterministic pipeline roughly: type and owner checks, signer and mutability flags, address and program constraints, init / init_if_needed (create when required), seeds and other attributes, custom constraint expressions, then full data load for typed accounts.
Practical consequences:
- Payer and system program must be valid before
initcan allocate. - Accounts referenced in
seedsneed to be present and keyed correctly for PDA derivation. - You cannot CPI from a constraint expression; side effects belong in the handler after validation.
- Debugging a failure means reading Anchor's constraint error (and logs), not reshuffling field order and hoping.
See Account Validation Order when designing create-then-use flows or hunting "why did init fail before my handler?"
Seeds and bump: proving PDAs
Program Derived Addresses (PDAs) are addresses controlled by your program via seeds. Constraints make that proof declarative:
#[account(
seeds = [b"vault", authority.key().as_ref()],
bump = vault.bump,
)]
pub vault: Account<'info, Vault>,seeds = [...]must match client-side derivation (byte order, endianness for integers, account key order).bumponinitfinds the canonical (highest) bump; store it in account data.bump = account.bumpon later instructions reuses the stored value so you avoid bump search every time.
Wrong seeds mean a different address, not a soft mismatch. Cross-program PDAs need explicit program targeting; default derivation is under the current program id. Full patterns: Seeds & Bump Constraints.
init and init_if_needed
init creates a new account in the same instruction: system CPI allocate/assign (and fund), set owner to your program, size from space. It fails if the account already exists. Typical companions: payer = ..., space = 8 + T::INIT_SPACE, optional seeds + bump, and system_program.
init_if_needed creates only when the account is missing. That is convenient for idempotent onboarding, and dangerous when an existing account is accepted without proving it is your correctly initialized state. Prefer plain init for high-value vaults; if you need idempotency, add hard constraints on discriminator, authority, and seeds when the account already exists.
#[account(
init,
payer = user,
space = 8 + Profile::INIT_SPACE,
seeds = [b"profile", user.key().as_ref()],
bump,
)]
pub profile: Account<'info, Profile>,Requirements: mutable signer payer with enough SOL, system program present, space large enough for serialize. Deep dive: init & init_if_needed.
Space and rent
Solana accounts that hold data must remain rent-exempt. Anchor's space is the allocated byte length. For normal Anchor accounts that is 8 (discriminator) + payload. Use #[derive(InitSpace)] and T::INIT_SPACE, with #[max_len(n)] on String / Vec fields so size is known at compile time.
Under-allocate and serialization panics or corrupts state. Over-allocate and users overpay rent. Growing later needs realloc (and careful migration). Closing accounts should return lamports to a recipient via close = ... so rent is not stranded. Details: Space & Rent.
Custom constraints and relationships
Built-ins cover a lot (mut, has_one, address, owner, seeds, init). Domain rules use constraint = <bool expr>, optionally mapped with @ MyError::Variant for clear client errors:
#[account(
mut,
has_one = admin,
constraint = config.fee_bps <= 10_000 @ ConfigError::FeeTooHigh,
)]
pub config: Account<'info, Config>,- Multiple constraints AND together.
#[instruction(amount: u64)]brings instruction args into expressions.- Prefer declarative checks for static relationships; use
require!in the handler for oracle-dependent, looping, or remaining-accounts logic.
Reusable constraint modules and helpers keep large programs consistent. See Custom Constraints and sibling coverage of common #[account] attributes.
Token and ATA constraints (concept)
Token flows fail in production when mint, owner, or token program id do not match. anchor-spl adds helpers so you do not hand-check every field:
token::mint/token::authorityon token accountsassociated_token::mint/associated_token::authorityfor ATAsinit/init_if_neededon ATAs with associated token + system + token programs in the accounts list
For Token-2022, prefer interface types (InterfaceAccount, TokenInterface) so owner checks accept either legacy Token or Token-2022. Always mark source and destination token accounts mut when balances change. Conceptually: pin mint identity, authority, and program id the same way you pin vault PDAs with seeds.
Advanced Considerations & Applications
Constraints are a security architecture tool, not syntax sugar. In design reviews, map every sensitive account to (1) a type, (2) identity proof (seeds, address, ATA derivation), (3) relationship proof (has_one, mint match), and (4) lifecycle (init, close, realloc).
| Pattern | Constraint shape | Watch for |
|---|---|---|
| User profile PDA | init + seeds + bump + space | Client seed order; store bump |
| Config singleton | seeds [b"config"], has_one = admin | Upgrade authority vs admin role |
| Vault withdraw | seeds, constraint on balances, token mint match | CEI: validate then mutate then CPI |
| Idempotent ATA ensure | init_if_needed + associated_token::* | Reinit only safe with strong existing checks |
| Token-2022 path | InterfaceAccount + TokenInterface | Wrong program id fails transfer |
remaining_accounts sit outside the typed struct: validate each key and owner in the handler before use. Zero-copy and realloc change sizing and loader types but not the need for seeds and authority constraints.
On Agave 4.1.1 with Solana CLI 3.0.10, local tests (anchor test, Surfpool, LiteSVM) should assert both success paths and deliberate constraint failures (wrong mint, wrong PDA, missing mut). Failures at validation are cheaper and safer than partial handler execution.
Anchor 0.32.1 continues to generate IDL account metas from these structs: clients see which accounts are writable and which are required. Keep constraints and client builders in sync after seed or space changes.
Common Misconceptions
- "If it compiles, accounts are safe." Types help; substitution attacks need seeds, mint, and authority constraints too.
- "Field order in the struct is validation order." Attribute and type kinds drive the pipeline; do not rely on reordering fields to fix init bugs.
- "
init_if_neededis always safer UX." It is often weaker security. Preferinitunless existing-state constraints are airtight. - "Space is just the Rust sizeof the struct." Add the 8-byte discriminator; bound dynamic fields; rent depends on allocated size.
- "
bumpalone without seeds is enough." Seeds define the address; bump selects among valid off-curve points for those seeds. - "UncheckedAccount is fine if I check in the handler later." It is fine only when you actually check every path; missing a path is a common exploit class.
- "Token accounts only need to be mutable." Mutability without mint/authority/ATA constraints still allows wrong-account drains or failed CPIs.
- "Custom constraints replace the need for typed accounts." Use both: types for owner and layout, constraints for domain invariants.
FAQs
What problem do Anchor constraints solve?
They turn client-supplied account lists into validated, typed contexts so wrong owners, wrong PDAs, missing signers, and broken relationships fail before your business logic runs.
What is the difference between account types and #[account] attributes?
Types apply baseline checks (signer, owner, discriminator, program id). Attributes add lifecycle and relationship rules (init, seeds, mut, has_one, custom expressions, token helpers).
When does validation run relative to my handler?
All account validation for the Context completes first. Your instruction function body runs only after the accounts struct is fully built and constraints pass.
Why do seeds and bump belong in constraints?
They prove the passed account is the PDA your program expects for given seeds. Declaring them once keeps handlers free of repetitive derivation and fails closed on mismatch.
Should I use init or init_if_needed?
Use init when the account must be created exactly once. Use init_if_needed only for idempotent flows where existing accounts are still fully validated (type, seeds, authority, domain constraints).
How do I size space correctly?
Use space = 8 + T::INIT_SPACE with InitSpace (and max_len on dynamic fields). Never omit the discriminator. Fund rent-exempt lamports via a mutable payer.
What does a custom constraint expression do?
It evaluates a boolean over accounts and instruction args. Attach @ Error::Variant so clients and logs get a specific failure code instead of a generic constraint error.
How do token and ATA constraints fit this model?
They are specialized identity checks: the token account must match mint and authority (and usually the associated address derivation). They sit alongside seeds and mut the same way vault identity does.
Can constraints perform CPIs?
No. Creation via init is framework-managed. Arbitrary CPIs and multi-step logic belong in the handler after validation succeeds.
Do I still need require! in handlers?
Yes for dynamic or multi-step rules (oracles, remaining accounts, loops). Prefer declarative constraints for static relationships that are known at validation time.
What stack versions does this section target?
Examples align with Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, and clients such as @solana/kit 7.0.0.
Where should I go next after this page?
Read Account Types and Account Validation Order, then Seeds & Bump Constraints, init & init_if_needed, Space & Rent, and Custom Constraints as you implement real instructions.
Related
- Account Types - Signer, Account, Program, SystemAccount, UncheckedAccount
- Account Validation Order - when types, init, and constraints run
- Seeds & Bump Constraints - PDA identity with seeds and stored bump
- init & init_if_needed - create lifecycle and reinitialization risk
- Space & Rent - sizing, discriminator, rent-exempt funding
- Custom Constraints - constraint expressions, has_one, typed errors
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, and @solana/kit 7.0.0.