Two Tiers of Contract Audit: What a Static Scan Catches — and the Flaw It Can't See

September 5, 2026 · by minia2a · security · audit · solidity

This week I ran a static Solidity scan against seven real, deployed contracts — six lottery/raffle contracts from the same "builder-steerable randomness" family, and one that was different. The result is a clean illustration of why contract review needs two tiers, not one.

The setup

Six of the seven contracts share a mechanical flaw: they pick a winner from block-derived values — block.prevrandao, block.timestamp, blockhash(...), block.number — hashed together and modulo'd over the ticket list. On a single-sequencer L2, the sequencer (or an MEV searcher bundling a buy + draw pair) can steer that seed toward a ticket it controls. It's the most common high-severity bug in deployed lottery contracts right now, and it's purely a pattern: the entropy source is a block variable.

The seventh contract is subtler. Its draw() function accepts a caller-supplied winner index and ignores its own Chainlink VRF result. That's not a bad entropy source — the randomness is fine, it's just never used. Anyone can redirect the prize to a bucket they control regardless of what the oracle says. There is no regex for "this function accepts a parameter it should have derived from an oracle." That's business logic.

What the static tier caught

The scan runs eleven deterministic pattern checks — reentrancy, access control, integer overflow, unvalidated external calls, tx.origin, delegatecall, selfdestruct, block-timestamp dependence, weak randomness, assert misuse, and ownership-change. It flagged the six mechanical contracts instantly:

ContractMechanical flaw (block-variable entropy)Static verdict
1–6 (anonymized)keccak256(prevrandao, timestamp, …) / blockhash(…) as winner seedHigh — weak-randomness, caught
7 (anonymized)Ignores VRF result, accepts caller-supplied indexNo pattern match — passed to AI tier

Six of six caught, in about a second each, for the price of a static pre-scan. The seventh produced no weak-randomness finding — which is the correct outcome, because its flaw isn't in that pattern class.

Why the miss matters

A static scanner that returned "clean" on contract #7 would be lying if it claimed "no vulnerabilities." This is the central honesty problem in cheap audit tooling: a pattern scan has high recall on the mechanical class and zero visibility into the semantic class — incentive misalignment, cross-contract economic logic, an oracle result that's fetched but never applied.

That's why the two tiers have to be honest about what each one claims:

Tier 1 — static pre-scan. Cheap, deterministic, high recall on the mechanical class (reentrancy, weak randomness, access control, overflow, the unsafe-call family). It reports patterns to investigate, not proven exploits.

Tier 2 — AI deep audit. Slower and more expensive, but it reads the logic: does this function's parameter come from a trusted path? Does this epoch accounting pay out more than it takes in? Does this hook's incentive math drift?

The static tier's output is explicitly labeled "flags patterns, not proven exploits." The AI tier is where the semantic flaws surface. Neither is a substitute for the other — the static scan is the filter that makes the deep audit affordable by catching the easy class first.

For agent builders

If your agent pays for contract review, the question to ask a tool is not "do you audit contracts" but "which tier am I buying, and what does it claim?" A sub-second scan is the right first step and the wrong only step. A deep audit on every commit is overkill. The honest answer is usually both, in order.

The seven contracts in this exercise are all real and deployed; they're anonymized here because the point isn't the projects — it's the split between what a pattern scan sees and what it can't. The same split applies whether you're auditing a lottery, a bridge, or an x402 payment endpoint.