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.
Search across all documentation pages
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.
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 signer check on an UncheckedAccount authority
require!(ctx.accounts.authority.is_signer, ErrorCode::MissingSigner);
// Manual owner check on an UncheckedAccount mint (a mint does not sign)
require_keys_eq!(*ctx.accounts.mint.owner, anchor_spl::token::ID, ErrorCode::InvalidOwner);When to reach for this:
AccountInfo from user-supplied metas.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<()> {
// Bind the Pubkey to a local first - `authority.key()` returns by value and the
// temporary would be dropped while `seeds` still borrows it (E0716).
let authority_key = ctx.accounts.authority.key();
let bump = ctx.accounts.vault.bump;
let seeds: &[&[u8]] = &[b"vault", authority_key.as_ref(), &[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:
Signer on authority enforces human approval.has_one = authority ties vault state to the signer pubkey.invoke_signed seeds.is_signer on matching account metas.owner field on accounts restricts which program may write data.AccountInfo checks.| 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 |
// Native program equivalent
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
if vault.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}has_one or explicit constraint = authority.key() == vault.authority.CpiContext::new_with_signer.Account<'info, TokenAccount> already checks the account is owned by the SPL Token program, and .owner on that type is the token authority field, not the program owner. Fix: Add a check on the token authority: constraint = vault_token.owner == vault.key().Sysvar<'info, Clock> typed accounts.| 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 |
Only if your instruction explicitly allows it - never assume payer == authority.
No for user txs - PDAs sign only through invoke_signed inside the program.
Equality between a field on the account (e.g., vault.authority) and another account's key.
Rarely - only for reading immutably verified program IDs you then require_keys_eq!.
Map each state mutation to required signers; grep for UncheckedAccount without constraints.
Any program can read any account - owner checks prevent unauthorized writes and type deserialization trust.
Delegate can move tokens up to delegated_amount - validate delegate signer on delegated transfers.
Constraint syntax is stable - always pin anchor-lang 0.32.1 to match manifest for reproducible audits.
Yes if instruction truly read-only and cannot grief others - still validate account ownership for parsing.
LiteSVM 0.6.x tests with is_signer: false on authority meta - expect MissingRequiredSignature.
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026