Skip to content

Releases

v0.7.1: the same code gives the same answer

Choose a tag to compare

@Yonkoo11 Yonkoo11 released this 20 Sep 22:47
d5bf8f7

This is a defect fix, and the defect was reproducibility.

Three runs of the same benchmark corpus, on the same commit, reported 523, 518 and 517 sightings. The findings the two-witness rule publishes did not move, but the count underneath them did, and on one file an accounting finding appeared in one run and not in the next.

The cause is in _reads, which reports the variables a line mentions without writing. It reported only the first one, chosen by iterating a set, and Python randomises set iteration order per process. So require(balances[msg.sender] >= MinDeposit) reported balances in one run and MinDeposit in the next. payout-lens intersects what a function reads with what it writes, so which name it happened to see decided whether there was a finding at all.

Every match on a line is reported now, in a fixed order. Three runs under three different hash seeds produce byte-identical benchmark files on all three corpora. Two tests pin it, one on the reading itself and one on a payout that lowers the balance it checks.

A note in bench/README.md had already recorded one instance of this and put it down to a lens change. That was wrong, and the history table now says so.

Nothing this project publishes moved. SmartBugs-curated stays at 48% precision and 64% recall, DeFiVulnLabs at 62% and 50%, the recent audit contests at 29% recall with the same two findings. The any-lens count settles at 513, because the duplicates it had been counting were never distinct findings.

Also here, from reviewing the lens released in v0.7.0:

  • the one-shot test read the whole function header when a function carried no modifiers, so a parameter named initializer would have silenced the lens on that function. It reads the modifier list now.
  • the caller test read only a function's own body while the guard test read the helpers it calls. Both read the helpers now.
  • the lens built two write-maps for every function when each is needed for one role. The second is now built only for the function being judged, and only once it is known to carry no guard.

The ecosystem scan also runs projects side by side. Serial it managed four projects in three and a half minutes, which is about twenty-two hours for the listing it reads. Every repository already had its own clone and its own memory file, so nothing about the reading changed.

83 tests.

v0.7.0: a third reading of the access risk

Choose a tag to compare

@Yonkoo11 Yonkoo11 released this 19 Sep 22:16
cc5da71

Quorum had two readings of unguarded-state-write, and both judge a function by how it is written: whether it carries a modifier, and whether the variable it writes carries a privileged-looking name. Recent Solidity writes last_gauge and _validatorsMap, not feeRate, so on bench/MODERN.md that pair corroborated nothing and recall there was 0%. The last release ended by saying a genuinely different second reading was needed and was not designed yet. This is it.

consistency-lens asks the contract instead of the spelling. If every other externally callable function that writes a variable agrees on a modifier, and this one carries no guard at all, that disagreement is the evidence.

Three rules narrow it, and each came from opening a false confirmation and finding a reason for it:

  • a one-shot initializer is guarded by being one-shot, because after deployment nobody can call it;
  • a function that tests its own caller is not missing a guard, it is guarding differently;
  • the caller has to be able to reach the write, by choosing either the value or the slot it goes to.

Carrying a different guard is not enough to be sighted. An admin setter beside a sibling that checks the caller in its own body is guarded, only differently, and reading that as a finding was noise on every corpus here.

Measured on all three, before and after, with the before files kept in bench/history:

  • SmartBugs-curated: precision 49% to 48%, recall 63% to 64%, and one more labelled target confirmed, the owner change in multiowned_vulnerable.sol.
  • DeFiVulnLabs, held out: unchanged at 62% precision and 50% recall, the same eight confirmations.
  • Recent audit contests: recall 0% to 29%. Two real High severity findings confirmed, ServiceNft.updateImpact and AgentNftV2.addValidator in the Virtuals Protocol contest.

All 40 confirmations on that corpus were read by hand. Thirty-eight are false, and bench/MODERN.md names the four shapes they fall into: a write-once create on a fresh id, an in-body check the modifier reading cannot see, a caller writing their own row, and a value no caller can influence. The first and the last are the two worth a rule next, and they will be built against a labelled corpus before they are measured there.

The cost is written in the same file. This lens was developed against bench/MODERN.md, so that corpus is no longer held out for the access risk, and its 29% is not a measurement on unseen code. DeFiVulnLabs is now the only corpus no lens has ever been tuned against.

The package also reports its own version again. pyproject, quorum/init and the SARIF exporter all still said 0.1.0, six releases after that stopped being true.

81 tests.

v0.6.0: lenses read across files

Choose a tag to compare

@Yonkoo11 Yonkoo11 released this 19 Sep 08:35
e9d903a

