Skip to content

Commit ff041e9

Browse files
committed
test: close reachable coverage gaps in mint/burn message lookups and wrapper degenerate branch; regenerate
doc/coverage
1 parent bd62f94 commit ff041e9

172 files changed

Lines changed: 12873 additions & 6553 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ _Nothing yet._
8282
- **Batch adds reject `address(0)` rather than skipping it.** The batch convention still skips *duplicates* (an idempotent no-op the emitted event describes truthfully), but silently dropping the sentinel would make `AddAddresses` / `AddWhitelistAddresses` / `AddFrozenlistAddresses` report a member that is not in the set — re-polluting, off-chain, the very view this change cleans up on-chain.
8383
- **Removed capability (`RuleBlacklist` / `RuleSpenderWhitelist`):** these rules inherit the same address set, so blacklisting `address(0)` — which previously acted as an undocumented "halt all issuance" kill switch, since a mint has `from == address(0)` — is no longer possible. They did **not** receive `allowMint` / `allowBurn` flags: a deny-list has no legitimate reason to list the sentinel, and issuance is better halted at the token (CMTAT pause/deactivate) or with an allow-list rule. If you relied on that idiom, migrate before upgrading.
8484
- Access-control hooks: **all `_authorize*()` / `_only*()` hooks are now `internal view virtual`**, both the abstract declarations and every override. An authorization hook checks and reverts; `view` makes "authorization never mutates state" a compiler-enforced invariant rather than a convention. Compile-time only — `view` on an `internal` function has no gas or runtime impact. Normalised `RuleWhitelistWrapperBase._authorizeCheckSpenderManager` (declaration), the three `RuleWhitelistWrapper` overrides (which disagreed with their own `Ownable2Step` twin), and the six `_onlyComplianceManager` overrides across the operation rules. The convention is now recorded in `CLAUDE.md`/`AGENTS.md`. One documented exception remains: `RuleConditionalTransferLightMultiTokenBase._authorizeComplianceBindingChange` delegates to `_onlyComplianceManager()`, which `lib/RuleEngine` declares non-`view`, and Solidity checks mutability against a virtual's declared type — it can only become `view` once that is changed upstream.
85+
- `RuleSpenderWhitelist` / `RuleSpenderWhitelistOwnable2Step`: remove a dead `RuleTransferValidation` import. The symbol was referenced nowhere in either file (not in the inheritance list, an `override(...)` specifier, or a body). Cosmetic — no bytecode or behavioural change. Surfaced by Aderyn 0.6.5 (`Unused Import`) in the post-remediation re-run.
8586
- `RuleERC2980`: split the shared list-management errors into per-list errors so a revert identifies which list rejected. `RuleERC2980_AddressAlreadyListed` becomes `RuleERC2980_AddressAlreadyWhitelisted` / `RuleERC2980_AddressAlreadyFrozen`, and `RuleERC2980_AddressNotFound` becomes `RuleERC2980_AddressNotWhitelisted` / `RuleERC2980_AddressNotFrozen`. **Breaking (ABI):** the removed errors' 4-byte selectors no longer exist; off-chain tooling matching on them must update.
8687
- Update contract version in `VersionModule` to `0.4.0`.
8788
- Ownable2Step rule deployments now explicitly advertise ERC-165 `IERC165` (`0x01ffc9a7`), ERC-173 (`0x7f5828d0`), and Ownable2Step (`0x9ab669ef`) interface IDs.
@@ -114,7 +115,9 @@ _Nothing yet._
114115
- Add `test/invariant/` — the project's first stateful invariant suite (`StdInvariant`, `fail_on_revert = true`). Handler-driven fuzzing over the two stateful rules: `RuleConditionalTransferLight` (approval conservation, `INV-5`) and `RuleMintAllowance` (exact quota accounting via a ghost mirror, `INV-7`). 4 invariants × 8 192 calls each, 0 reverts. The handlers also prove mint/burn never consume an approval and that non-mint transfers never touch a quota. Both invariants were mutation-verified (injecting an approval double-spend and an off-by-one quota deduction makes them fail). Adds an `[invariant]` section to `foundry.toml`.
115116
- Add `test/ThreatModel/ThreatModelTests.t.sol` — 18 threat-model proof-of-concept tests (15 unit/integration, 3 fuzz) covering the identity-registry mint path, `RuleMaxTotalSupply` view overflow, the multi-token approval-key divergence, `approveAndTransferIfAllowed` under a `RuleEngine`, residual state after `unbindToken`, `_transferHash` injectivity, mint-quota accounting, and `RuleWhitelistWrapper` child-rule composition. Tests suffixed `_CurrentBehaviour` assert behaviour the audit considers wrong: fixing the underlying issue must make them fail.
116117
- Add `test/MintBurnFlags/MintBurnFlags.t.sol` and `test/RuleConditionalTransferLightMultiToken/MultiTokenSurface.t.sol` — cover the new `allowMint` / `allowBurn` setters, their access control and events, the dedicated mint/burn restriction codes, and the multi-token pre-flight surface. Also `test/InterfaceId/AddressListInterfaceId.t.sol`, `test/StaleState/StaleStateHygiene.t.sol`, `test/RuleConditionalTransferLight/RuleConditionalTransferLightBindRuleEngine.t.sol`.
117-
- **Full suite: 506 tests across 78 suites, production line coverage 97.38%** (up from 425 / 94.91%).
118+
- Close the last reachable coverage gaps introduced by this release: the `messageForTransferRestriction` lookups for the new mint/burn codes (`24`/`25` on the whitelist family, `64`/`65` on ERC-2980) and the wrapper's degenerate `from == 0 && to == 0` branch — the anti-drift guard that keeps `RuleWhitelistWrapper` and `RuleWhitelistBase` agreeing — were all reachable but unexecuted. 5 tests added; **every uncovered line in `src/` is now an abstract declaration with no body.**
119+
- Regenerate the coverage report in `doc/coverage` (`forge coverage --report lcov` + `genhtml`).
120+
- **Full suite: 511 tests across 78 suites — 97.8% line coverage (1 082/1 106), 97.3% branch coverage (220/226)** (up from 425 tests / 94.91% lines).
118121
- Added `RuleConditionalTransferLightMultiToken` tests proving approvals are token-scoped and cannot be consumed cross-token.
119122
- Added explicit RuleEngine integration tests for `RuleConditionalTransferLightMultiToken` documenting caller-context behavior in shared RuleEngine topology.
120123
- Added `Ownable2StepERC165Support` test covering all Ownable2Step rule deployments.

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,7 @@ This repository is developed and tested with [Foundry](https://book.getfoundry.s
757757
| Gas report | `forge test --gas-report` |
758758
| Gas snapshot | `forge snapshot` (check only: `forge snapshot --check`) |
759759
| Coverage | `forge coverage` |
760+
| Coverage report ([`doc/coverage`](./doc/coverage/)) | `forge coverage --no-match-coverage "(script\|mocks\|test)" --report lcov && genhtml lcov.info --branch-coverage --prefix "$PWD/" --output-dir coverage` |
760761
| Invariant suite only | `forge test --match-path "test/invariant/*"` |
761762
| Format | `forge fmt` |
762763
| Deploy a script | `forge script script/<Deploy...>.s.sol --rpc-url <url> --account <keystore>` |
@@ -1633,22 +1634,24 @@ aderyn -x mocks --output doc/security/audits/tools/v0.4.0/aderyn-report.md
16331634

16341635
#### Aderyn (v0.4.0)
16351636

1636-
Static analysis with [Aderyn](https://github.com/Cyfrin/aderyn) 0.6.5. Full report and feedback in [`doc/security/audits/tools/v0.4.0/`](./doc/security/audits/tools/v0.4.0/). **No High/Medium issues; nothing to fix** — all 8 Low findings are by-design or false positives (see [feedback](./doc/security/audits/tools/v0.4.0/aderyn-report-feedback.md)). Instance counts rose vs `v0.3.0` only because `v0.4.0` adds the `RuleMintAllowance` and `RuleConditionalTransferLightMultiToken` families.
1637+
Static analysis with [Aderyn](https://github.com/Cyfrin/aderyn) 0.6.5, re-run **2026-07-14** after the security remediation. Full report and feedback in [`doc/security/audits/tools/v0.4.0/`](./doc/security/audits/tools/v0.4.0/). **No High/Medium issues; nothing to fix** — all 9 Low findings are by-design or false positives (see [feedback](./doc/security/audits/tools/v0.4.0/aderyn-report-feedback.md)). The run initially reported 10: an `Unused Import` (dead `RuleTransferValidation` import in the two `RuleSpenderWhitelist` deployment files) was a genuine cosmetic defect and has been **fixed**.
16371638

16381639
| ID | Title | Instances | Verdict |
16391640
|---|---|---|---|
1640-
| L-1 | Centralization Risk | 62 | By design (regulated token issuer model) |
1641+
| L-1 | Centralization Risk | 68 | By design (regulated token issuer model) |
16411642
| L-2 | Unspecific Solidity Pragma | 63 | By design (`^0.8.20` library; project pins solc 0.8.34) |
16421643
| L-3 | Address State Variable Set Without Checks | 1 | False positive — zero-check enforced at public `setSanctionListOracle` |
1643-
| L-4 | PUSH0 Opcode | 63 | By design — project targets Prague EVM |
1644+
| L-4 | PUSH0 Opcode | 64 | By design — project targets Prague EVM |
16441645
| L-5 | Modifier Invoked Only Once | 2 | By design — template method pattern |
1645-
| L-6 | Empty Block | 55 | By design — `_authorize*()` hooks + required interface no-ops |
1646-
| L-7 | Costly operations inside loop | 6 | By design — `EnumerableSet` requires one `SSTORE` per element |
1647-
| L-8 | Unchecked Return | 13 | Mixed — majority false positives; constructor `_grantRole` intentional |
1646+
| L-6 | Empty Block | 61 | By design — `_authorize*()` hooks + required interface no-ops |
1647+
| L-7 | Loop Contains `require`/`revert` | 3 | **By design — recommendation rejected.** Batch adds revert on `address(0)` on purpose: skipping it made the emitted event name the sentinel as a set member |
1648+
| L-8 | Costly operations inside loop | 7 | By design — `EnumerableSet` requires one `SSTORE` per element |
1649+
| L-9 | Unchecked Return | 13 | Mixed — majority false positives; constructor `_grantRole` intentional |
1650+
|| Unused Import | 0 | **Fixed** during this run (was 2) |
16481651

16491652
#### Slither (v0.4.0)
16501653

1651-
Static analysis with [Slither](https://github.com/crytic/slither) 0.11.5. Full report and feedback in [`doc/security/audits/tools/v0.4.0/`](./doc/security/audits/tools/v0.4.0/). **Nothing to fix** — the two High `arbitrary-send-erc20` hits are false positives (approval-gated, allowance-checked compliance flow); see [feedback](./doc/security/audits/tools/v0.4.0/slither-report-feedback.md).
1654+
Static analysis with [Slither](https://github.com/crytic/slither) 0.11.5, re-run **2026-07-14** after the security remediation (tally unchanged from the previous run). Full report and feedback in [`doc/security/audits/tools/v0.4.0/`](./doc/security/audits/tools/v0.4.0/). **Nothing to fix** — the two High `arbitrary-send-erc20` hits are false positives (approval-gated, allowance-checked compliance flow); see [feedback](./doc/security/audits/tools/v0.4.0/slither-report-feedback.md).
16521655

16531656
| Category | Severity | Instances | Verdict |
16541657
|---|---|---|---|

doc/coverage/coverage.png

-294 KB
Binary file not shown.

doc/coverage/coverage/index-sort-b.html

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,27 @@
3131
<td class="headerValue">lcov.info</td>
3232
<td></td>
3333
<td class="headerItem">Lines:</td>
34-
<td class="headerCovTableEntry">764</td>
35-
<td class="headerCovTableEntry">781</td>
34+
<td class="headerCovTableEntry">1082</td>
35+
<td class="headerCovTableEntry">1106</td>
3636
<td class="headerCovTableEntryHi">97.8 %</td>
3737
</tr>
3838
<tr>
3939
<td class="headerItem">Date:</td>
40-
<td class="headerValue">2026-04-16 15:29:51</td>
40+
<td class="headerValue">2026-07-14 13:44:06</td>
4141
<td></td>
4242
<td class="headerItem">Functions:</td>
43-
<td class="headerCovTableEntry">270</td>
44-
<td class="headerCovTableEntry">287</td>
45-
<td class="headerCovTableEntryHi">94.1 %</td>
43+
<td class="headerCovTableEntry">363</td>
44+
<td class="headerCovTableEntry">387</td>
45+
<td class="headerCovTableEntryHi">93.8 %</td>
4646
</tr>
4747
<tr>
4848
<td></td>
4949
<td></td>
5050
<td></td>
5151
<td class="headerItem">Branches:</td>
52-
<td class="headerCovTableEntry">161</td>
53-
<td class="headerCovTableEntry">161</td>
54-
<td class="headerCovTableEntryHi">100.0 %</td>
52+
<td class="headerCovTableEntry">220</td>
53+
<td class="headerCovTableEntry">226</td>
54+
<td class="headerCovTableEntryHi">97.3 %</td>
5555
</tr>
5656
<tr><td><img src="glass.png" width=3 height=3 alt=""></td></tr>
5757
</table>
@@ -82,14 +82,38 @@
8282
<td class="tableHead" colspan=2>Branches <span class="tableHeadSort"><img src="glass.png" width=10 height=14 alt="Sort by branch coverage" title="Sort by branch coverage" border=0></span></td>
8383
</tr>
8484
<tr>
85-
<td class="coverFile"><a href="src/rules/operation/index.html">src/rules/operation</a></td>
85+
<td class="coverFile"><a href="src/rules/validation/abstract/RuleERC2980/index.html">src/rules/validation/abstract/RuleERC2980</a></td>
8686
<td class="coverBar" align="center">
8787
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=100 height=10 alt="100.0%"></td></tr></table>
8888
</td>
8989
<td class="coverPerHi">100.0&nbsp;%</td>
90-
<td class="coverNumHi">18 / 18</td>
90+
<td class="coverNumHi">38 / 38</td>
9191
<td class="coverPerHi">100.0&nbsp;%</td>
92-
<td class="coverNumHi">6 / 6</td>
92+
<td class="coverNumHi">12 / 12</td>
93+
<td class="coverPerMed">83.3&nbsp;%</td>
94+
<td class="coverNumMed">10 / 12</td>
95+
</tr>
96+
<tr>
97+
<td class="coverFile"><a href="src/rules/operation/abstract/index.html">src/rules/operation/abstract</a></td>
98+
<td class="coverBar" align="center">
99+
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=98 height=10 alt="98.4%"><img src="snow.png" width=2 height=10 alt="98.4%"></td></tr></table>
100+
</td>
101+
<td class="coverPerHi">98.4&nbsp;%</td>
102+
<td class="coverNumHi">240 / 244</td>
103+
<td class="coverPerHi">94.8&nbsp;%</td>
104+
<td class="coverNumHi">73 / 77</td>
105+
<td class="coverPerHi">92.6&nbsp;%</td>
106+
<td class="coverNumHi">50 / 54</td>
107+
</tr>
108+
<tr>
109+
<td class="coverFile"><a href="src/rules/operation/index.html">src/rules/operation</a></td>
110+
<td class="coverBar" align="center">
111+
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=94 height=10 alt="94.0%"><img src="snow.png" width=6 height=10 alt="94.0%"></td></tr></table>
112+
</td>
113+
<td class="coverPerHi">94.0&nbsp;%</td>
114+
<td class="coverNumHi">47 / 50</td>
115+
<td class="coverPerMed">86.4&nbsp;%</td>
116+
<td class="coverNumMed">19 / 22</td>
93117
<td class="coverPerHi">-</td>
94118
<td class="coverNumHi">0 / 0</td>
95119
</tr>
@@ -99,9 +123,9 @@
99123
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=100 height=10 alt="100.0%"></td></tr></table>
100124
</td>
101125
<td class="coverPerHi">100.0&nbsp;%</td>
102-
<td class="coverNumHi">133 / 133</td>
126+
<td class="coverNumHi">164 / 164</td>
103127
<td class="coverPerHi">100.0&nbsp;%</td>
104-
<td class="coverNumHi">79 / 79</td>
128+
<td class="coverNumHi">95 / 95</td>
105129
<td class="coverPerHi">-</td>
106130
<td class="coverNumHi">0 / 0</td>
107131
</tr>
@@ -111,71 +135,47 @@
111135
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=100 height=10 alt="100.0%"></td></tr></table>
112136
</td>
113137
<td class="coverPerHi">100.0&nbsp;%</td>
114-
<td class="coverNumHi">9 / 9</td>
115-
<td class="coverPerHi">100.0&nbsp;%</td>
116-
<td class="coverNumHi">3 / 3</td>
138+
<td class="coverNumHi">13 / 13</td>
117139
<td class="coverPerHi">100.0&nbsp;%</td>
118140
<td class="coverNumHi">4 / 4</td>
119-
</tr>
120-
<tr>
121-
<td class="coverFile"><a href="src/rules/validation/abstract/RuleERC2980/index.html">src/rules/validation/abstract/RuleERC2980</a></td>
122-
<td class="coverBar" align="center">
123-
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=100 height=10 alt="100.0%"></td></tr></table>
124-
</td>
125141
<td class="coverPerHi">100.0&nbsp;%</td>
126-
<td class="coverNumHi">36 / 36</td>
127-
<td class="coverPerHi">100.0&nbsp;%</td>
128-
<td class="coverNumHi">12 / 12</td>
129-
<td class="coverPerHi">100.0&nbsp;%</td>
130-
<td class="coverNumHi">8 / 8</td>
142+
<td class="coverNumHi">4 / 4</td>
131143
</tr>
132144
<tr>
133145
<td class="coverFile"><a href="src/rules/validation/abstract/RuleAddressSet/index.html">src/rules/validation/abstract/RuleAddressSet</a></td>
134146
<td class="coverBar" align="center">
135-
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=96 height=10 alt="96.3%"><img src="snow.png" width=4 height=10 alt="96.3%"></td></tr></table>
147+
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=96 height=10 alt="96.4%"><img src="snow.png" width=4 height=10 alt="96.4%"></td></tr></table>
136148
</td>
137-
<td class="coverPerHi">96.3&nbsp;%</td>
138-
<td class="coverNumHi">52 / 54</td>
149+
<td class="coverPerHi">96.4&nbsp;%</td>
150+
<td class="coverNumHi">54 / 56</td>
139151
<td class="coverPerHi">90.5&nbsp;%</td>
140152
<td class="coverNumHi">19 / 21</td>
141153
<td class="coverPerHi">100.0&nbsp;%</td>
142-
<td class="coverNumHi">8 / 8</td>
154+
<td class="coverNumHi">12 / 12</td>
143155
</tr>
144156
<tr>
145157
<td class="coverFile"><a href="src/rules/validation/abstract/core/index.html">src/rules/validation/abstract/core</a></td>
146158
<td class="coverBar" align="center">
147-
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=93 height=10 alt="93.3%"><img src="snow.png" width=7 height=10 alt="93.3%"></td></tr></table>
148-
</td>
149-
<td class="coverPerHi">93.3&nbsp;%</td>
150-
<td class="coverNumHi">56 / 60</td>
151-
<td class="coverPerMed">82.6&nbsp;%</td>
152-
<td class="coverNumMed">19 / 23</td>
153-
<td class="coverPerHi">100.0&nbsp;%</td>
154-
<td class="coverNumHi">14 / 14</td>
155-
</tr>
156-
<tr>
157-
<td class="coverFile"><a href="src/rules/operation/abstract/index.html">src/rules/operation/abstract</a></td>
158-
<td class="coverBar" align="center">
159-
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=97 height=10 alt="97.4%"><img src="snow.png" width=3 height=10 alt="97.4%"></td></tr></table>
159+
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=94 height=10 alt="94.0%"><img src="snow.png" width=6 height=10 alt="94.0%"></td></tr></table>
160160
</td>
161-
<td class="coverPerHi">97.4&nbsp;%</td>
162-
<td class="coverNumHi">75 / 77</td>
163-
<td class="coverPerHi">91.7&nbsp;%</td>
164-
<td class="coverNumHi">22 / 24</td>
161+
<td class="coverPerHi">94.0&nbsp;%</td>
162+
<td class="coverNumHi">79 / 84</td>
163+
<td class="coverPerMed">82.8&nbsp;%</td>
164+
<td class="coverNumMed">24 / 29</td>
165165
<td class="coverPerHi">100.0&nbsp;%</td>
166-
<td class="coverNumHi">16 / 16</td>
166+
<td class="coverNumHi">20 / 20</td>
167167
</tr>
168168
<tr>
169169
<td class="coverFile"><a href="src/rules/validation/abstract/base/index.html">src/rules/validation/abstract/base</a></td>
170170
<td class="coverBar" align="center">
171-
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=98 height=10 alt="97.7%"><img src="snow.png" width=2 height=10 alt="97.7%"></td></tr></table>
171+
<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="emerald.png" width=98 height=10 alt="97.8%"><img src="snow.png" width=2 height=10 alt="97.8%"></td></tr></table>
172172
</td>
173-
<td class="coverPerHi">97.7&nbsp;%</td>
174-
<td class="coverNumHi">385 / 394</td>
175-
<td class="coverPerHi">92.4&nbsp;%</td>
176-
<td class="coverNumHi">110 / 119</td>
173+
<td class="coverPerHi">97.8&nbsp;%</td>
174+
<td class="coverNumHi">447 / 457</td>
175+
<td class="coverPerHi">92.1&nbsp;%</td>
176+
<td class="coverNumHi">117 / 127</td>
177177
<td class="coverPerHi">100.0&nbsp;%</td>
178-
<td class="coverNumHi">111 / 111</td>
178+
<td class="coverNumHi">124 / 124</td>
179179
</tr>
180180
</table>
181181
</center>

0 commit comments

Comments
 (0)