Constraints
Ethereum guards privileged functions with modifiers like onlyOwner. Anchor moves the check onto the instruction's account struct - has_one = owner runs before the handler and surfaces in the IDL.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// `onlyOwner` is a modifier - it runs before the function body. Same idea
/// applies anywhere there's a guard around a privileged function.
contract Vault {
address public owner;
uint16 public feeBps;
owner is a single address on the contract. Nothing on the type system distinguishes it from any other address field - the "privileged" semantics are implied by the modifier that reads it later, not by the declaration itself.
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
onlyOwner runs require(msg.sender == owner) and then yields to the function via _;. It's a one-line guard, a code pattern, and a security mistake away from "forgot to add the modifier on the new function".
function setFee(uint16 newFee) external onlyOwner {
feeBps = newFee;
}
Every privileged function has to remember to apply onlyOwner. Adding a function later inherits no defaults - the check has to be remembered. Reviewers scan function-by-function looking for the missing decoration.
}
// Anchor has no modifier syntax. The equivalent check moves out of the
// handler body and onto the `#[derive(Accounts)]` struct - `has_one = owner`
// plus a `Signer<'info>` named `owner` does what `onlyOwner` did, but the
// check runs at account validation, before any state-mutating code.
use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");
#[program]
pub mod vault {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.config.owner = ctx.accounts.payer.key();
ctx.accounts.config.fee_bps = 0;
Ok(())
}
// Handler is now just business logic - the access check is enforced by
// the `SetFee` struct below.
pub fn set_fee(ctx: Context<SetFee>, new_fee: u16) -> Result<()> {
ctx.accounts.config.fee_bps = new_fee;
Ok(())
}
}
By the time set_fee runs, both the signer and the has_one field match have already been verified. No require!(...) for access at the top - there's nothing to forget.
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + 32 + 2,
seeds = [b"config"],
bump,
)]
pub config: Account<'info, Config>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct SetFee<'info> {
#[account(
mut,
seeds = [b"config"],
bump,
has_one = owner, // <- the modifier, declarative
Anchor has no modifier syntax and doesn't need one. The same check moves out of the function body and onto the #[derive(Accounts)] struct. has_one = owner plus a Signer<'info> field named owner says "the account's owner field must equal the owner signer in this instruction". Wrong signer → instruction reverts at account validation, before any state can be mutated.
)]
pub config: Account<'info, Config>,
pub owner: Signer<'info>,
}
A Solidity reviewer scans functions for missing onlyOwner. A Solana reviewer scans #[derive(Accounts)] structs and sees every access rule at the top of every instruction. Constraints also surface in the program's IDL, so off-chain tooling can read the access model without parsing function bodies. Other useful constraints in the same family: constraint = x == y @ MyError::Bad, seeds = [...] + bump, address = HARDCODED_KEY. All run before the handler.
#[account]
pub struct Config {
pub owner: Pubkey,
pub fee_bps: u16,
}