A lens used to read one file. Modern protocols declare their state in a base contract and put a function's work in an internal helper one file up, which is why four of the seven targets in bench/MODERN.md were invisible to every lens. That file ended by naming this as the only change that would move the number, so this release makes it.

A run now builds the inheritance graph of its own files, and a function is read with the state and the internal helpers its contract actually has. Nothing wider: an unrelated contract's names stay out of scope, or every same-named local variable would look like a state write.

A second defect surfaced in the same work. The declaration reader stopped at the first closing bracket, so mapping(a => mapping(b => c)) was not a declaration at all and a balance keyed by two things was not state.

Measured on all three corpora, before and after, with the before files kept in bench/history:

  • SmartBugs-curated: recall 62% to 63%, precision held at 49%.
  • DeFiVulnLabs, held out: unchanged at 50% recall and 62% precision.
  • Recent audit contests: four of the seven targets went from invisible to candidates. None is confirmed. All 28 confirmations were read by hand again and none is a bug the audits missed; the one that is not quite a false alarm is written up in bench/MODERN.md.

What stops those four candidates is now a design question rather than a defect. The second reading of the access pair keys on the name of the variable written, and modern code writes last_gauge and _validatorsMap, not feeRate. Loosening that to any state write would make the two lenses one lens, and a quorum of one is not a quorum. A genuinely different second reading is needed and is not designed yet.

75 tests.

v0.5.1: measured against modern audits

Choose a tag to compare

@Yonkoo11 Yonkoo11 released this 19 Sep 00:25
0f73b47

The measurement this project was missing: what the lenses do against recent audits, not against corpora built from the bugs they were written for.

bench/MODERN.md takes 16 recent public audit contests, each repository at the exact commit its published report links. Every High and Medium finding was read and classified first, before any lens ran. Two numbers came out of it:

  • The four risks Quorum covers are 5% of what those audits reported at High and Medium. 131 findings; 7 in those shapes; the rest were business logic, denial of service, validation, signatures, slippage, rounding and the like. That is the ceiling on this tool against a modern audit.
  • Of those 7, the two-witness rule confirmed 0. Three were seen by one lens and held back. All 26 confirmations it did make were read by hand, and every one is false.

Every miss has its cause written down. Four of the seven come from one thing: this tool reads a file at a time, while modern protocols keep state in a diamond struct or a base contract and split a function's work across files. That is a different program, not a rule to tune, and it is now the next decision rather than a hidden number.

Three defects the corpus exposed are fixed here, each with a test built from the shape that exposed it: a return clause (external returns (address)) read as a modifier, which hid every returning function from the access lens; the access pair not following the helpers a function calls, although the reentrancy pair already did; and a lock modifier over an _unlocked flag not counted as a reentrancy guard, which is the Uniswap V2 Pair idiom and the most forked guard in Solidity. On SmartBugs-curated those moved precision from 48% to 49% with recall unchanged at 62%; the held-out corpus did not move.

Two false-positive classes the corpus showed were deliberately not turned into rules, because inventing a rule from the test set is how a benchmark stops meaning anything. Both are written down in the file and will be built against the labelled corpora first.

71 tests.

v0.5.0: the claim registry, live

Choose a tag to compare

@Yonkoo11 Yonkoo11 released this 18 Sep 22:55
1388714

The claim now goes through a contract. The ClaimRegistry at 0xDeA0792cEc959CE6893C24dEeFc6FE9B047a3Ea3 on Robinhood Chain (deployed 18 September 2026, block 66593107) pulls the 100,000 QUORUM fee, burns it through the token's own burn, and only then records the claim, all in one transaction. A record cannot exist without its own fee and one fee cannot back two records. No owner, no pause, no upgrade, no ether, nothing to sweep. The fee and the token are immutable.

Proven on chain the same day: one claim made from the tool (block 66594959), read back with quorum verify, revealed, and imported into an empty memory as a hint. The deployed bytecode matches the local build with the immutables masked. Contracts, 18 Foundry tests, a fuzz and an invariant are in contracts/, and forge test now runs in CI.

Older claims (the self-addressed QUORUM2 shape) still read and verify against their separate burn; a self-addressed claim mined after the registry existed is refused.

Not done: source verification on the block explorer (its API blocks scripts; the browser form is pending), and the fork test against the real token, which no public node has let finish.

v0.4.2: the run as a page on the pull request

Choose a tag to compare

@Yonkoo11 Yonkoo11 released this 18 Sep 22:19
74d65e9

The action now writes the run as a page: every confirmed finding with the two lenses that agreed and the line each read, plus the count of candidates held back. It lands in the job summary (no write token needed) and, with the comment job shown in .github/workflows/quorum.yml, as one comment on the pull request that is updated in place on every push. The comment job runs no repository code. Tested on a pull request against this repository: one comment, updated on the second push, then closed.

