Skip to content
Merged

Dev #60

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
91d512d
Update doc and add audit agent report
rya-sge Mar 17, 2026
3e030a6
ID-2:remove IAccessControl ERC165 advertisement and document Finding …
rya-sge Mar 17, 2026
b709d1d
ID-7: docs(rules-management): clarify setRules non-empty design and d…
rya-sge Mar 17, 2026
a4740b2
ID-1: docs(compliance) - Add trust warnings
rya-sge Mar 17, 2026
1caf4ea
Id-3: docs(rules-management): clarify operator-owned rule-count risk …
rya-sge Mar 17, 2026
3fff502
ID-4: docs(restrictions): enforce unique-code convention or identical…
rya-sge Mar 17, 2026
33ff6fa
ID5: docs(access-control): warn that rule contracts must never hold R…
rya-sge Mar 17, 2026
4ef6cd5
ID-6: feat(erc165): advertise ERC-3643 and IERC7551 subset IDs with d…
rya-sge Mar 17, 2026
9867c0c
Add nethermind audit agent feedback
rya-sge Mar 17, 2026
e523637
refactor(erc165): centralize compliance interface IDs in shared libra…
rya-sge Mar 17, 2026
8f2f80b
fix(mocks): correct OpenZeppelin IERC165 imports to resolve AccessCon…
rya-sge Mar 17, 2026
91a2890
update changelog
rya-sge Mar 17, 2026
3be724a
Update CMTAT to v3.2.0, OpenZeppelin standard and upgrade version to …
rya-sge Mar 17, 2026
74438f3
Update library in documentation
rya-sge Mar 17, 2026
1228f18
refactor(tests): move compliance mock interfaces to src/mocks and upd…
rya-sge Mar 17, 2026
7fb8f56
Update audit tools documentation
rya-sge Mar 17, 2026
d0a2bb2
refactor(access): remove AccessControl from RulesManagementModule and…
rya-sge Mar 17, 2026
c735820
refactor(deployment): restore RBAC contract name to RuleEngine while …
rya-sge Mar 18, 2026
75d1bc0
Update slither and aderyn report
rya-sge Mar 18, 2026
a5a7010
Update slither and aderyn report + contract code size
rya-sge Mar 18, 2026
45ad5ec
Update code coverage
rya-sge Mar 18, 2026
7b81240
Add Ownable2Step variant
rya-sge Mar 18, 2026
07a59a9
Factor shared ERC-165 RuleEngine interface support into RuleEngineBas…
rya-sge Mar 19, 2026
6756b31
Update surya schema
rya-sge Mar 29, 2026
82cdc15
Update Nethermind Audit Agent Report
rya-sge Mar 29, 2026
aa7514e
Use AccessControlEnumerable instead of AccessControl
rya-sge Mar 29, 2026
5a4a19e
Update README
rya-sge Mar 29, 2026
0c209cf
Udate documentation and script
rya-sge Apr 13, 2026
1481646
Update schema
rya-sge Apr 13, 2026
703e978
Run npm audit fix
rya-sge Apr 13, 2026
10aee03
Update OpenZeppelib library to v5.6.1
rya-sge Apr 13, 2026
2728e09
update remapping and hardhat config files to make it work with hardhat
rya-sge Apr 13, 2026
e3f95a5
Add compilation files for v3.0.0-rc2
rya-sge Apr 14, 2026
833adbf
Add small test for hardhat
rya-sge Apr 14, 2026
5a198ef
Update README
rya-sge Apr 14, 2026
47f5361
Update changelog and doc
rya-sge Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,17 @@ jobs:

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

- name: Setup NodeJS 20.5.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version: 20.5.0

- name: Show NodeJS version
run: npm --version

- name: Install Project Dependencies
run: npm install

- name: Run Hardhat Test
run: npx hardhat test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ cache_hardhat/
*.bkp
*.dtmp
#claude
history
INSTRUCTION.md
PLAN.md
SUMMARY.md
FEEDBACK.md
.codex
258 changes: 258 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
# AI Agent Guide for RuleEngine

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

AGENTS.md and CLAUDE.md files must always be identical

## Project Summary

**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.

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

## Build & Test Commands

```bash
forge build # Compile all contracts
forge test # Run all tests
forge test -vvv # Verbose test output
forge test --match-contract <Name> --match-test <fn> # Run specific test
forge coverage # Code coverage
forge coverage --no-match-coverage "(script|mocks|test)" --report lcov # Production coverage
forge fmt # Format code
```

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

## Import Remappings

| Alias | Path |
|-------|------|
| `CMTAT/` | `lib/CMTAT/contracts/` |
| `CMTATv3.0.0/` | `lib/CMTATv3.0.0/contracts/` |
| `@openzeppelin/contracts/` | `lib/openzeppelin-contracts/contracts` |

