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
RuleEngine is a Solidity smart contract system that enforces transfer restrictions for CMTAT and ERC-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
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 codeDependencies are git submodules. Initialize with forge install, update with forge update.
CMTAT submodule also needs cd lib/CMTAT && npm install for its OpenZeppelin deps.
| 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.
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.
RuleEngineBase (abstract)
├── VersionModule → version() returns "3.0.0"
├── RulesManagementModule → add/remove/set/clear rules, maxRules cap
│ ├── AccessControl (OZ)
│ └── RulesManagementModuleInvariantStorage → errors, events, roles
├── ERC3643ComplianceExtendedModule → bind/unbind tokens (extended API)
│ └── ERC3643ComplianceModule → core ERC-3643 compliance
│ ├── IERC3643Compliance
│ └── ERC3643ComplianceModuleInvariantStorage → errors
├── 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
Modules define virtual internal hooks for access control. Concrete contracts override them:
// In RulesManagementModule (abstract):
function _onlyRulesManager() internal virtual;
function _onlyRulesLimitManager() internal virtual; // guards setMaxRules
// In ERC3643ComplianceModule (abstract):
function _onlyComplianceManager() internal virtual;
// RuleEngine overrides with RBAC:
function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {}
function _onlyRulesLimitManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
// RuleEngineOwnable overrides with Ownable:
function _onlyRulesManager() internal virtual override onlyOwner {}
function _onlyRulesLimitManager() 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.
Rule validation uses a two-layer override:
RulesManagementModule._checkRule()— checks zero address + duplicatesRuleEngineBase._checkRule()— callsRulesManagementModule._checkRule()then validates ERC-165 interface
// 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 {
RulesManagementModule._checkRule(rule_);
if (!ERC165Checker.supportsInterface(rule_, RuleInterfaceId.IRULE_INTERFACE_ID))
revert RuleEngine_RuleInvalidInterface();
}Token operation → RuleEngine.transferred(spender, from, to, value) ← CMTAT v3.3.0+ primary path
├── onlyBoundToken modifier (caller must be bound)
└── for each rule in _rules:
rule.transferred(spender, from, to, value) // reverts if disallowed
RuleEngine.transferred(from, to, value) ← 3-arg fallback (spender == address(0))
├── onlyBoundToken modifier
└── for each rule in _rules:
rule.transferred(from, to, value)
RuleEngine.created(to, value) ← ERC-3643 mint entry point
├── onlyBoundToken modifier
└── calls _transferred(address(0), to, value)
RuleEngine.destroyed(from, value) ← ERC-3643 burn entry point
├── onlyBoundToken modifier
└── calls _transferred(from, address(0), value)
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.
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.
View path: detectTransferRestriction() iterates rules, returns first non-zero code.
Both rules and bound tokens use EnumerableSet.AddressSet:
_rulesinRulesManagementModule— the set of active rules_boundTokensinERC3643ComplianceModule— tokens allowed to calltransferred
This gives O(1) add/remove/contains and iterable storage.
| 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 insrc/modules/library/RuleInterfaceId.sol)IRuleEngine: fromCMTAT/library/RuleEngineInterfaceId.solIERC1404Extend: fromCMTAT/library/ERC1404ExtendInterfaceId.solERC-173:0x7f5828d0(hardcoded inRuleEngineOwnable)
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.
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
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
RuleEnginego intest/RuleEngine/, tests forRuleEngineOwnablego intest/RuleEngineOwnable/ - Tests for
RuleEngineOwnable2Stepgo intest/RuleEngineOwnable2Step/ - Use
HelperContractfor RBAC tests,HelperContractOwnablefor Ownable tests - Use
HelperContractOwnable2StepforRuleEngineOwnable2Steptests - Always use specific error selectors in
vm.expectRevert() - When adding a feature to
RuleEngineBase, add tests for all deployable variants
| 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 |
- Only bound tokens can call
transferred(),created(),destroyed() - Rules are validated via ERC-165 before being added — they must support
IRULE_INTERFACE_ID - No duplicate rules —
EnumerableSetprevents this - No zero-address rules — checked in
_checkRule - Admin has all roles in
RuleEngine(thehasRoleoverride) - Forwarder is immutable — set at construction, cannot be changed
- 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.
- Follow the Solidity style guide
- 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
- In
src/, avoidsupercalls and prefer explicit parent-contract calls (e.g.,AccessControl.grantRole(...)) for readability and deterministic inheritance behavior. - Section headers:
/* ============ SECTION ============ */ - Run
forge fmtbefore committing
- Create the module in
src/modules/ - Create an invariant storage contract in
src/modules/library/for errors/events - Add a virtual access control hook (e.g.,
_onlyNewManager()) - Have
RuleEngineBaseinherit the module - Override the hook in both
RuleEngineandRuleEngineOwnable - Add tests in
test/RuleEngine/,test/RuleEngineOwnable/, andtest/RuleEngineOwnable2Step/
- Create the rule in
src/mocks/rules/ - Implement
IRule(which extendsIRuleEngineERC1404) - Implement ERC-165 with
IRULE_INTERFACE_ID - Add tests using the existing
HelperContractbase
- Update the virtual hook in the relevant module
- Update overrides in both
RuleEngine.solandRuleEngineOwnable.sol - Update tests in all affected test directories