v0.4.1: the lenses after the Robinhood Chain run

Choose a tag to compare

@Yonkoo11 Yonkoo11 released this 18 Sep 20:56
5f8b857

We ran the eight lenses over every public Solidity repo HEY Research lists for Robinhood Chain: 631 repos, 11,506 files, 281 findings confirmed by two lenses. Then we read all 281 by hand. Three were true. The record, every verdict with its reason, is bench/ROBINHOOD.md.

The false ones taught five changes, all in this release:

  • a function only the owner can call is not a reentrancy target;
  • the reentrancy pair reads one level into the private helpers a function calls (the bug that mattered most in the run sat in a private _claim reached from three public functions);
  • a write anywhere on a line, and a write through a storage alias, count for the one-way ledger check;
  • unchecked { x += y; } on one line is read by the arithmetic pair;
  • unsafe-math confirms only when both readings are of the same line.

On the labelled corpora: SmartBugs 62% recall at 48% precision (was 63% / 48%; one owner-only target lost, reentrancy precision 69% to 72%), held-out unchanged at 50% / 62%. Before files kept in bench/history. 63 tests.

The three real bugs found next to flagged lines are with their owners first (public issues, filed 18 September) and join the record when fixed or after fourteen days.

Corrected after publishing: the owner-only rule was narrowed the same day to names that mean one privileged party (onlyMember and the like stay in scope), which returned two false confirmations. The numbers above are the corrected ones; the commit history has both.

v0.4.0: the accounting pair

Choose a tag to compare

@Yonkoo11 Yonkoo11 released this 16 Sep 17:35
969dd2e

A fourth pair of lenses, for the shape announced on 15 Sep: a contract that adds to a balance in one function and never takes it back in another. ledger-lens reads the whole contract (does this balance have any way down?), payout-lens reads one function (does value leave against a balance it never reduces?); a finding needs both. Eight lenses, four risks.

Measured before it was posted (bench/ACCOUNTING.md): 1,164 files from twelve audited codebases, 0 confirmed; SmartBugs-curated, 4 confirmed, each read by hand and each paying against a balance the contract never lowers, all four counted false because that corpus labels no such bug (overall precision 51% to 48%, recall unchanged at 63%); DeFiVulnLabs unchanged. No recall number exists for this bug yet.

What the pair cannot see: a sibling function that forgot one debit another function has. There the balance does go down, on the other path, and that needs a reader that follows calls.

Also fixed: explicit --targets with one file name on two chains no longer collapse into one target. 52 tests.

v0.3.0: verified source from five chains, no key

Choose a tag to compare

@Yonkoo11 Yonkoo11 released this 13 Sep 22:30
c448df5

quorum fetch --chain ethereum | base | arbitrum | optimism | polygon reads each chain's Blockscout API with no key, joins multi-file contracts into one file with a marker per part, saves under targets//, and refuses anything that is not verified JSON under 5 MB. Findings are keyed by the path under targets/, so one contract name on two chains stays two targets. The run on the wrapped token from each chain is in bench/MULTICHAIN.md. 49 tests. README and the registry page now say when, and only when, the token would go to a second chain.

Quorum 0.2.0

Choose a tag to compare

@Yonkoo11 Yonkoo11 released this 13 Sep 11:23
b13bdaa

Three days after the hackathon submission. Everything below is measured, and the files that measured it are in bench/.

Findings where code review happens. quorum run --sarif writes SARIF 2.1.0 and the composite action (uses: Yonkoo11/quorum@v0.2.0) uploads it to the GitHub Security tab. Every alert names the two lenses that agreed and what each one read. A candidate seen by one lens is never written.

The registry can no longer be poisoned for one fee. An imported pattern is a hint until two local lenses agree; one fee burn admits one import per memory; the idiom signature keeps the member name; a claim that reverted, is not self-addressed, or is unpaid after the fee existed is refused. One limit is written down instead of fixed: on chain, one burn can still back more than one claim.

The arithmetic pair was rebuilt as two readings of one bug (the old pair could never agree with itself). 0% → 76% recall on SmartBugs-curated.

Benchmarks: SmartBugs-curated (143 files, 73 targets) 63% recall at 51% precision, reentrancy 94% / 69%; DeFiVulnLabs held out (10 targets) 50% / 62%; Slither scored the same way in bench/SLITHER*.md. Both the runs where Slither was ahead and the runs where it is not are kept in bench/history/.

The action installs the pinned ref, takes inputs through the environment, and every third-party action is pinned to a commit. 41 tests.