Use `@openzeppelin/contracts/` for OpenZeppelin imports, `CMTAT/` for CMTAT imports, `src/` for local imports.

## Architecture

### Three Deployable Contracts

```
RuleEngine — RBAC via AccessControl (multi-operator)
RuleEngineOwnable — ERC-173 Ownable (single-owner)
RuleEngineOwnable2Step — ERC-173 Ownable2Step (single-owner, two-step handover)
```

All three share their core logic through `RuleEngineBase` directly or via `RuleEngineOwnableShared`.

### Inheritance Hierarchy

```
RuleEngineBase (abstract)
├── VersionModule → version() returns "3.0.0"
├── RulesManagementModule → add/remove/set/clear rules
│ ├── AccessControl (OZ)
│ └── RulesManagementModuleInvariantStorage → errors, events, roles
├── ERC3643ComplianceModule → bind/unbind tokens
│ └── IERC3643Compliance
├── RuleEngineInvariantStorage → errors
└── IRuleEngineERC1404 → CMTAT interface

RuleEngine
├── ERC2771ModuleStandalone → gasless support
└── RuleEngineBase

RuleEngineOwnable
├── ERC2771ModuleStandalone → gasless support
├── RuleEngineOwnableShared
│ └── RuleEngineBase
└── Ownable (OZ) → ERC-173

RuleEngineOwnable2Step
├── ERC2771ModuleStandalone → gasless support
├── RuleEngineOwnableShared
│ └── RuleEngineBase
└── Ownable2Step (OZ) → ERC-173
```

### Access Control Pattern

Modules define **virtual internal hooks** for access control. Concrete contracts override them:

```solidity
// In RulesManagementModule (abstract):
function _onlyRulesManager() internal virtual;

// In ERC3643ComplianceModule (abstract):
function _onlyComplianceManager() internal virtual;

// RuleEngine overrides with RBAC:
function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {}
function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}

// RuleEngineOwnable overrides with Ownable:
function _onlyRulesManager() internal virtual override onlyOwner {}
function _onlyComplianceManager() internal virtual override onlyOwner {}
```

**When adding a new protected function**, follow this pattern: define a virtual hook in the module, then override it in `RuleEngine`, `RuleEngineOwnable`, and `RuleEngineOwnable2Step`.

### `_checkRule` Override Chain

Rule validation uses a two-layer override:

1. **`RulesManagementModule._checkRule()`** — checks zero address + duplicates
2. **`RuleEngineBase._checkRule()`** — calls `super._checkRule()` then validates ERC-165 interface

```solidity
// RulesManagementModule (generic checks):
function _checkRule(address rule_) internal view virtual {
if (rule_ == address(0x0)) revert ...ZeroNotAllowed();
if (_rules.contains(rule_)) revert ...AlreadyExists();
}

// RuleEngineBase (adds ERC-165 check):
function _checkRule(address rule_) internal view virtual override {
super._checkRule(rule_);
if (!ERC165Checker.supportsInterface(rule_, RuleInterfaceId.IRULE_INTERFACE_ID))
revert RuleEngine_RuleInvalidInterface();
}
```

### Rule Execution Flow

```
Token.transfer() → RuleEngine.transferred(from, to, value)
├── onlyBoundToken modifier (caller must be bound)
└── for each rule in _rules:
rule.transferred(from, to, value) // reverts if disallowed
```

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

### Storage: EnumerableSet

Both rules and bound tokens use `EnumerableSet.AddressSet`:
- `_rules` in `RulesManagementModule` — the set of active rules
- `_boundTokens` in `ERC3643ComplianceModule` — tokens allowed to call `transferred`

This gives O(1) add/remove/contains and iterable storage.

## Key Interfaces

| Interface | Purpose | Where Defined |
|-----------|---------|---------------|
| `IRule` | What every rule must implement (extends `IRuleEngineERC1404`) | `src/interfaces/IRule.sol` |
| `IRulesManagementModule` | Rule CRUD operations | `src/interfaces/IRulesManagementModule.sol` |
| `IERC3643Compliance` | Token binding + compliance hooks | `src/interfaces/IERC3643Compliance.sol` |
| `IRuleEngine` | Full CMTAT integration interface | `lib/CMTAT/contracts/interfaces/engine/IRuleEngine.sol` |

**ERC-165 interface IDs:**
- `IRule`: `0x2497d6cb` (defined in `src/modules/library/RuleInterfaceId.sol`)
- `IRuleEngine`: from `CMTAT/library/RuleEngineInterfaceId.sol`
- `IERC1404Extend`: from `CMTAT/library/ERC1404ExtendInterfaceId.sol`
- `ERC-173`: `0x7f5828d0` (hardcoded in `RuleEngineOwnable`)

## Invariant Storage Pattern

Errors, events, and role constants are centralized in "invariant storage" abstract contracts:

