EXAMPLE FINDING
Missing Signer Check in a Solana Vault
This is what an Odin Scan finding looks like, end to end: the code, the severity, how an attacker would exploit it, a working proof of concept, and the fix. It comes from the official Solana Developer Bootcamp's "Attack the Bank" exercise (project 12), so it is a real, publicly verifiable program - not a contrived snippet.
update_authority lets anyone change the vault owner without the current authority signing
update_authority writes bank.authority to the caller-supplied new_authority pubkey, but the account struct never requires the current owner to sign. Any wallet can call it, take over the vault, and withdraw every lamport.
The vulnerable code
pub fn update_authority(ctx: Context<UpdateAuthority>) -> Result<()> {
ctx.accounts.bank.authority = ctx.accounts.new_authority.key();
msg!("{:#?}", ctx.accounts.bank);
Ok(())
}Note the account struct: Bank::authority is the vault owner, but the constraint says has_one = authority where authority is a SystemAccount - a non-signer.
Exploit sketch
- An attacker calls
updateAuthoritypassing their own pubkey asnewAuthority. The current owner never signs, so the instruction succeeds. - The vault's stored
bank.authorityis now the attacker. - The attacker calls
withdraw(amount)with themselves asauthority. Thehas_one = authorityconstraint now passes on the attacker's key, and the vault transfers the full balance out.
This is the exact sequence the bootcamp's own test performs after an attacker takes over: update authority, then withdraw. It's a one-transaction steal.
Proof of concept
In the bootcamp test suite, the attacker (the test wallet) is never the original depositor, yet still drains the vault:
// attacker = test wallet, not the depositor
await program.methods
.updateAuthority()
.accounts({ newAuthority: attacker })
.instruction();
const withdrawInstruction = await program.methods
.withdraw(new anchor.BN(amount))
.accounts({ authority: attacker })
.instruction();
// ... send both in one transaction
assert.equal(walletFinalBalance, walletInitialBalance + amount);The fix
Make the current authority a required signer. In Anchor, theSigner constraint or ahas_one backed by a signer check is enough:
#[derive(Accounts)]
pub struct UpdateAuthority<'info> {
pub authority: Signer<'info>, // MUST sign
pub new_authority: SystemAccount<'info>,
#[account(
mut,
has_one = authority, // bank.authority == authority.key()
seeds = [b"bank"],
bump,
)]
pub bank: Account<'info, Bank>,
}With this, only the current vault owner can rotate the authority. The vault configuration cannot be hijacked by an arbitrary wallet.
How Odin Scan catches it
The deterministic rule missing_signer_check flags functions that modify state or move lamports while their Anchor context lacks a Signer constraint or an explicit is_signer check. The deep AI agent independently confirms the same issue and adds the exploit path. On the OdinBench suite, this class of finding is a top-precision detector. Run it yourself on the live demo - paste the vulnerable snippet above and you will get this finding in about three minutes.
Download the full example report
Same content as above, in the formats Odin Scan ships to its subscribers.
Every finding ships like this: severity, exploit path, PoC, fix.
Try the no-account demo, or run the full deep-agent scan on your whole repository with a 7-day Pro trial.