Most Solidity scanners are high-recall, low-precision. They flag 40 things, 38 are noise, and after the third report you stop reading them — so the one real bug ships. Precision, not recall, is what makes a security tool actually get used.
I've been building OpenClaw, a heuristic Solidity scanner with the opposite bar: silence on sound code. To pressure-test it, I pointed it at six codebases that top firms have already audited — Yearn, Sablier, Ajna, Liquity, and a couple of smaller protocols — and hand-verified every single flag.
The result: 14 HIGH/CRITICAL candidates across the six. Every one was a false positive.
That sounds like a failure. It's the whole point — and each FP is a lesson in the exact traps that fool most scanners. Here they are.
1. The "donation attack" that isn't (a PaymentSplitter)
The scanner flagged balanceOf(address(this)) used in accounting as a donation / share-inflation attack. But the contract was a fork of OpenZeppelin's PaymentSplitter: shares are fixed at deployment, and a "donation" is exactly the input that gets split proportionally among those fixed shares. There's no share to mint, no share price to manipulate, no first-depositor. balance + totalReleased is the canonical, correct accounting.
Lesson: balanceOf(this) in accounting is only a donation risk when shares are minted against it. In a fixed-share pull-payment splitter, it's intended behavior.
2. The "unprotected initialize" with a guard the scanner didn't recognize (Ajna)
Ajna's pools were flagged as "unprotected initialize()". But there it was, first line:
if (isPoolInitialized) revert AlreadyInitialized();
Enter fullscreen mode Exit fullscreen mode
A one-time-init guard. Plenty of non-OpenZeppelin protocols protect initialize with a boolean flag + revert instead of the initializer modifier. A detector that only knows the OZ modifier misses it and screams.
Lesson: an initializer is protected if it has any one-time guard — the OZ modifier, a boolean flag that reverts, factory/clone init, or _disableInitializers().
3. "Read-only reentrancy" in functions nobody uses as an oracle (Ajna, Liquity)
poolBalanceDetails(), flashLoan(), simulateRedemption() — all flagged for reading a balance after an external call. Read-only reentrancy (the Curve/Balancer class that paid $100k+ bounties) is only exploitable if an external protocol consumes the function as a price oracle. A view utility in a Multicall helper, a flash loan's repayment check, a simulate* function — none of those are oracles.
Lesson: read-only reentrancy needs an oracle consumer. simulate / preview / *Details / multicall / flashLoan aren't oracles → not exploitable.
4. The "manipulable rate" that's bounded and role-gated (Ember)
Flagged CRITICAL: "rate can be updated." But the rate was managed and bounded — a rateManager role, a maxRateChangePerUpdate cap, a rateUpdateInterval, and events on every change. A trusted, bounded, time-gated exchange rate is intended design, not an exploit. Even a malicious manager can only nudge it by the cap per interval.
Lesson: a role-updatable value bounded by max-change + interval is a managed parameter, not a vulnerability (at most a centralization note).
5. "Fee-on-transfer not supported" — by design (Liquity)
Flagged for not handling fee-on-transfer tokens. But Liquity's BOLD uses a vetted, fixed set of collateral (WETH/wstETH-class), not arbitrary ERC-20s. "Doesn't support FoT" is a deliberate decision for a protocol with curated collateral.
Lesson: FoT-not-supported is only a bug if the protocol accepts arbitrary tokens. With vetted collateral, it's intended.
Why this matters
Every one of these is a place a naive scanner shouts "CRITICAL" and a good auditor quietly says "no." The value of a low false-positive rate isn't that the tool finds less — it's that when it (or I) stay silent, the silence means something. A tool that cries wolf 38 times out of 40 trains you to ignore the 39th. The 39th is the real one.
Each of these five FP classes is now a permanent fix in the detector, not a patch — so the next PaymentSplitter, the next flag-guarded initialize, the next view utility, doesn't fire. That's how you drive false positives toward zero without going blind to the real thing.
The scanner
OpenClaw is calibrated against the codebases everyone treats as a gold standard — OpenZeppelin, Solady, Solmate, Uniswap v2/v3/v4, Permit2, Morpho Blue, PRBMath. 608 source files, 14 total flags, 0 across the entire OpenZeppelin library. MIT, pure Python, runs as a GitHub Action that comments on every PR:
pipx run --spec git+https://github.com/juan23z/openclaw-audit openclaw-audit <repo> --out ./report
Enter fullscreen mode Exit fullscreen mode
Honest about what it is not: it's heuristic, not formal verification. It catches classes of bugs (access control, vault math, reentrancy shape, oracle staleness, upgradeability) — not your protocol's bespoke economic logic bug, the one where two functions interact in a way nobody drew on the whiteboard. That still needs a human. But the silence is honest.
If you're shipping to mainnet and want a second set of eyes — hand-verified findings, zero false-positive spam, a plain-English report in 48h — I do fast pre-mainnet reviews. Or just run the free scanner and keep the signal.
I'd genuinely like feedback on the false-positive classes above — which ones have bitten you with Slither/others?
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.