Signer & Owner Checks
Missing signer and owner validation causes more Solana program exploits than any other bug class. Every instruction must prove the right accounts signed and belong to the expected programs.
Recipe
Quick-reference recipe card - copy-paste ready.
#[derive(Accounts)]
pub struct TransferOut<'info> {
#[account(mut, has_one = authority)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>,
}
// Manual check for UncheckedAccount
require!(ctx.accounts.mint.is_signer, ErrorCode::MissingSigner);
require_keys_eq!(*ctx.accounts.mint.owner, anchor_spl::token::ID, ErrorCode::InvalidOwner);When to reach for this:
- Any instruction moving lamports or tokens.
- Authority changes and config updates.
- Accepting
AccountInfofrom user-supplied metas. - Reviewing audit findings on access control.
Working Example
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
#[account]
pub struct Vault {
pub authority: Pubkey,
pub bump: u8,
}
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(
mut,
seeds = [b"vault", authority.key().as_ref()],
bump = vault.bump,
has_one = authority,
)]
pub vault: Account<'info, Vault>,
#[account(mut)]
pub vault_token: Account<'info, TokenAccount>,
#[account(mut)]
pub destination: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
let seeds = &[b"vault", ctx.accounts.authority.key().as_ref(), &[ctx.accounts.vault.bump]];
let signer = &[&seeds[..]];
let cpi = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.vault_token.to_account_info(),
to: ctx.accounts.destination.to_account_info(),
authority: ctx.accounts.vault.to_account_info(),
},
signer,
);
token::transfer(cpi, amount)?;
Ok(())
}What this demonstrates:
Signeronauthorityenforces human approval.has_one = authorityties vault state to the signer pubkey.- PDA signs token transfer via
invoke_signedseeds.
Deep Dive
How It Works
- Transaction includes an Ed25519 signature per signing key.
- Runtime sets
is_signeron matching account metas. ownerfield on accounts restricts which program may write data.- Anchor macros encode common signer/owner patterns; native code uses
AccountInfochecks.
Check Matrix
| Account type | Signer check | Owner check |
|---|---|---|
Signer<'info> | Automatic | N/A |
Account<'info, T> | Manual if needed | Automatic (your program) |
Account<'info, TokenAccount> | Manual | Token program |
UncheckedAccount | Manual required | Manual required |
Rust Notes
// Native program equivalent
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
if vault.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}Gotchas
- Signer on wrong account - Fee payer signs but authority does not. Fix:
has_oneor explicitconstraint = authority.key() == vault.authority. - PDA as authority without invoke_signed - CPI fails or uses wrong signer. Fix: Pass seeds to
CpiContext::new_with_signer. - Token account owner - Vault token owned by Token program, not yours. Fix: Validate
vault_token.owner == token_program::IDvia Anchor SPL types. - Sysvar as UncheckedAccount - Attacker passes fake sysvar. Fix: Use
Sysvar<'info, Clock>typed accounts. - Multisig - SPL multisig requires extra signer metas. Fix: Match Token program multisig layout.
- Closed account revival - Closed account can be reallocated same tx in some patterns. Fix: See reinitialization guards.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
has_one constraint | Single pubkey field on state | Composite authority logic |
| Custom authority PDA | Program-controlled custody | User must directly sign |
| Governance program CPI | DAO-controlled upgrades | Simple wallet auth |
FAQs
Does the fee payer count as authority?
Only if your instruction explicitly allows it - never assume payer == authority.
Can a PDA be a Signer in the account macro?
No for user txs - PDAs sign only through invoke_signed inside the program.
What does has_one check?
Equality between a field on the account (e.g., vault.authority) and another account's key.
Is AccountInfo ever safe without checks?
Rarely - only for reading immutably verified program IDs you then require_keys_eq!.
How do I audit signer coverage?
Map each state mutation to required signers; grep for UncheckedAccount without constraints.
Do owner checks stop data reads?
Any program can read any account - owner checks prevent unauthorized writes and type deserialization trust.
What about token delegate signers?
Delegate can move tokens up to delegated_amount - validate delegate signer on delegated transfers.
How does Anchor 0.32.1 differ?
Constraint syntax is stable - always pin anchor-lang 0.32.1 to match manifest for reproducible audits.
Can I skip signer for view-only ix?
Yes if instruction truly read-only and cannot grief others - still validate account ownership for parsing.
How do I test missing signer?
LiteSVM 0.6.x tests with is_signer: false on authority meta - expect MissingRequiredSignature.
Related
- Security Basics - threat model intro
- Account Validation Attacks - type confusion
- PDA & Seed Attacks - PDA authority
- Sealevel Attacks Catalog - full checklist
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.