Every automated Solidity security tool has the same disease: it cries wolf. Run one on an audited protocol and you get 600 "findings," 98% of which are noise. The tragedy isn't the wasted time — it's that after the tenth false alarm, you stop reading. The one real bug then hides in the noise.
I spent this week hand-verifying every flag my scanner produced against production protocols (Ember, Euler, Liquity, Arcadia, Rubicon, and more). Every single one was a false positive. Here are five of the most common classes, why a naive tool reports them, and the deterministic check that kills each — no AI guesswork required.
1. The "spec violation" that's just... the design
A tool reads a spec or a NatSpec comment — "only the rate manager can update the rate" — and flags the function as a violation because it "can't prove" the restriction. On Ember's vaults this produced a CRITICAL:
function pause() external onlyGuardian { ... }
function processWithdrawalRequests(uint256 n) external onlyOperator { ... }
function setMaxTVL(uint256 v) external onlyAdmin { ... }
Every one is a correctly access-controlled, intended feature. The tool listed the protocol's own role design and called it a bug.
The fix: before emitting, find the affected function and check whether the restriction is actually enforced (onlyX / onlyRole / require(msg.sender == ...)). If it is, it's the design, not a violation. If there's genuinely no guard, it still fires. Safe direction.
2. Fee-on-transfer on a token that can't be fee-on-transfer
A vault does token.transferFrom(user, address(this), amount) and uses amount for accounting. Fee-on-transfer tokens arrive short, so the internal books inflate → the tool screams "insolvency."
Real? Only if users can deposit arbitrary tokens. Two very common cases where they can't:
// (a) the deposit is onlyOwner — the owner picks what enters
function deposit(address token, uint amount) external onlyOwner { ... }
// (b) the token set is curated by a registry / whitelist
uint256[] memory types = IRegistry(registry).batchGetAssetTypes(assets);
If governance (or the owner) curates which tokens can ever enter, a fee-on-transfer token simply isn't on the list. It's an accepted design assumption, not an exploit. Verified this week on Arcadia (registry-gated) and Rubicon (onlyOwner + pool-curated assets).
The fix: suppress when the deposit is onlyOwner/role-gated or the repo curates tokens. Keep firing on permissionless deposit paths — that's where the real risk lives.
3. The "unchecked call" that's checked one line down
(bool success, ) = recipient.call{ value: amount }("");
require(success, "transfer failed");
A regex that only looks at the call line flags this as "unchecked return value." Look one line down — the require(success) is right there. .transfer() gets flagged too, even though it auto-reverts.
The fix: read the next few lines for a require/revert, and know the difference between .call (returns a bool) and .transfer (reverts on failure). Two lines of context kill the whole class.
4. Read-only reentrancy... into an address the attacker doesn't control
Read-only reentrancy is real and nasty — but only when an attacker can reenter through a manipulable path. On Cap's distributeRewards, a tool flagged it, but:
address network = $.agentData[_agent].network; // governance-set, not attacker input
IERC20(_asset).safeTransfer(network, _amount);
ISymbioticNetworkMiddleware(network).distributeRewards(_agent, _asset);
The external callee is a governance-configured address, and there's no price-view being read mid-call. No attacker entry point, no manipulable value. Not exploitable.
The fix: check where the callee address comes from. If it's immutable or governance-set (not user input), and there's no manipulable view during the call, suppress.
5. Flagging a bug in code the client didn't write
The last one is subtle: my scanner flagged an unchecked send in LzApp.sol — LayerZero's base contract, vendored into the repo. It's not the client's code. Reporting it is noise (and slightly embarrassing — you're telling a founder to go fix LayerZero).
The fix: skip vendored / third-party code (node_modules, @openzeppelin, LayerZero's LzApp / OApp / OFT bases). Audit the client's contracts, not their dependencies.
The point
None of these fixes are clever AI. They're the exact checks a human auditor does in their head: is it guarded? is the token curated? is success checked below? who controls the callee? is this even the client's code? Encode them deterministically and the false-positive flood drains away — permanently, on every future scan.
I keep my pipeline at zero false positives across the entire OpenZeppelin library, because a report that cries wolf trains you to ignore it. Silence, on my reports, means something.
If you're pre-mainnet and want a security first-pass that only surfaces real issues — hand-verified, no noise — I'm happy to take a look: juan23z.github.io.
Top comments (0)