Skip to content

Commit ab9def2

Browse files
authored
Merge pull request #68 from CMTA/dev
v3.3.0-rc5
2 parents 66fcf2a + 4995503 commit ab9def2

243 files changed

Lines changed: 10689 additions & 4075 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.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ lib/
66
out/
77
docOut/
88
cache/
9+
#coverage scratch output (the published report lives in doc/coverage/)
10+
lcov.info
11+
/coverage/
912
.~lock.test.odt#
1013
nethereum-gen.settings
1114
#hardhat

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@
1313
[submodule "lib/CMTATv3.0.0"]
1414
path = lib/CMTATv3.0.0
1515
url = https://github.com/CMTA/CMTAT
16+
[submodule "ERC-3643"]
17+
path = lib/ERC-3643
18+
url = https://github.com/ERC-3643/ERC-3643

AGENTS.md

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
This file helps AI agents (Cursor, Claude Code, etc.) understand and work with this codebase.
44

5-
AGENTS.md and CLAUDE.md files must always be identical
5+
AGENTS.md and CLAUDE.md files must always be identical — always update both together.
66

77
## Project Summary
88

99
**RuleEngine** is a Solidity smart contract system that enforces transfer restrictions for [CMTAT](https://github.com/CMTA/CMTAT) and [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) tokens. It acts as an external controller that calls pluggable rule contracts on each token transfer, mint, or burn.
1010

1111
- **Version:** 3.0.0 (defined in `src/modules/VersionModule.sol`)
12-
- **Solidity:** ^0.8.20 (compiled with 0.8.34)
12+
- **Solidity:** ^0.8.20 (compiled with 0.8.36)
1313
- **EVM target:** Prague
1414
- **License:** MPL-2.0
1515

@@ -21,13 +21,27 @@ forge test # Run all tests
2121
forge test -vvv # Verbose test output
2222
forge test --match-contract <Name> --match-test <fn> # Run specific test
2323
forge coverage # Code coverage
24-
forge coverage --no-match-coverage "(script|mocks|test)" --report lcov # Production coverage
24+
forge coverage --no-match-coverage "(mocks|test)" --report lcov # Production coverage (src/ + script/)
2525
forge fmt # Format code
2626
```
2727

2828
Dependencies are git submodules. Initialize with `forge install`, update with `forge update`.
2929
CMTAT submodule also needs `cd lib/CMTAT && npm install` for its OpenZeppelin deps.
3030

31+
## Agent Workflow
32+
33+
- **Never create git commits.** Provide commit messages only when they are requested.
34+
- **Always run the full test suite (`forge test`) after any code modification** — including lint-driven or mechanical refactors — before reporting completion.
35+
- **Always update the documentation** to reflect the latest change. There are two READMEs: `README.md` at the root is the short overview (project, architecture, main files, quick start); `doc/README.md` is the full reference (interfaces, Ethereum API, deployment, UML, audits). Update whichever the change affects — often both.
36+
- After each implemented feature or fix, provide a **one-line GitHub commit message** covering all changes since the last commit.
37+
38+
### When implementing a new rule or feature
39+
40+
1. Create or update the technical documentation in `doc/technical`
41+
2. Update `README.md` (root overview) and `doc/README.md` (full reference) as applicable
42+
3. Create or update tests, targeting **100% code coverage** — check with `forge coverage --report summary`
43+
4. Update `CHANGELOG.md`
44+
3145
## Import Remappings
3246

3347
| Alias | Path |
@@ -132,31 +146,42 @@ function _checkRule(address rule_) internal view virtual override {
132146
### Rule Execution Flow
133147

134148
```
135-
Token operation → RuleEngine.transferred(spender, from, to, value) ← CMTAT v3.3.0+ primary path
149+
CMTAT only: RuleEngine.transferred(spender, from, to, value) ← transferFrom, mint, burn (spender = _msgSender())
136150
├── onlyBoundToken modifier (caller must be bound)
137151
└── for each rule in _rules:
138152
rule.transferred(spender, from, to, value) // reverts if disallowed
139153
140-
RuleEngine.transferred(from, to, value) ← 3-arg fallback (spender == address(0))
154+
CMTAT + ERC-3643: RuleEngine.transferred(from, to, value) ← standard transfer (spender == address(0))
141155
├── onlyBoundToken modifier
142156
└── for each rule in _rules:
143157
rule.transferred(from, to, value)
144158
145-
RuleEngine.created(to, value) ← ERC-3643 mint entry point
159+
ERC-3643 only: RuleEngine.created(to, value) ← ERC-3643 mint entry point
146160
├── onlyBoundToken modifier
147161
└── calls _transferred(address(0), to, value)
148162
149-
RuleEngine.destroyed(from, value) ← ERC-3643 burn entry point
163+
ERC-3643 only: RuleEngine.destroyed(from, value) ← ERC-3643 burn entry point
150164
├── onlyBoundToken modifier
151165
└── calls _transferred(from, address(0), value)
152166
```
153167

154-
Since CMTAT v3.3.0, mint (`from == address(0)`) and burn (`to == address(0)`) also go through the 4-argument overload with the operator as `spender`. Rules that check `spender` must skip or adapt that check for mint/burn to avoid blocking those operations unintentionally.
168+
**CMTAT and ERC-3643 use disjoint entry points.** The 4-argument `transferred` is declared by CMTAT's `IRuleEngine` (`lib/CMTAT/contracts/interfaces/engine/IRuleEngine.sol`), so an ERC-3643 token never reaches it. Conversely `created` / `destroyed` are declared by `IERC3643Compliance` and CMTAT never calls them — CMTAT routes mint and burn through the 4-argument `transferred` instead. Only the 3-argument `transferred` is shared by both.
169+
170+
CMTAT selects the overload in `ValidationModuleRuleEngine._callRuleEngineTransferred`, branching on `spender != address(0)`. A standard `transfer` has no spender (`CMTATBaseCommon.transfer` passes `address(0)` internally), so the `else` branch calls the **3-argument** `transferred(from, to, value)` — the zero address is a branch condition only and is never forwarded to the engine. `transferFrom`, `mint` and `burn` carry `_msgSender()` as spender and take the **4-argument** overload. Neither is a fallback: which one is called depends purely on the operation.
171+
172+
Since CMTAT v3.3.0, mint (`from == address(0)`) and burn (`to == address(0)`) therefore also reach the 4-argument overload with the operator as `spender`. Rules that check `spender` must skip or adapt that check for mint/burn to avoid blocking those operations unintentionally.
155173

156174
`created` and `destroyed` use the 3-argument `_transferred` path (no spender), consistent with the ERC-3643 spec which does not carry a spender for mint/burn.
157175

158176
View path: `detectTransferRestriction()` iterates rules, returns first non-zero code.
159177

178+
**The 3-argument view path fails open for spender-dependent rules.** `detectTransferRestriction` and
179+
`canTransfer` carry no `spender`, so a rule keyed by spender (e.g. a per-minter mint allowance) cannot
180+
evaluate the operation and must answer "no restriction". The engine aggregates that answer, so these two views
181+
can report a mint as allowed that `transferred(spender, ...)` will revert. Use the 4-argument
182+
`detectTransferRestrictionFrom` / `canTransferFrom` to pre-check an operation that has an operator. See
183+
`H-1` in `doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md`.
184+
160185
### Storage: EnumerableSet
161186

162187
Both rules and bound tokens use `EnumerableSet.AddressSet`:
@@ -255,8 +280,11 @@ Key points:
255280
- NatSpec comments on all public/external functions
256281
- Function ordering: constructor, receive, fallback, external, public, internal, private (view/pure last within each group)
257282
- Function declaration order: visibility, mutability, virtual, override, custom modifiers
283+
- All `internal` functions must be marked `virtual`, so inheriting contracts can override them.
284+
- Use `require(condition, CustomError(...))` for custom errors; avoid direct `revert CustomError(...)`.
258285
- In `src/`, avoid `super` calls and prefer explicit parent-contract calls (e.g., `AccessControl.grantRole(...)`) for readability and deterministic inheritance behavior.
259286
- Section headers: `/* ============ SECTION ============ */`
287+
- **No emoji in code comments or NatSpec.** Use a plain word marker instead: `WARNING:`, `NOTE:`, `IMPORTANT:`. Emoji render inconsistently across editors, terminals, `forge doc` output and diffs; they are not searchable (`grep WARNING` finds the marker, `grep ⚠️` depends on the shell); and they encode as multi-byte sequences that can be silently mangled by tooling. This applies to `src/`, `test/` and `script/`. Markdown documentation may use emoji freely — the restriction is Solidity comments only.
260288
- Run `forge fmt` before committing
261289

262290
## Common Tasks

CHANGELOG.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,56 @@ forge lint
4747

4848

4949

50+
### v3.0.0-rc5
51+
52+
### Changed
53+
54+
- `ERC3643ComplianceModule._bindToken` / `_unbindToken`: rely on the `EnumerableSet` mutation return value instead of a preceding `contains()` lookup, keeping the `TokenAlreadyBound` / `TokenNotBound` diagnostics (269 gas measured).
55+
- `_bindToken`, `_unbindToken` and `RuleEngineBase._supportsRuleEngineBaseInterface` are now `virtual`, along with the remaining non-`virtual` internals in the mock rules, per the project convention.
56+
- `RuleAddressList.addressIsListedBatch`: `memory` parameter changed to `calldata` (587 gas measured for 10 addresses).
57+
- Deployment now emits `SetMaxRules` with the initial cap, so the event log alone is sufficient to reconstruct `maxRules`.
58+
- `RulesManagementModule`: the rule-cap write and its event moved into a new `internal virtual _setMaxRules(uint256)`, called by `setMaxRules` and by the deployable contracts' constructors. `_maxRules` is now written from a single place, so the invariant "every change to the cap emits `SetMaxRules`" holds structurally rather than by convention, and the non-zero check guards every path including construction.
59+
- `RulesManagementModule`: rule insertion moved into a new `internal virtual _addRule(IRule)`, called by `addRule` and by the `setRules` loop. `AddRule` is now emitted from a single site. The `maxRules` cap is deliberately checked by the callers, since `addRule` checks per insertion while `setRules` checks the whole batch up front.
60+
61+
### Added
62+
63+
- Add `ERC3643TokenMock`: a minimal ERC-3643 (T-REX) style token whose compliance interaction mirrors `Token.sol` from the reference implementation, used to test the RuleEngine through the ERC-3643 entry points (`setCompliance` self-binding, `transferred`, `created`, `destroyed`).
64+
- Add `ERC3643TokenIntegration.t.sol` (11 tests), including a regression guard for the H-1 mint pre-check fail-open and one pinning the requirement that `address(0)` be whitelisted for an ERC-3643 token to mint.
65+
66+
- **Renamed the reference rules in `src/mocks/rules/` with a `Mock` suffix**, so a reader cannot mistake them for the production rules of the same name maintained in [CMTA/Rules](https://github.com/CMTA/Rules): `RuleWhitelist` -> `RuleWhitelistMock`, `RuleConditionalTransferLight` -> `RuleConditionalTransferLightMock`, `RuleMintAllowance` -> `RuleMintAllowanceMock`, `RuleOperationRevert` -> `RuleOperationRevertMock`. Files renamed to match. The abstract bases and invariant-storage contracts they build on are unchanged, as they are not themselves rules.
67+
68+
### Removed
69+
70+
- `RuleEngine_ERC3643Compliance_OperationNotSuccessful`: unreachable after the bind/unbind simplification and referenced nowhere else.
71+
72+
### Documentation
73+
74+
- Document that the ERC-1404 3-argument `canTransfer` / `detectTransferRestriction` path fails open for spender-dependent rules, and that `canTransferFrom` / `detectTransferRestrictionFrom` must be used to pre-check an operation that has an operator.
75+
- Add the code-quality review in [doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md](./doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md).
76+
- Add integration guides in `doc/technical`: [RuleEngine-with-CMTAT.md](./doc/technical/RuleEngine-with-CMTAT.md) and [RuleEngine-with-ERC3643.md](./doc/technical/RuleEngine-with-ERC3643.md), covering entry points, configuration, warnings, limitations and test coverage for each token standard.
77+
- Add the script review in [doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS_SCRIPT.md](./doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS_SCRIPT.md).
78+
- Add the v3.0.0-rc5 Slither and Aderyn reports with their assessment feedback, each prefixed with a summary table of findings and dispositions.
79+
- Add [doc/security/audits/AUDIT_OVERVIEW.md](./doc/security/audits/AUDIT_OVERVIEW.md) indexing every analysis performed, the static-analysis results per tool, and the substantive findings that were fixed.
80+
81+
### Fixed
82+
83+
- `RuleEngineScript.s.sol`: the CMTAT token is now bound to the engine (passed to the constructor). Previously the script produced a deployment in which every transfer, mint and burn reverted with `RuleEngine_ERC3643Compliance_UnauthorizedCaller`, because the token was never bound.
84+
- `RuleEngineScript.s.sol`: `setRuleEngine` is now called through the typed interface instead of a low-level `.call` guarded by a bare `require(success)`. The previous form returned success when `CMTAT_ADDRESS` held no code, silently producing an unconfigured deployment, and discarded the revert reason on failure.
85+
- `RuleEngineScript.s.sol`: the demo whitelist is now seeded with the deployer and `address(0)`, so the resulting deployment can transfer, mint and burn as-is.
86+
- `test/script/RuleEngineScript.t.sol`: asserts the resulting deployment works (engine set, token bound, rule configured, a real mint) instead of only that `run()` does not revert.
87+
- `doc/script/script_surya_*.sh`: fixed the shebang (`#/bin/bash` -> `#!/bin/bash`), the undefined `$dir` loop variable, `mkdir` without `-p` in the report script, and the output-directory guard in the inheritance script; added `set -euo pipefail` and null-delimited `find` iteration to all three. The loop iterates `find .` rather than an absolute path on purpose: `surya mdreport` embeds the path it is given, so an absolute one would write machine-specific paths into the committed reports under `doc/schema/surya/surya_report`.
88+
- `package.json`: the `surya:*` and `uml:*` scripts now write beneath `docOut/` (gitignored) instead of the repository root.
89+
- `doc/script/convert_links_for_pdf.sh`: the default input is now `doc/README.md` (the full documentation) rather than the short root README.
90+
91+
### Dependencies
92+
93+
- Update CMTAT submodule to [v3.3.0-rc3](https://github.com/CMTA/CMTAT/releases/tag/v3.3.0-rc3).
94+
- Update OpenZeppelin Contracts and OpenZeppelin Contracts Upgradeable submodules to [v5.7.0](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.7.0).
95+
5096
### v3.0.0-rc4 - 2026-05-22
5197

98+
Commit: `66fcf2aafebd1f9d9de8a81dec92b88da071c9b3`
99+
52100
### Added
53101

54102
- Add `RuleMintAllowance` mock rule: admin-controlled per-minter mint allowance with `setMintAllowance(address, uint256)`, enforcement in `transferred(spender, from, to, value)` when `from == address(0)`, and `CODE_MINTER_INSUFFICIENT_ALLOWANCE` (code 81).

0 commit comments

Comments
 (0)