Skip to content
Open

Dev #60

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
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
244 changes: 244 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
# 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.33)
- **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 |
|-------|------|
| `OZ/` | `lib/openzeppelin-contracts/contracts` |
| `CMTAT/` | `lib/CMTAT/contracts/` |
| `CMTATv3.0.0/` | `lib/CMTATv3.0.0/contracts/` |
| `@openzeppelin/contracts/` | `lib/openzeppelin-contracts/contracts` |

Use `OZ/` for OpenZeppelin imports, `CMTAT/` for CMTAT imports, `src/` for local imports.

## Architecture

### Two Deployable Contracts

```
RuleEngine — RBAC via AccessControl (multi-operator)
RuleEngineOwnable — ERC-173 Ownable (single-owner)
```

Both share 100% of their core logic through `RuleEngineBase`.

### 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
├── RuleEngineBase
└── Ownable (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 both `RuleEngine` and `RuleEngineOwnable`.

### `_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/
├── RuleEngine.sol # RBAC variant (deploy this)
├── RuleEngineOwnable.sol # Ownable variant (deploy this)
├── RuleEngineBase.sol # Abstract core logic (do not deploy)
├── interfaces/ # IRule, IRulesManagementModule, IERC3643Compliance
├── modules/ # VersionModule, RulesManagementModule, ERC3643ComplianceModule, ERC2771ModuleStandalone
│ └── library/ # InvariantStorage contracts, RuleInterfaceId
└── mocks/ # Test-only contracts (RuleWhitelist, RuleConditionalTransferLight, etc.)

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

script/ # Foundry 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/`
- Use `HelperContract` for RBAC tests, `HelperContractOwnable` for Ownable tests
- Always use specific error selectors in `vm.expectRevert()`
- When adding a feature to `RuleEngineBase`, add tests for **both** 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 are in `src/mocks/`** — they are reference implementations for testing, not production rules. 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 both `test/RuleEngine/` and `test/RuleEngineOwnable/`

### 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 **both** test directories
45 changes: 43 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,48 @@ 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-03-17

### Dependencies

- Update CMTAT submodule to [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0).
- Update OpenZeppelin Contracts (submodule) to [v5.6.0](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.6.0).
- 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.

### 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`.

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

### 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 +130,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
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# CLAUDE.md — AI Agent Guide for RuleEngine
# 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.
Expand Down
Loading
Loading