Anchor Constraint Bypass Remediation
How to fix missing or insufficient Anchor account constraints that allow unauthorized account substitution.
Anchor Constraint Bypass Remediation
Overview
Related Detector: Anchor Constraint Bypass
Anchor’s #[derive(Accounts)] macro generates account validation at deserialization, but only for the constraints the developer explicitly declares. Missing has_one, seeds, signer, or constraint attributes leave gaps that allow attackers to substitute unrelated accounts.
Recommended Fix
Before (Vulnerable)
#[derive(Accounts)]
pub struct UpdateConfig<'info> {
#[account(mut)]
pub config: Account<'info, Config>,
pub authority: Signer<'info>,
}
The authority is a signer, but Anchor never verifies it matches config.authority. Any signer can update any config account.
After (Fixed)
#[derive(Accounts)]
pub struct UpdateConfig<'info> {
#[account(
mut,
has_one = authority @ ErrorCode::Unauthorized,
)]
pub config: Account<'info, Config>,
pub authority: Signer<'info>,
}
The has_one = authority constraint forces Anchor to verify config.authority == authority.key() before the instruction body runs.
Alternative Mitigations
Use seeds for PDA accounts
#[account(
mut,
seeds = [b"config", authority.key().as_ref()],
bump = config.bump,
)]
pub config: Account<'info, Config>,
The PDA derivation binds the config to a specific authority — a different authority can never derive the same address.
Use constraint for invariants Anchor cannot infer
#[account(
mut,
constraint = vault.owner == authority.key() @ ErrorCode::WrongOwner,
constraint = amount <= vault.balance @ ErrorCode::InsufficientFunds,
)]
pub vault: Account<'info, Vault>,
Use constraint for relationships across accounts or against instruction parameters.
Add address = ... for fixed program IDs
#[account(address = expected_program_id::ID)]
pub external_program: Program<'info, ExternalProgram>,
Common Mistakes
Assuming Signer<'info> is enough. A signer is just a public key with a valid signature. Without has_one or seeds, the signer is not bound to any specific account.
Forgetting @ ErrorCode. Without an explicit error mapping, Anchor returns a generic error that is harder to diagnose. Always attach a custom error code so users can identify which constraint failed.
Mixing Account and AccountInfo. AccountInfo skips Anchor’s discriminator check. Use Account<'info, T> whenever the account stores program-defined state, so the type acts as a constraint.
Trusting init without seeds. #[account(init, payer = authority, space = 8 + 32)] allocates an account but does not bind its address. Combine with seeds and bump to make the address deterministic.