Repository navigation
fix(runtime): correct the assertion-mode revert guidance and warn about invariant checks that cannot fail (#687) - #1296
Merged
Conversation
…ut invariant checks that cannot fail (#687) Recon runs in assertion mode: only a failed assert is a finding, and a call that reverts for any other reason, including a custom error, a failed require or an arithmetic panic, is discarded. Four invariant prompts claimed every reached revert was "the signal the campaign records", which steers agents toward oracles that cannot fail. They now state how assertion mode works, require assert for every invariant check, and describe the selector-aware liveness oracle: the typed call in try, the catalog's documented selectors accepted in catch, and assert(false) for any other failure (the part of #524 moved here). The campaign's verifier now warns, without failing the node, about generated test code Recon cannot fail: constant-true assertions, a require that checks an outcome after a call made for its effect, a catch that asserts nothing, and an assertion that computes its expected value with code imported from src/ or contracts/. The node that writes the finding lifecycle ledger carries these warnings to the final report, whose warnings companion lists them first under "Invariant checks that cannot fail", with their count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 13, 2026
- The assertion matcher was one global RegExp shared by every check. A selector-aware catch left its lastIndex past the catch, and matchAll starts from lastIndex, so assertions earlier in the file or in the next file went unchecked. Each search now builds its own matcher. - A catch that only rethrows is now a blanket catch: Recon discards the rethrown revert, so only an assertion in the catch can report a failure. - The report section is now "Invariant checks that may not be able to fail" and asks for review: the checks are heuristics, and an import or a constant-true branch does not prove a property untested. The implementation-call message says the same. - A truncation notice takes the gate of the warning that overflowed, so it was counted as a check. Only INVARIANT_ORACLE_* codes are counted now; the notice stays with the other warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines
+264
to
+266
| function lineOf(text: string, offset: number): number { | ||
| let line = 1; | ||
| for (let index = 0; index < offset && index < text.length; index += 1) if (text[index] === "\n") line += 1; |
There was a problem hiding this comment.
Large suites slow verification
lineOf scans from the start of the file for every warning. The analyzer also builds all warnings before boundArtifactValidationWarnings applies the limit. With files allowed to reach 16 MiB, many flagged checks can make campaign verification slow even though most warnings will be discarded.
Build a line-offset index once per file and stop collecting warnings when the warning budget is reached.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/artifacts/src/invariant-oracle-warnings.ts
Line: 264-266
Comment:
**Large suites slow verification**
`lineOf` scans from the start of the file for every warning. The analyzer also builds all warnings before `boundArtifactValidationWarnings` applies the limit. With files allowed to reach 16 MiB, many flagged checks can make campaign verification slow even though most warnings will be discarded.
Build a line-offset index once per file and stop collecting warnings when the warning budget is reached.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #687 (and the residual of #524 that moved there). Warn-only, as discussed on #524: nothing here fails a node.
Problem
Recon runs in assertion mode (
recon fuzz --test-mode assertion): only a failedassertis a finding, and a call that reverts for any other reason is discarded. Four invariant prompts said the opposite, that every reached revert "is the signal the campaign records" (handlers.md:86-87,invariant-testing-campaign.md:86-90,coverage.md:69-71,implement-properties.md:109-110). That steers agents toward liveness checks andrequirepost-conditions that can never fail. Nothing checked the generated Solidity, so a suite that could not fail looked like a clean campaign (#687: 7 of 12 implemented properties).Changes
Prompts. The four passages now state assertion-mode behaviour, require
assertfor every invariant or correctness check, and describe the selector-aware liveness oracle: the typed call intry,catch (bytes memory reason)accepting only the selectors the catalog documents for that action, andassert(false)for any other selector, an empty reason or a panic. That is the only acceptedtry/catcharound a protocol action; blankettry/catchstays an audit violation.Warn-only checks (
packages/artifacts/src/invariant-oracle-warnings.ts, gateinvariant-oracle-falsifiability). The campaign node (the one declaringultrafuzz/property-campaign@3) reads the test sources of the suite it ran,test/andtests/only, and warns about:INVARIANT_ORACLE_CONSTANT_TRUEassert(true),t(true, …),assertEq(x, x),assert(a == a)INVARIANT_ORACLE_REQUIRE_DISCARDEDrequireafter a call made for its effect (a statement call ortry); a guard after a read such asx = vault.balanceOf(a)is not flaggedINVARIANT_ORACLE_BLANKET_CATCHcatchwhose body asserts nothing (rethrowing does not count: Recon discards the rethrown revert)INVARIANT_ORACLE_IMPLEMENTATION_CALLsrc/orcontracts/(#687'sMathLibcase)The final report reads warnings only from its direct dependencies, so the node that writes the finding lifecycle ledger (
dedupe-findings, which depends on the campaign in every shipped topology) forwards them, keeping theirartifacts/<campaign>/…location. The report's warnings companion lists them first under "Invariant checks that may not be able to fail (N)".report.mditself still renders no warnings, per the current report design; adding the count there is a separate call.These are text heuristics, so they can miss a case and a flagged line can be deliberate; they mark a check for review rather than prove a property untested, which is why they warn rather than fail.
Evidence
All runs inside a Linux Podman container (Ubuntu 24.04 arm64), recon-fuzzer v0.4.17 built from its tag (the version the Modal image pins; there is no prebuilt Linux arm64 binary), with
recon-generate@0.0.42,slither-analyzer==0.11.5and the pinnedcovg-eval.1. Recon's semantics, on a purpose-built seeded target
A small share vault with a plain-language spec and three ordinary-looking bugs (withdraw rounds up against a "round down" spec; a full exit reverts with an undocumented
DustFloor();collectFees()never zeroesfeesAccrued), fuzzed with hand-writtenCryticTesterharnesses (--test-mode assertion):withdrawtry/catch, undocumented selector →assert(false)InsufficientShares()require(balance >= assets + fees)aftercollectFees()assertPanic(0x11))This is what the prompt text now states. The target and harnesses are not committed.
2. Before/after with Opus 4.8 (one pair, evidence not proof)
The same seeded target, a trimmed 21-node
invariant-onlytopology (two property lanes, all five stateful stages),unstableprompts vs this branch:unstableproperty_viewGettersNoRevert: view calls thent(true, …); it passed under Recon)Neither run reached its campaign node because of an unrelated failure in the coverage stage on both branches (to be filed separately), so the "fuzzed" row is Recon run by hand on each agent's generated suite. On this small target Opus 4.8 wrote falsifiable suites under the old prompts too; the case for the prompt change is §1, not detection here. The analyzer raised no false positives across the 20 generated test files.
Tests
Linux container:
@ultrafuzz/artifacts336/336 (3 new analyzer tests: a falsifiable suite with guards, documented-selector catches, spec-derived bounds andrequiretext inside strings produces no warnings; each #687 pattern is flagged at its line; only test sources are read),@ultrafuzz/prompts62/62, runtime supporting suite all green exceptgenerated Smithers pinned source proof counts hidden unreachable commits…, which fails identically onunstablein the container;runtime.test.tsreport / invariant / warning batches all pass. A newfinal-report-markdowntest covers the counted section. Prompt wording is not pinned by a test, perprompt-structure.test.ts's stated policy; the warnings measure what agents actually produce instead.Not in this PR
blocker.codeis free text): a schema change.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge, with a non-blocking performance issue in warning generation.Fix with agent prompt
Summary
The PR corrects Recon assertion-mode guidance and adds advisory checks for generated invariants.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Campaign suite test sources] --> B[Oracle warning checks] B --> C[Campaign verification marker] C --> D[dedupe-findings forwards warnings] D --> E[Final report warnings companion]Reviews (3) · Last reviewed commit: "Merge unstable into invariant guidance f..." · Reviewed by Greptile