Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
716bb47
Update spec for rc6
rya-sge Oct 14, 2026
b152e94
Update CMTAT submodule to v3.3.0-rc4 and fix the CMTAT transferred-o…
rya-sge Oct 14, 2026
a84f04d
Update CMTAT to v3.3.0-rc4 and apply the RuleEngine documentation rev…
rya-sge Oct 14, 2026
bc32f20
docs: remove the RuleEngine v3.0.0-rc3 row from the compatibility t…
rya-sge Oct 14, 2026
cc984d8
test: add a real ERC-3643 token suite (T-REX 4.2.0-beta1, 13 tests) …
rya-sge Oct 14, 2026
32ed59c
feat: version() returns 3.0.0-rc6 to match the release tag, with a ve…
rya-sge Oct 14, 2026
bcd9a88
chore(release): bump version to 3.0.0-rc7 and move the post-rc6 cha…
rya-sge Oct 14, 2026
e6b047a
docs(changelog): add the rc7 breaking-change section and an unrelea…
rya-sge Oct 14, 2026
2b7460f
chore(deps): compile with Solidity 0.8.37 (checked against the releas…
rya-sge Oct 14, 2026
3edada4
chore(config): target the Osaka EVM (Fusaka) instead of Prague; deplo…
rya-sge Oct 14, 2026
056ca5f
Run npm audit fix
rya-sge Oct 14, 2026
4219823
chore(deps): update forge-std to v1.17.0 (drop-in, removes the 58 Vm…
rya-sge Oct 14, 2026
5a45e07
docs: remove the outdated Surya IRuleEngine inheritance graph and sta…
rya-sge Oct 14, 2026
2b3dee4
docs: replace the outdated CMTAT code screenshots in doc/other/CMTA…
rya-sge Oct 14, 2026
a91ca9a
style: apply forge fmt to the interface-ID libraries (line wrapping …
rya-sge Oct 14, 2026
9ee134d
docs(security): add the v3.0.0-rc7 Slither and Aderyn reports with f…
rya-sge Oct 14, 2026
6476c3a
Update changelog
rya-sge Oct 14, 2026
4946c87
fix(security): identify tokens by msg.sender in the bound-token gate …
rya-sge Oct 15, 2026
5786de8
docs(security): re-run Slither and Aderyn on the ERC-2771 fix (4946c…
rya-sge Oct 15, 2026
c54e69a
test: add rule-aggregation fuzz tests, self-binding approval event and
rya-sge Oct 15, 2026
a183425
feat: split the RBAC engine into RuleEngine (no ERC-2771, constructor…
rya-sge Oct 15, 2026
850976f
ommit message:
rya-sge Oct 15, 2026
3b94564
fix: raise the pragma to ^0.8.27, the real floor for require with c…
rya-sge Oct 15, 2026
2c0092e
docs(security): second pass of the v3.0.0-rc7 code-quality review on …
rya-sge Oct 15, 2026
9c12cdf
erf: iterate the rule set directly in the RuleEngineBase view loops i…
rya-sge Oct 15, 2026
9f9628c
perf: read each rule once in messageForTransferRestriction, saving …
rya-sge Oct 15, 2026
7dca285
perf: rely on the EnumerableSet return value in addRule/removeRule in…
rya-sge Oct 15, 2026
34e113c
refactor: fold RuleEngineAccessControlShared into RuleEngine so RuleE…
rya-sge Oct 15, 2026
6b1d67c
refactor: fold RuleEngineAccessControlShared into RuleEngine so RuleE…
rya-sge Oct 15, 2026
29b60b1
ci: run the real ERC-3643 suite (FOUNDRY_PROFILE=erc3643 forge test)…
rya-sge Oct 15, 2026
3d45562
test: cover a reference rule set directly as a CMTAT token's rule eng…
rya-sge Oct 15, 2026
ba86f58
refactor: replace the remaining if/revert sites in src and the mock r…
rya-sge Oct 15, 2026
743d6e8
feat: narrow IRule to the functions the engine calls (no canTransfer/…
rya-sge Oct 15, 2026
310e748
docs: state that bindTokens/unbindTokens are atomic by design and why…
rya-sge Oct 15, 2026
f62e8c3
docs(security): re-run the J-1 registry reuse probes on the final rc7…
rya-sge Oct 15, 2026
4c92713
docs(security): present the rc7 J-1 registry reuse checks as examples…
rya-sge Oct 15, 2026
9d60b26
feat: accept the legacy IRule ERC-165 id 0x2497d6cb alongside the nar…
rya-sge Oct 15, 2026
3055fe2
docs: fix the remaining stale NatSpec and comments in src (RulesMana…
rya-sge Oct 15, 2026
4e49528
docs(security): re-run Slither and Aderyn on the final rc7 code (30…
rya-sge Oct 15, 2026
e41e8a7
test: add handler-based invariant tests over the rule set and bound t…
rya-sge Oct 15, 2026
51d0337
docs: list every rc7 breaking change in the CHANGELOG - removed Opera…
rya-sge Oct 15, 2026
97cbd63
test: add rc7 security review tests on all six deployables (admin a…
rya-sge Oct 15, 2026
efaf860
fix: RuleEngineOwnable2Step re-checks the rule-account guard in
rya-sge Oct 15, 2026
367d28c
ocs(security): detail the implementation options for rc7 security rev…
rya-sge Oct 15, 2026
9c43b01
fix: an administrative unbind (unbindToken / unbindTokens) now revoke…
rya-sge Oct 15, 2026
c7a19c7
fix: keep rules and bound tokens disjoint - binding a current rule an…
rya-sge Oct 15, 2026
848bdb0
ocs(security): analyse a try/catch in canTransfer for rc7 security re…
rya-sge Oct 16, 2026
3d4913b
docs: check the rule documentation against Rules v0.6.0 (added as a r…
rya-sge Oct 16, 2026
29556e5
docs: check the rule documentation against Rules v0.6.0 (added as a r…
rya-sge Oct 16, 2026
74c5d47
docs(security): re-run Slither and Aderyn on the final rc7 code (2955…
rya-sge Oct 16, 2026
bfa9ece
docs: add a CMTAT compatibility table to the root README - RuleEngine…
rya-sge Oct 16, 2026
3386555
docs: replace the v3.0.0-rc2 compilation artifacts with the v3.0.0-rc…
rya-sge Oct 16, 2026
e52f1a7
docs: add a release checklist item requiring the latest compilation a…
rya-sge Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 4 additions & 3 deletions .claude/skills/testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ test/
### Organization Rules

- Tests for `RuleEngine` go in `test/RuleEngine/`
- Tests for each ERC-2771 contract go in its own directory: `test/RuleEngine2771/`, `test/RuleEngineOwnable2771/`, `test/RuleEngineOwnable2Step2771/`
- Tests for `RuleEngineOwnable` go in `test/RuleEngineOwnable/`
- Tests for `RuleEngineOwnable2Step` go in `test/RuleEngineOwnable2Step/`
- Shared test logic uses `*Base.sol` abstract contracts with virtual hooks
- Test files use `.t.sol` suffix; base/helper files use `.sol`
- **Both variants must be tested.** When adding a feature to `RuleEngineBase`, add tests in both `test/RuleEngine/` and `test/RuleEngineOwnable/`.
- **Every variant must be tested.** When adding a feature to `RuleEngineBase`, add tests for all six deployables, preferably through a shared `test/utils/*TestBase.sol` with one short file per variant directory.

## Helper Contracts

Expand Down Expand Up @@ -176,8 +178,7 @@ contract MyFeatureTest is Test, HelperContract {
ruleWhitelist = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS);
ruleEngineMock = new RuleEngine(
DEFAULT_ADMIN_ADDRESS,
ZERO_ADDRESS, // forwarder
ZERO_ADDRESS // token (bound later)
ZERO_ADDRESS // token (bound later); use RuleEngine2771 to test a forwarder
);
}

Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ jobs:

- name: Run Forge tests
run: forge test -vvv


# test/ERC3643Real/ is skipped by the default profile: it deploys the vendored T-REX token (solc 0.8.30)
- name: Run Forge tests against the real ERC-3643 token
run: FOUNDRY_PROFILE=erc3643 forge test -vvv

- name: Setup NodeJS 20.5.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
Expand Down
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ cache_hardhat/
*.dtmp
#claude
history
INSTRUCTION.md
PLAN.md
SUMMARY.md
FEEDBACK.md
draft
.codex
out-erc3643/

3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@
[submodule "ERC-3643"]
path = lib/ERC-3643
url = https://github.com/ERC-3643/ERC-3643
[submodule "lib/Rules"]
path = lib/Rules
url = https://github.com/CMTA/Rules
124 changes: 85 additions & 39 deletions AGENTS.md

Large diffs are not rendered by default.

158 changes: 157 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

124 changes: 85 additions & 39 deletions CLAUDE.md

Large diffs are not rendered by default.

72 changes: 49 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,39 @@ RuleEngine applies transfer restrictions to [CMTAT](https://github.com/CMTA/CMTA

It is an *external controller*: the token calls the engine on every transfer, mint and burn, and the engine forwards the call to a configurable list of pluggable rule contracts. This keeps compliance logic out of the token, lets each issuer compose the rules they need, and avoids growing the token's already large bytecode.

- **Version:** 3.0.0
- **Solidity:** ^0.8.20 (compiled with 0.8.36)
- **EVM target:** Prague
- **Version:** 3.0.0-rc7 (`version()` returns `"3.0.0-rc7"`)
- **Solidity:** ^0.8.27 (compiled with 0.8.37; 0.8.27 is the first version supporting `require` with a custom error on the legacy pipeline)
- **EVM target:** Osaka (Fusaka upgrade)
- **License:** MPL-2.0

**Full documentation: [doc/README.md](./doc/README.md)** — interfaces, Ethereum API, deployment, UML and call graphs, audits, and toolchain usage.

> This project has not undergone an audit and is provided as-is without any warranties.

## CMTAT compatibility

| RuleEngine | CMTAT | Tested against |
|---|---|---|
| **v3.0.0-rc7** (this version) | ≥ v3.0.0 | v3.0.0 (last audited CMTAT release) and v3.3.0-rc4 (target version) |
| earlier v3.0.0 release candidates | ≥ v3.0.0 | their target version, see [doc/README.md § Compatibility](./doc/README.md#compatibility) |
| [v1.0.2.1](https://github.com/CMTA/RuleEngine/releases/tag/v1.0.2.1) | v2.3.0 (audited) | — |

For a **CMTAT v3.0.0** token, use RuleEngine **v3.0.0-rc7**: it is tested against that version (v1.0.2.1 is for CMTAT v2 only). Up to CMTAT v3.2.0, mint and burn reach the engine without the operator, so spender-keyed rules (per-minter mint allowance) do not apply to them; see [doc/README.md § CMTAT v3.0.0](./doc/README.md#cmtat-v300).

## Contract Variants

Three deployable contracts share the same core logic and differ only in access control:
Six deployable contracts share the same core logic and differ only in access control and ERC-2771 support. Each access-control model comes in two versions: without ERC-2771, and with an immutable trusted forwarder (`*2771`):

| Contract | Access Control | Use Case |
|----------|---------------|----------|
| `RuleEngine` | Role-based (`AccessControlEnumerable`) | Multi-operator environments with granular permissions |
| `RuleEngineOwnable` | ERC-173 `Ownable` | Single-owner setups, simpler administration |
| `RuleEngineOwnable2Step` | ERC-173 `Ownable2Step` | Single-owner setups with safer ownership handover |
| Contract | Access Control | ERC-2771 (gasless) | Use Case |
|----------|---------------|--------------------|----------|
| `RuleEngine` | Role-based (`AccessControlEnumerable`) | No | Multi-operator environments with granular permissions |
| `RuleEngine2771` | Role-based (`AccessControlEnumerable`) | Yes (immutable trusted forwarder) | Same, with administration through meta-transactions |
| `RuleEngineOwnable` | ERC-173 `Ownable` | No | Single-owner setups, simpler administration |
| `RuleEngineOwnable2771` | ERC-173 `Ownable` | Yes (immutable trusted forwarder) | Same, with administration through meta-transactions |
| `RuleEngineOwnable2Step` | ERC-173 `Ownable2Step` | No | Single-owner setups with safer ownership handover |
| `RuleEngineOwnable2Step2771` | ERC-173 `Ownable2Step` | Yes (immutable trusted forwarder) | Same, with administration through meta-transactions |

All three support ERC-1404 transfer restrictions, the ERC-3643 compliance interface, ERC-2771 meta-transactions (gasless), and multiple token bindings.
All of them support ERC-1404 transfer restrictions, the ERC-3643 compliance interface and multiple token bindings.

> **Warning (shared engine across multiple tokens):** one RuleEngine instance can be bound to several tokens, but they must be equally trusted and governed together. ERC-3643 callbacks do not pass the token address to rules, so stateful rules are not safe for mutually untrusted tokens sharing an engine.

Expand All @@ -40,7 +53,7 @@ The token calls the engine on every transfer, mint and burn. The engine checks t
| CMTAT | `transferred(from, to, value)` | `transferred(spender, …)` | `transferred(spender, …)` | `transferred(spender, …)` |
| ERC-3643 | `transferred(from, to, value)` | `transferred(from, to, value)` | `created(to, value)` | `destroyed(from, value)` |

CMTAT picks the overload according to whether the operation has a spender. A plain `transfer` has none, so CMTAT calls the 3-argument `transferred(from, to, value)` — the engine is never called with a zero spender. `transferFrom`, `mint` and `burn` do have one (`_msgSender()`), so those call the 4-argument `transferred(spender, from, to, value)`. Whichever entry point is used, the engine then runs the same rule loop.
CMTAT picks the overload according to whether the operation has a spender. A plain `transfer` has none, so CMTAT calls the 3-argument `transferred(from, to, value)` — the engine is never called with a zero spender. `transferFrom`, `mint` and `burn` do have one (`_msgSender()`), so those call the 4-argument `transferred(spender, from, to, value)` — as do `batchMint`, `batchBurn`, `burnFrom`, cross-chain mint/burn and the minter `batchTransfer` (where `spender == from`). CMTAT's `forcedTransfer` does not call the engine at all. Whichever entry point is used, the engine then runs the same rule loop. The complete per-operation table is in [doc/technical/RuleEngine-with-CMTAT.md](./doc/technical/RuleEngine-with-CMTAT.md#2-which-entry-points-cmtat-uses).

The view path, `detectTransferRestriction()`, iterates the same rules and returns the first non-zero ERC-1404 restriction code instead of reverting.

Expand All @@ -58,9 +71,12 @@ RuleEngineBase (abstract) — core logic, shared by all variants
│ └── TokenBindingExtendedModule — batch binding, token self-binding
└── IRuleEngineERC1404 — CMTAT interface

RuleEngine = RuleEngineBase + AccessControl + ERC2771ModuleStandalone
RuleEngineOwnable = RuleEngineOwnableShared + Ownable + ERC2771ModuleStandalone
RuleEngineOwnable2Step = RuleEngineOwnableShared + Ownable2Step + ERC2771ModuleStandalone
RuleEngine = RuleEngineBase + AccessControlEnumerable
RuleEngine2771 = RuleEngine + ERC2771ModuleStandalone
RuleEngineOwnable = RuleEngineOwnableShared + Ownable
RuleEngineOwnable2771 = RuleEngineOwnable + ERC2771ModuleStandalone
RuleEngineOwnable2Step = RuleEngineOwnableShared + Ownable2Step
RuleEngineOwnable2Step2771 = RuleEngineOwnable2Step + ERC2771ModuleStandalone
```

Token binding is deliberately split in two layers: `TokenBindingModule` / `TokenBindingExtendedModule` hold the whole registry (storage, `bindToken` / `unbindToken` / `isTokenBound`, batch binding, self-binding, the `onlyBoundToken` guard) and depend on nothing but OpenZeppelin, so they can be reused by any project that has to bind tokens; `ERC3643ComplianceModule` / `ERC3643ComplianceExtendedModule` are thin ERC-3643 adapters on top, adding `getTokenBound()` and the compliance-manager vocabulary. `src/mocks/TokenBindingStandaloneMock.sol` shows the registry used on its own, outside any compliance context.
Expand All @@ -73,10 +89,15 @@ Modules declare access control as **virtual internal hooks** (`_onlyRulesManager
src/
├── RuleEngineBase.sol # abstract core logic (not deployable)
├── RuleEngineOwnableShared.sol # shared logic for the two ownable variants
├── deployment/ # the three deployable contracts
│ ├── RuleEngine.sol
│ ├── RuleEngineOwnable.sol
│ └── RuleEngineOwnable2Step.sol
├── deployment/ # the six deployable contracts, one directory per access-control model
│ ├── accessControl/ # RBAC (AccessControlEnumerable)
│ │ ├── RuleEngine.sol
│ │ └── RuleEngine2771.sol
│ └── ownable/ # ERC-173 (Ownable / Ownable2Step)
│ ├── RuleEngineOwnable.sol
│ ├── RuleEngineOwnable2771.sol
│ ├── RuleEngineOwnable2Step.sol
│ └── RuleEngineOwnable2Step2771.sol
├── interfaces/ # IRule, IRulesManagementModule, ITokenBinding(Extended),
│ # IERC3643Compliance(Extended)
├── modules/ # VersionModule, RulesManagementModule, TokenBinding(Extended)Module,
Expand All @@ -95,25 +116,30 @@ doc/ # full documentation, schemas, coverage, aud

Production rules are maintained at [github.com/CMTA/Rules](https://github.com/CMTA/Rules), in two families:

- **Validation rules (read-only)** — evaluate eligibility without mutating state: `RuleWhitelist`, `RuleBlacklist`, `RuleSanctionList`, `RuleIdentityRegistry`, `RuleSpenderWhitelist`, `RuleERC2980`, `RuleMaxTotalSupply`
- **Operation rules (read-write)** — may update rule state on transfer: `RuleConditionalTransferLight`
- **Validation rules (read-only)** — evaluate eligibility without mutating state: `RuleWhitelist`, `RuleWhitelistWrapper`, `RuleReceiverWhitelist`, `RuleBlacklist`, `RuleSanctionsList`, `RuleIdentityRegistry`, `RuleSpenderWhitelist`, `RuleERC2980`, `RuleMaxTotalSupply` / `RuleMaxTotalSupplyERC3643`, `RuleMaxBalance`, `RuleChainlinkPoR` / `RuleChainlinkPoRERC3643`
- **Operation rules (read-write)** — may update rule state on transfer: `RuleConditionalTransferLight`, `RuleMintAllowance`, and `RuleConditionalTransferLightMultiToken` (set directly on each token, never behind a RuleEngine)

To be usable by the engine, a rule must implement `IRule` and advertise it through ERC-165. Restriction codes should stay unique across the composed rule set.
The Rules repository is the source of truth for this list; see [doc/README.md § Available Rules](./doc/README.md#available-rules) for details and caveats: restriction codes, ERC-3643 support per rule, rules that protect one token per instance, and how to bind the operation rules to the engine.

To be usable by the engine, a rule must implement `IRule` and advertise it through ERC-165 (`0xb1a69752` since v3.0.0-rc7; the older `0x2497d6cb`, which also covers `canTransfer` / `canTransferFrom` and is advertised by Rules up to v0.6.0, is still accepted). Restriction codes should stay unique across the composed rule set.

## Quick start

Dependencies are git submodules.

```bash
git submodule update --init --recursive # or: forge install
cd lib/CMTAT && npm install && cd ../.. # CMTAT's own OpenZeppelin deps
git submodule update --init # non-recursive is enough: remappings.txt resolves OpenZeppelin
# to this repo's own submodules, including for the CMTAT sources

forge build # compile
forge test # run the test suite
FOUNDRY_PROFILE=erc3643 forge test # tests against the real ERC-3643 token (T-REX 4.2.0-beta1, solc 0.8.30)
forge coverage # code coverage
forge fmt # format
```

`--recursive` and `cd lib/CMTAT && npm install` are only needed to run CMTAT's own Hardhat suite from inside `lib/CMTAT`.

See [doc/README.md](./doc/README.md) for deployment scripts, the production deployment checklist, and Hardhat usage.

Parts of this project were written with the help of AI coding assistants, principally Claude Code (Anthropic) and Codex (OpenAI).
Expand Down
Loading
Loading