AIOil Security Shield · Post-mortem scan

We pointed our scanner at the code from the day before two hacks.
It found one of them.

Published 16 August 2026. Both programs below were fixed years ago — this is a re-run on public history, not a disclosure.

What we did

We took the on-chain source of two Solana protocols as it stood the day before each was exploited, and ran it through the same automated pipeline we sell: three AI agents (engineer, hacker, reviewer), no human hints, no knowledge of what the answer was supposed to be.

We are publishing both results — the one it got and the one it missed. A page with only the win is the page nobody believes.

TargetReal root causeTime
Wormhole core bridge
Feb 2022 · $325M
✔ Found — reported Critical, right location, right fix 7m 33s
Cashio brrr / bankman
Mar 2022 · $52M
✘ Missed — flagged a related symptom, not the exploit path 9m 36s

Wormhole — it found the bug

Finding #2 · Critical · api/verify_signature.rs instruction_acc is a raw Info account and is never checked against solana_program::sysvar::instructions::id(). The code parses its data as the serialized instructions sysvar, so an attacker can supply a fake account with crafted bytes that looks like a previous secp256k1 instruction.”

That is the bug. Not an adjacent one — the one. The attacker forges the instructions sysvar, every guardian signature is marked valid without a single ECDSA signature, and the resulting VAA mints wrapped ETH out of nothing.

You can check it yourself in about thirty seconds

Open the pre-patch source. The account is declared untyped:

/// Instruction reflection account (special sysvar)
pub instruction_acc: Info<'b>,          // no type, no owner check

Its data is then parsed twice as the instructions sysvar — load_current_index and load_instruction_at — and in the whole program those two calls are the only places the sysvar appears. There is no comparison against sysvar::instructions::id() anywhere. Nothing establishes that the account handed in is the real sysvar.

The fix the pipeline proposed is the fix that was actually shipped: use a typed Sysvar<Instructions>, or compare the key explicitly.

What it also reported, and what does not hold

The same scan returned five findings. Honesty requires saying which ones survive review:

Critical · does NOT apply
“Permissionless initialize lets attacker seize bridge governance.” The code property is real — initialize has no constraint tying the caller to the deployer. But it requires the bridge accounts to be uninitialized, and the bridge has been live since 2021. We verified the program is deployed and executable on mainnet today. The scenario describes a deployment-window risk, not a live one, and Critical is the wrong severity for a deployed bridge.
High · real, severity arguable
transfer_fees does not update bridge.last_lamports. Verified: last_lamports is written in exactly two places, and the fee-withdrawal path is not one of them, so the next post_message hits MathOverflow. Real — but it needs a governance action to trigger and anyone can unbrick it by topping the collector back up.
Medium · over-rated
post_message fee accounting can be front-run. The mechanism is real, but the source carries a comment saying the behaviour is deliberate — “checking previously known balance allows us to not care who is the payer”. The pipeline flagged an intentional design choice and did not notice the comment saying so.
Low · real
upgrade_guardian_set accepts up to 255 keys while initialize caps at 19 and the signer array is fixed at 19. Verified.

scan_id 6b7986a94058b5a4 · code SHA-256 086aabb627653d90cb7375e0364db27cf87651e4c8b29a5958edbd70fe994168


Cashio — it missed

What the exploit actually was BrrrCommon::validate() and SaberSwapAccounts::validate() perform nine assert_keys_eq! checks. Every one of them compares two caller-supplied accounts to each other. Not one is anchored to a canonical root — a known address, a PDA with fixed seeds, an upgrade authority. An attacker who builds the whole chain out of accounts they control passes all nine.

The codebase knows how to anchor: print_cash checks issue_authority against a hardcoded ISSUE_AUTHORITY_ADDRESS. That discipline was simply never applied to the bank/collateral chain.

What our pipeline said instead

It reported High on Saber reserve balances being usable as a manipulable price source — a real anti-pattern, and a different vulnerability class from the one that was exploited. And it reported Medium on new_bank being callable by anyone, which is a genuine precondition — but it framed it as front-running the deployer to squat the canonical bank, and closed with:

"If multiple banks are intentionally permissionless,
 document it and ignore this finding."

The real attacker never needed to front-run anyone. They created a parallel bank after deployment — allowed by design — and the flaw is that the minting path never checks the bank is the bank. Our pipeline saw the unlocked door and described the wrong way through it.

We are not going to call that a near-miss. It is a miss.

scan_id a5d505580e2aaa5f · code SHA-256 c2783d1c352fc333f310ea48a51187b8f82d65b8a11590c9a5a0b211eff3de69


What we changed because of it

The two runs disagree with each other in a way that is more useful than either result alone. The same structural pattern — an unauthenticated initializer that lets an attacker own the root of a validation chain — was rated Critical on Wormhole and Medium on Cashio. The pipeline is not blind to the pattern. It weighs it inconsistently.

So we wrote the rule down and put it in the reviewer's rulebook:

Rule When a validation chain is only internally consistent — every check compares two caller-supplied accounts and none is anchored to a canonical root — the severity is Critical, not Medium. The attacker can forge the entire chain.

The same rule applies to a High our scanner once found in our own fee router, three days before we shipped it to mainnet. That report is public too, and no badge was issued for it — the system withheld it from us.

See how the scanner works →

AIOil Security Shield runs automated, multi-agent static analysis. It is not a professional human audit and does not guarantee the absence of vulnerabilities — it reduces risk, it does not eliminate it. Both programs discussed here were patched years ago; nothing on this page is an unreported vulnerability, and no source code belonging to a customer is ever published. We make no claim that this scan would have prevented either incident: the scans were run in 2026, on code whose outcome was already public.