DEV Community

juan23z
juan23z

Posted on

5 false positives your Solidity scanner is probably reporting right now

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 { ... }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)