| Contract | Contains |
|----------|----------|
| `RuleEngineInvariantStorage` | `RuleEngine_AdminWithAddressZeroNotAllowed`, `RuleEngine_RuleInvalidInterface` |
| `RulesManagementModuleInvariantStorage` | Rule errors, `AddRule`/`RemoveRule`/`ClearRules` events, `RULES_MANAGEMENT_ROLE` |

**Convention:** Error names follow `Contract_Module_ErrorName` pattern. Test contracts inherit these to access `.selector` for `vm.expectRevert`.

## Project Structure

```
src/
├── deployment/
│ ├── RuleEngine.sol # RBAC variant (deploy this)
│ ├── RuleEngineOwnable.sol # Ownable variant (deploy this)
│ └── RuleEngineOwnable2Step.sol # Ownable2Step variant (deploy this)
├── RuleEngineBase.sol # Abstract core logic (do not deploy)
├── RuleEngineOwnableShared.sol # Shared logic for ownable variants
├── interfaces/ # IRule, IRulesManagementModule, IERC3643Compliance
├── modules/ # VersionModule, RulesManagementModule, ERC3643ComplianceModule, ERC2771ModuleStandalone
│ └── library/ # InvariantStorage contracts, RuleInterfaceId
└── mocks/ # Test-only/reference contracts

test/
├── HelperContract.sol # Base helper for RuleEngine tests
├── HelperContractOwnable.sol # Base helper for RuleEngineOwnable tests
├── HelperContractOwnable2Step.sol # Base helper for RuleEngineOwnable2Step tests
├── utils/ # CMTAT deployment helpers
├── RuleEngine/ # Tests for RuleEngine (RBAC)
├── RuleEngineOwnable/ # Tests for RuleEngineOwnable
├── RuleEngineOwnable2Step/ # Tests for RuleEngineOwnable2Step
└── RuleWhitelist/ # Tests for the whitelist mock rule

script/ # Foundry example/deployment scripts
```

## Test Conventions

For detailed test conventions, templates, helper contracts, test addresses, naming patterns, and the base test pattern, see the **testing skill**: `.claude/skills/testing/SKILL.md`.

Key points:
- Tests for `RuleEngine` go in `test/RuleEngine/`, tests for `RuleEngineOwnable` go in `test/RuleEngineOwnable/`
- Tests for `RuleEngineOwnable2Step` go in `test/RuleEngineOwnable2Step/`
- Use `HelperContract` for RBAC tests, `HelperContractOwnable` for Ownable tests
- Use `HelperContractOwnable2Step` for `RuleEngineOwnable2Step` tests
- Always use specific error selectors in `vm.expectRevert()`
- When adding a feature to `RuleEngineBase`, add tests for **all deployable variants**

## RBAC Roles (RuleEngine only)

| Role | Identifier | Purpose |
|------|-----------|---------|
| `DEFAULT_ADMIN_ROLE` | `0x00...00` | Has all roles (via `hasRole` override) |
| `RULES_MANAGEMENT_ROLE` | `keccak256("RULES_MANAGEMENT_ROLE")` | Add/remove/set/clear rules |
| `COMPLIANCE_MANAGER_ROLE` | `keccak256("COMPLIANCE_MANAGER_ROLE")` | Bind/unbind tokens |

## Key Invariants

