All articles
Solana·May 31, 2026·5 min read

Is a Solana Program Audit Worth It Before Mainnet Launch?

Is a Solana program audit worth it before mainnet? We weigh audit cost against real exploits—account confusion, CPI reentrancy, missing signer checks.

The bugs that actually drain Solana programs

Three bug classes account for most of the money lost to Solana program exploits, and none of them are exotic. They're boring, mechanical mistakes that a careful second pair of eyes catches in an afternoon.

Account confusion. Solana programs receive accounts as raw pubkeys plus data blobs — the runtime doesn't enforce that the account passed into an instruction is actually the type your code assumes it is. If you don't check the account's owner program and discriminator, an attacker can hand you a lookalike account (or one they crafted themselves) and your program will happily deserialize it as if it were legitimate state. Cashio lost $52M in March 2022 this way — a fake collateral account passed validation because the program never verified it was minted by the real bank.

CPI reentrancy. Cross-program invocations (CPIs) let one program call into another mid-instruction. If your program updates state after making a CPI rather than before, a malicious callee can call back into you while your account balances are still in a stale, exploitable state. This is the same reentrancy bug class Ethereum has dealt with since the DAO hack, just wearing a different runtime.

Missing signer checks. An AccountInfo can claim is_signer: true or false, but it's your instruction handler's job to actually verify that the accounts that should have signed — the authority, the owner, the admin — did sign. Skip that check and anyone can construct a transaction that impersonates the real authority using nothing but the public key, which is, by definition, public.

Crema Finance lost $8.8 million in July 2022 to a variant of account confusion: an unvalidated tick account let an attacker fabricate fake liquidity positions and drain a flash-loan-funded swap. Mango Markets' $114M implosion in October 2022 wasn't a raw code bug so much as an oracle-price manipulation that the program's risk logic didn't defend against — a reminder that audits also need to reason about economic assumptions, not just memory safety.

What an audit actually costs versus what a bug costs

A serious audit of a mid-sized Anchor program — say, an AMM, a lending market, or a bot with on-chain settlement logic — runs somewhere between $8,000 and $40,000 depending on line count, CPI surface area, and whether it touches token custody. A full-scope audit on something with real complexity (multiple pools, oracle integrations, governance) can run higher. That's real money for a small team.

Compare that to the exploits above. Cashio: $52M. Mango: $114M. Wormhole (a bridge, not a pure Solana program, but instructive): $325M, caused by a signature verification function that failed to check that a system account matched the expected sysvar. In every one of these, the fix would have been a handful of lines. The audit spend that would have caught it was a rounding error against the loss.

The ROI case isn't really about probability — it's about asymmetry. A $20K audit against a program holding $2M in TVL isn't a bet that you'll definitely get hacked; it's insurance against a tail event that, on Solana's track record, happens often enough to be the base rate, not the exception.

A worked example: the signer check that isn't there

Here's a stripped-down Anchor instruction that looks fine at a glance and isn't.

#[derive(Accounts)]
pub struct WithdrawFunds<'info> {
    #[account(mut)]
    pub vault: Account<'info, Vault>,
    /// CHECK: authority of the vault
    pub authority: AccountInfo<'info>,
    #[account(mut)]
    pub destination: AccountInfo<'info>,
}

pub fn withdraw(ctx: Context<WithdrawFunds>, amount: u64) -> Result<()> {
    require_keys_eq!(ctx.accounts.vault.authority, ctx.accounts.authority.key());
    **ctx.accounts.vault.to_account_info().try_borrow_mut_lamports()? -= amount;
    **ctx.accounts.destination.try_borrow_mut_lamports()? += amount;
    Ok(())
}

That require_keys_eq! check looks like it's doing its job — it confirms the authority account passed in matches the vault's stored authority. But nothing here checks authority.is_signer. An attacker who knows your vault's authority pubkey (which is on-chain and public) can build a transaction that passes that same pubkey as the authority account without ever holding its private key. The key-match check passes; no signature was ever required.

The fix is one line:

#[account(mut)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>,
#[account(mut)]
pub destination: AccountInfo<'info>,

Switching the type from AccountInfo to Signer makes Anchor's account validation layer enforce is_signer automatically at deserialization, before your instruction body even runs. This is exactly the kind of bug that's invisible in a normal code review — the logic reads correctly — but jumps out immediately to someone doing a line-by-line audit against Anchor's constraint system, or running a static analyzer like Sec3's X-ray or Soteria configured to flag unconstrained AccountInfo fields.

What a real audit covers that a scanner doesn't

Automated tools (X-ray, Soteria, cargo audit for dependency CVEs) are good at flagging missing constraints, unsafe arithmetic, and known-vulnerable crate versions. They're fast and cheap, and you should run them on every PR. But they don't reason about your program's economic logic — whether your liquidation threshold makes sense under a flash-loan price swing, whether your CPI ordering creates a reentrancy window, whether two instructions can be combined in a single transaction to bypass a check that assumes sequential execution. That's manual review territory, and it's where the money-losing bugs tend to hide.

If you're running infrastructure that talks to Solana state directly rather than just holding funds — a wallet tracker, a market maker, or anything ingesting live chain data via Yellowstone gRPC — the audit scope should extend to how your off-chain logic trusts on-chain state, not just the program itself.

When you can skip it

Not every deploy needs a full audit. An internal tool with no user funds, a devnet prototype, a sniper bot or copytrading bot running entirely from a wallet you control with no external depositors — the blast radius is your own capital, and you already accepted that risk. The calculus changes the instant a program custodies funds it doesn't own, exposes an instruction to arbitrary callers, or gets composed into by other programs via CPI, which is also where reentrancy and MEV-adjacent risks start compounding. At that point, an audit isn't overhead — it's the cost of being allowed to hold other people's money.

If your program is heading to mainnet with real deposits behind it, get a Solana smart contract audit scoped before you flip the switch, not after your users find the bug for you.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#Solana#Smart Contract Audit#Security#Anchor#Program Development