Who audits the auditor?
I built a small Solidity static-analysis tool — scaudit, a dependency-free scanner that reads a contract and flags reentrancy, tx.origin auth, unchecked low-level calls, unprotected selfdestruct, and a dozen other things, plus some gas hints. Writing the detectors is the easy, satisfying part. It’s also the part that means almost nothing, because a security tool’s entire worth is whether you can trust its verdict — and the way that trust dies isn’t a crash, it’s a quiet lie.
A security scanner fails in two directions, and both are silent:
- False negative — a green light on vulnerable code. The worst outcome, because it manufactures confidence. You ran the tool, it said clean, you shipped the reentrancy.
- False positive — a red flag on safe code. Less dangerous but corrosive: enough of them and people stop reading the output, which converts every real finding into a false negative by way of fatigue.
So the interesting phase of building this wasn’t the detectors. It was handing the finished detectors to a different, stronger model — one that hadn’t written them and had no stake in them looking good — with a single instruction: construct the contracts these miss, and the safe ones they wrongly flag.
What it found
The detectors were heuristics — regex and brace-counting over source, no compiler, no AST — and heuristics break on structure. The auditor found exactly where:
- A function’s
returns (bool)clause was being parsed into its modifier list. So a function that returned a value looked, to the access-control detector, like it had a guarding modifier — and its findings were suppressed. False negatives, by punctuation. receive()andfallback()weren’t being segmented as functions at all, so aselfdestructsitting in a fallback was simply invisible.- A
nonReentrantmodifier silenced unrelated checks, because the engine treated “has any modifier” as “is access-controlled.” - On the other side:
tx.originpassed to an event got flagged as phishable auth (it’s a log), anduint x = 5got flagged as a reentrancy state-write (it’s a declaration). Crying wolf.
Each one became a regression fixture — a tiny contract that must trip the detector, or must not. The suite went from 29 tests to 46. And then I did the part the fleet can’t do for me: re-ran them myself, and pointed the CLI at a vulnerable fixture (one HIGH finding, correct line) and a clean one (no findings) to watch recall and precision with my own eyes.
The honest footer
scaudit is a first-pass triage aid. It has no control-flow graph, no taint analysis, no inheritance resolution; it reads one file at a time; its access-control detector is high-false-positive by nature. Slither compiles the contract and reasons over an IR; Mythril runs symbolic execution; real coverage is fuzzing and formal verification. I put all of that in the README in plain words, because a tool that overstates its reach is just a better-dressed false negative.
But the structure is the point worth keeping. The same agent workflow that built scaudit is, itself, this idea: a builder model and a separate, sharper auditor model that exists only to disbelieve it. The most useful thing you can do with a second model isn’t more output — it’s adversarial doubt aimed at the first one. Who audits the auditor? A different auditor, who was told to assume it’s lying.