1. **Only bound tokens** can call `transferred()`, `created()`, `destroyed()`
2. **Rules are validated via ERC-165** before being added — they must support `IRULE_INTERFACE_ID`
3. **No duplicate rules** — `EnumerableSet` prevents this
4. **No zero-address rules** — checked in `_checkRule`
5. **Admin has all roles** in `RuleEngine` (the `hasRole` override)
6. **Forwarder is immutable** — set at construction, cannot be changed
7. **Rule contracts in `src/mocks/` are reference implementations** — they are useful for testing and examples, not as production rule contracts. Production rules live in a [separate repository](https://github.com/CMTA/Rules).

## Solidity Style

- Follow the [Solidity style guide](https://docs.soliditylang.org/en/latest/style-guide.html)
- NatSpec comments on all public/external functions
- Function ordering: constructor, receive, fallback, external, public, internal, private (view/pure last within each group)
- Function declaration order: visibility, mutability, virtual, override, custom modifiers
- Section headers: `/* ============ SECTION ============ */`
- Run `forge fmt` before committing

## Common Tasks

### Adding a new module
1. Create the module in `src/modules/`
2. Create an invariant storage contract in `src/modules/library/` for errors/events
3. Add a virtual access control hook (e.g., `_onlyNewManager()`)
4. Have `RuleEngineBase` inherit the module
5. Override the hook in both `RuleEngine` and `RuleEngineOwnable`
6. Add tests in `test/RuleEngine/`, `test/RuleEngineOwnable/`, and `test/RuleEngineOwnable2Step/`

### Adding a new rule (mock)
1. Create the rule in `src/mocks/rules/`
2. Implement `IRule` (which extends `IRuleEngineERC1404`)
3. Implement ERC-165 with `IRULE_INTERFACE_ID`
4. Add tests using the existing `HelperContract` base

### Modifying access control
1. Update the virtual hook in the relevant module
2. Update overrides in **both** `RuleEngine.sol` and `RuleEngineOwnable.sol`
3. Update tests in all affected test directories
48 changes: 46 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,51 @@ forge lint
- Update surya doc by running the 3 scripts in [./doc/script](./doc/script)
- Update changelog

## v3.0.0-rc1 - 2026-02-16


### v3.0.0-rc2 - 2026-04-13

### Dependencies

- Update CMTAT submodule to [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0).
- Update OpenZeppelin Contracts and OpenZeppelin Contracts Upgradeable submodules to [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.6.1).
- Set Solidity version to [0.8.34](https://docs.soliditylang.org/en/v0.8.34/) in `hardhat.config.js` and `foundry.toml`.

### Fixed

- `RuleEngineOwnable.supportsInterface` incorrectly advertised `IAccessControl` via the inherited `AccessControl.supportsInterface` fallback. Replaced with an explicit whitelist; `supportsInterface(IAccessControl)` now returns `false` as expected (Nethermind AuditAgent finding 2).
- Remove `AccessControl` inheritance from `RulesManagementModule`; RBAC responsibilities are now explicitly held by `RuleEngine`, while the module remains access-control agnostic.
- Switch `RuleEngine` RBAC base from OpenZeppelin `AccessControl` to `AccessControlEnumerable` while keeping the custom "default admin has all roles" behavior.

### Added

- Advertise ERC-3643 compliance interface ID (`0x3144991c`) and IERC7551Compliance subset interface ID (`0x7157797f`) in `supportsInterface` for both `RuleEngine` and `RuleEngineOwnable` (Nethermind AuditAgent finding 6).
- Move deployable contracts to `src/deployment/` and rename RBAC deployable contract `RuleEngine` to `RuleEngine`.
- `RuleEngine.supportsInterface` now advertises `IAccessControlEnumerable`.

### Security

- Add NatSpec and README warnings on `bindToken` / `unbindToken`: in a multi-tenant setup (multiple tokens sharing one engine), all bound tokens must be equally trusted and governed together; ERC-3643 callbacks do not carry the token address to rules (Nethermind AuditAgent finding 1).
- Add NatSpec warnings on `addRule`, `setRules`, and `_transferred`: rule contracts must not be granted `RULES_MANAGEMENT_ROLE` or admin privileges (Nethermind AuditAgent finding 5).
- Add NatSpec warnings on `addRule`, `setRules`, and `_transferred`: no on-chain maximum rule count is enforced; operators are responsible for sizing the rule set for the target chain gas limits (Nethermind AuditAgent finding 3).
- Add restriction-code uniqueness convention to `IRule.canReturnTransferRestrictionCode` and `_messageForTransferRestriction`: codes must be unique across rules, or rules sharing a code must return the same message (Nethermind AuditAgent finding 4).
- Add NatSpec on `setRules` documenting the empty-array rejection by design and referring to `clearRules` for explicit removal (Nethermind AuditAgent finding 7).

### Testing

- Add `testDoesNotSupportIAccessControlInterface` to `RuleEngineOwnableCoverage` asserting `IAccessControl` is not advertised.
- Add ERC-3643 and IERC7551Compliance `supportsInterface` coverage tests to both `RuleEngineCoverage` and `RuleEngineOwnableCoverage`.
- Add mock interfaces `src/mocks/ICompliance.sol` and `src/mocks/IERC7551ComplianceSubset.sol` used by coverage tests.
- Extend `RuleEngineDeployment` interface coverage to assert support for `IAccessControlEnumerable`.

### Documentation

- Add Nethermind AuditAgent scan #1 report and remediation assessment (`doc/security/audits/tools/nethermind-audit-agent/`).
- Update README Security section with Nethermind AuditAgent findings summary table.

### v3.0.0-rc1 - 2026-02-16

Commit: `f3e27c190635e91a64215276f4757d65eb2d2b2c`

### Added

Expand Down Expand Up @@ -89,7 +133,7 @@ forge lint

## v3.0.0-rc0 - 2025-08-15

Commit: f3283c3b8a99089c3c6f674150831003a6bd2927
Commit: `f3283c3b8a99089c3c6f674150831003a6bd2927`

- Rule contracts, requires to perform compliance check, have now their own dedicated [GitHub repository](https://github.com/CMTA/Rules). It means that these contract will be developed and audited separately from the `RuleEngine`. This provides more flexibility and makes it easier to manage audits.
- There is now only one type of rule (read-write rules). Before that:
Expand Down
Loading