Skip to main content
Sigvex

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

the account-validation framework’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.

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 the account-validation framework’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.

References