# Audit evidence status, 2026-09-15 (revision 3) Dated note for the phase 1 reviewer. It records what postdates the handoff tag `audit-2026-09-12-supplement` (commit `a44d93f`, 2026-09-10), what an additional in-house AI review of that evidence found on 2026-09-15, and what changed in response. Everything below is in tag `audit-2026-09-15-addendum-r3`; the earlier tags and zips are untouched. **Revision 3 corrects revision 2** on two server-side points the review of r2 raised: the legacy reveal scheme is now selected by the chain the RPC serves (Sepolia, 11155111) AND a listed address, never by the address alone, and a configured `CHAIN_ID` that disagrees with the RPC is an error before any read; the watcher's state save backs up only a primary that parses, so a recovery from `.bak` can no longer be followed by the corrupt primary overwriting that backup, and the recovered state is written back at once. Tests cover the same address on Sepolia, mainnet and a local chain, the mismatch rule, a failed read, and recover then save then restart. The reviewer note's line-count sentence is corrected. **Revision 2 corrects revision 1.** Revision 1 (tag `audit-2026-09-15-addendum`, commit `12b7c26`) stated that the campaign logs were in the package; they were not, a `*.log` ignore rule had kept them out of the tag and the zip. They are tracked from r2 on. Revision 2 also restores compatibility with the Sepolia collections deployed before the reveal change, makes the watcher's state file crash-safe, tightens the router harness's success checks, and corrects the wording noted below. Static analysis and coverage predate the two contract changes and were not rerun on them; the fuzzing and symbolic runs were. ## What postdates the supplement tag | item | where | date | |---|---|---| | Static analysis: Slither 0.11.6, Semgrep + Decurity rules, Aderyn 0.6.8, 4naly3er; triage against the brief | `docs/audits/tools-2026-09-11/` | 2026-09-11 | | Echidna campaigns: distributor liability (9 properties), router reserve (8, now 9) | `contracts/fuzz/`, logs in `docs/audits/fuzz-2026-09-15/` | 2026-09-11, rerun 2026-09-15 | | Halmos: Merkle claim binding (4, now 5 checks), reveal mapping (4, now 5) | `contracts/halmos/`, log in `docs/audits/fuzz-2026-09-15/` | 2026-09-11, rerun 2026-09-15 | | Trail of Bits skills: entry-point map, code maturity | `docs/audits/tob-2026-09-11/` | 2026-09-11 | | Coverage report (98.89% statements, 87.79% branches, production contracts) | `docs/audits/coverage-2026-09-11.md` | 2026-09-11 | | Monitoring and incident plan; event watcher installed on the droplet | `docs/MONITORING-AND-INCIDENTS.md`, `scripts/watch-events.js` | 2026-09-11, installed 2026-09-15 | | Full-scale mint rehearsal on Sepolia (1,111 minted, revealed, frozen; draw recomputed) | `docs/MINT-REHEARSAL-2026-09-14.md`, `docs/EVIDENCE.md` | 2026-09-14 | | Corrections to `contracts/AUDIT.md` (valued at release, 2-of-3, five contracts); royalty-receiver decision documented | `contracts/AUDIT.md` section 7, `docs/AUDIT-BRIEF.md` section 5 | 2026-09-11 | ## What the review of the evidence found (2026-09-15) The review was an additional in-house AI pass (a different assistant, reading the reports against the code), not an independent third party. No new confirmed fund-loss issue. Three weaknesses in the evidence itself, all fixed and rerun: 1. `contracts/halmos/test/Claim.t.sol`: the two-leaf check let the "wrong amount" attempt succeed when the two symbolic amounts were equal, which consumed the second account's claim and pruned the path before the final assertions. Now assumes unequal amounts; the equal case is its own check. 2. `contracts/fuzz/DistributorFuzz.sol`: duplicate mode built leaf 1 for alice but claimed it as bob, so the competing-claim scenario never ran. Recipients are now stored per cycle and used by the claims. 3. `contracts/fuzz/RouterFuzz.sol`: every revert was swallowed and the threshold-crossing branch only bounded the deltas. A valid call that reverts is now a property failure, and the crossing release is recomputed independently and matched exactly. And one operational gap, fixed: the event watcher advanced its block pointer after calling the notifier, which never throws, so a webhook outage lost the alert to the local log. Findings are now queued in the state file before the pointer moves and retried until the webhook accepts them (at-least-once delivery with a stable `:` id in every payload); the state file is replaced atomically with a `.bak` of the previous copy and a corrupt file is recovered or refused, never read as empty; the webhook post has a timeout; the reserve and ops Safes are watched alongside the treasury Safe, module and guard changes included. ## Contract changes made in response (please review) Both are in `Disorderly721.sol` and `RoyaltyRouter.sol` at the addendum commit and are described in `docs/AUDIT-BRIEF.md` section 5: - **Per-tier reveal draw.** `setStartingIndex` now derives `councilOffset` (modulo 100) and `operatorOffset` (modulo 1011) from the committed block hash, and `metadataId` rotates each tier by its own offset. The single index reduced modulo both tier sizes gave some rotations two or three times the probability of others; the "raw zero becomes one" guard added bias of its own. Each tier's rotation is now effectively uniform, with the negligible modulo bias of a 256-bit value over 100 or 1011 under the usual hash assumptions; offset zero is allowed. The block-proposer and reveal-expiry assumptions are unchanged. The raw `startingIndex` is still recorded and emitted. Five Halmos checks prove range, injectivity and tier independence over every offset pair; they establish the mapping's properties, not that the randomness source is unbiased. The recompute is in `contracts/scripts/rehearse-mint.js` and `docs/LAUNCH-RUNBOOK.md`. The server reads the offsets from new deployments and reduces the single index of the three pre-change Sepolia collections, chosen by an explicit address list together with the chain the RPC serves (11155111), never by the address alone and never by a failed call; a `CHAIN_ID` that disagrees with the RPC is an error (`server/chain.js`). - **Gas-bounded royalty release.** `release()` forwards `SEND_GAS` (100,000) to each destination, so a destination that burns gas is deferred like a rejecting one rather than reverting the release. `pushOwed` and `withdrawOwed` are unbounded on purpose: each pays one leg in its own transaction and can roll back nothing else. Tests: one burner, two burners, a storing-and-requiring receiver, and a push to a burner failing alone. A receive test against the real Sepolia Safes is still to do. Consequences: the phase 1 code baseline is the addendum commit, not the 2026-09-12 tag; the Sepolia mint rehearsal of 2026-09-14 ran on the previous mapping and is repeated on this code before `openMint`. ## Reruns, 2026-09-15 | tool | image digest | result | |---|---|---| | Echidna (distributor) | `ghcr.io/crytic/echidna/echidna@sha256:80f90c3a727986fc31380a509a87fe0a14cdbda13f4ead102b2d7ffaff285261` | 9 properties passing, 200,291 calls, seed 4070208826713903324 | | Echidna (router) | same | 9 properties passing, 200,286 calls, seed 6528613900499222773 (r2 harness; the r1 run, seed 2039011567472805080, also passed) | | Halmos | `ghcr.io/a16z/halmos@sha256:4076f8929c2d32db7b2120feebb7dff256bb09de371db94c1fa41dd8c32dc0f6` | 10 checks proved (5 claim, 5 reveal), 32 s | | Hardhat suite | node 22, solc 0.8.24 | 261 passing | | Server suite | node 22 | 37 files passing (includes the watcher queue and state tests and the legacy-collection and network-boundary tests) | Static analysis (2026-09-11) and the coverage report (2026-09-11) were run on the contracts BEFORE the two changes above and are not rerun here; the changes are 23 and 14 lines. Line counts at r2: `Disorderly721.sol` 511 lines / 272 nSLOC (was 488 / 264), `RoyaltyRouter.sol` 408 / 200 (was 394 / 199), so phase 1 is 472 nSLOC (was 463) and the five contracts 890 (was 881), counted as non-blank, non-comment lines. Replay a campaign with its seed by adding `--seed ` to the command in `docs/audits/fuzzing.md`; the logs above are the full text output. ## Still open, and whose - Custody: the mainnet Safes are created with three independent signers and a rehearsed recovery before deployment (owner's task, in progress); the rehearsal Safes on Sepolia are not reused. - Payout rehearsal on the version 3 record format: a manual vote, a funded payout and a claim in one cycle have not yet been rehearsed together (cycle 7 was killed, cycle 8 failed at the vote). Planned for the full-population Sepolia cycle before mainnet. - Recurring fuzz and symbolic runs in CI, pinned to the digests above, and a Safe-receive gas test against the real Sepolia Safes: on the list, not yet done. - The full-population Sepolia rehearsal's acceptance criterion now includes the payout path above: one cycle where a manual vote passes, a mandate delivers, the Safe funds the close and a holder claims. - The Trail of Bits maturity score of 2026-09-11 is historical; several of its "missing" items now exist (monitoring, coverage, the corrections above). It has not been rescored. ## Package - Revision 3 tag: `audit-2026-09-15-addendum-r3`, commit `0ec05b35ea1b0a64b308c536f312df3f2b24f927` (annotated; `git rev-parse 'audit-2026-09-15-addendum-r3^{commit}'` resolves it). - Revision 3 zip: `disorderly-audit-2026-09-15-addendum-r3.zip`, sha256 `f2f1337f8ba8669bce8431ed1d3efdedb76b8bf9d432a381bf311ae8652ed27f` (recorded here one commit after the tag, since a file cannot carry the hash of the archive that contains it). An export of the tag plus `PACKAGE-MANIFEST.json` (sha256 of every file, the commit, the image digests) and `IMAGES.json`. Built by `.audit/package-addendum.cjs audit-2026-09-15-addendum-r3` then `.audit/zip-addendum.py`. - Revision 2, superseded and kept: tag `audit-2026-09-15-addendum-r2`, commit `226cfa2cfd94c8338eb659f8d55f6cb336f684fb`, zip sha256 `50bd69bfc09ccd974699689f56c24e51cb2a4d25542c3649bb12913c3cf62301`. - Revision 1, superseded and kept: tag `audit-2026-09-15-addendum`, commit `12b7c2689b18671ed85916d583a53b6a02e483be`, zip `disorderly-audit-2026-09-15-addendum.zip` sha256 `4d8f8661166c994612947d6aa787be7727b1c7965a0f34c46fa40c5ef45849f5` (382 entries). It lacks the three campaign logs.