Weakspot

The missing signer check, Solana's most expensive one-line bug

How an Anchor instruction ends up with no authority verification at all: AccountInfo instead of Signer, a missing has_one constraint, and what the fixed account struct looks like.

Updated 2026-09-03

Anchor does an enormous amount of validation for you, which is exactly why this bug is so easy to ship: the framework only checks what you declare in the account struct, and an omission there is silent. There is no warning, no runtime error, and the happy path works perfectly.

The vulnerable program

A config program storing an authority and a fee rate. Only the authority should be able to change the fee.

lib.rs — update_fee_rate never verifies its caller
pub fn update_fee_rate(ctx: Context<UpdateFeeRate>, new_fee_bps: u16) -> Result<()> {
    require!(new_fee_bps <= 10_000, ConfigError::FeeTooHigh);
    let config = &mut ctx.accounts.config;
    config.fee_bps = new_fee_bps;
    Ok(())
}

#[derive(Accounts)]
pub struct UpdateFeeRate<'info> {
    #[account(mut)]
    pub config: Account<'info, Config>,
    /// CHECK: not constrained
    pub authority: AccountInfo<'info>,
}

Two things are missing, not one

  • authority is an AccountInfo, not a Signer. Anchor therefore never checks that this account signed the transaction. Anyone can pass any public key here.
  • There is no has_one = authority constraint on config. Even if the account did have to sign, nothing ties it to the config.authority stored on-chain — so any signer would do.

Either omission alone is fatal. Together they mean update_fee_rate has no access control whatsoever: any wallet on Solana can call it and set the fee to anything up to 100%. The require! on new_fee_bps validates the value and creates a convincing impression that the instruction is guarded. It validates nothing about the caller.

The fix

Both constraints restored
#[derive(Accounts)]
pub struct UpdateFeeRate<'info> {
    #[account(mut, has_one = authority)]
    pub config: Account<'info, Config>,
    pub authority: Signer<'info>,
}

Signer makes Anchor verify the signature. has_one = authority makes it verify that the signing key equals config.authority. Two words, and the instruction is correctly gated — the handler body does not change at all.

Keep reading