diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e314250 --- /dev/null +++ b/AGENTS.md @@ -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 --match-test # 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a4f705..f6c8c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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: diff --git a/CLAUDE.md b/CLAUDE.md index 63a96b3..e314250 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/README.md b/README.md index a75560c..2b9f870 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Both contracts share the same core functionality through `RuleEngineBase` and su - ERC-2771 meta-transactions (gasless) - Multiple token bindings +> **Warning (shared engine across multiple tokens):** A "multi-tenant" setup here means one RuleEngine instance is shared by several token contracts (all bound through `bindToken`). In this setup, tokens must be equally trusted and governed together. ERC-3643 callbacks (`transferred`, `created`, `destroyed`) do not pass the token address to rules, so stateful/accounting rules are not safe for mutually untrusted tokens sharing the same engine. + [TOC] ## Motivation @@ -64,7 +66,9 @@ This diagram illustrates how a transfer with a CMTAT or ERC-3643 token with a Ru 2. The transfer function inside the token calls the ERC-3643 function `transferred` from the RuleEngine with the following parameters inside: `from, to, value`. 3. The Rule Engine calls each rule separately. If the transfer is not authorized by the rule, the rule must directly revert (no return value). -> **Warning:** The RuleEngine iterates over all configured rules on every transfer (and on every call to `detectTransferRestriction`, `canTransfer`, etc.). Adding a large number of rules increases gas consumption for each transfer and may eventually exceed the block gas limit, effectively preventing any transfer from succeeding. Administrators should keep the rule set small and be mindful that a misconfigured or gas-heavy rule can also impact all transfers. +> **Warning:** The RuleEngine iterates over all configured rules on every transfer (and on every call to `detectTransferRestriction`, `canTransfer`, etc.). Adding a large number of rules increases gas consumption for each transfer and may eventually exceed the block gas limit, effectively preventing any transfer from succeeding. There is no hard on-chain maximum rule count; administrators are responsible for sizing the rule set for their target blockchain and should keep it small. A misconfigured or gas-heavy rule can also impact all transfers. + +> **Warning (restriction code conventions):** Rule implementations should use unique ERC-1404 restriction codes across the rule set. If several rules intentionally share the same restriction code, they should return the exact same `messageForTransferRestriction` text for that code to avoid inconsistent operator/user feedback. ### How to set it @@ -72,7 +76,7 @@ This diagram illustrates how a transfer with a CMTAT or ERC-3643 token with a Ru | RuleEngine version | Compatible Versions | | ------------------------------------------------------------ | ------------------------------------------------------------ | -| **v3.0.0-rc1** | CMTAT ≥ v3.0.0
CMTAT target version: [v3.2.0-rc2](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0-rc2) | +| **v3.0.0-rc1** | CMTAT ≥ v3.0.0
CMTAT target version: [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0) | | **v3.0.0-rc0** | CMTAT ≥ v3.0.0
| | **[v1.0.2.1](https://github.com/CMTA/RuleEngine/releases/tag/v1.0.2.1)** | CMTAT v2.3.0 (audited) | @@ -200,6 +204,8 @@ function transferred(address from, address to, uint256 value) external; ``` +> Note: `IERC7551Compliance` comes from `draft-IERC7551` (not final) and, in this project, is used as a subset compliance interface focused on `canTransferFrom`. + ### ERC-3643 @@ -216,10 +222,10 @@ A specific module implements this interface for the RuleEngine: [ERC3643Complian The toolchain includes the following components, where the versions are the latest ones that we tested: -- Foundry (forge-std) [v1.14.0](https://github.com/foundry-rs/forge-std/releases/tag/v1.10.0) -- Solidity [0.8.33](https://docs.soliditylang.org/en/v0.8.33/) -- OpenZeppelin Contracts (submodule) [v5.5.0](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.5.0) -- CMTAT [v3.2.0-rc2](https://github.com/CMTA/CMTAT/releases/tag/v3.0.0-rc7) +- Foundry (forge-std) [v1.14.0](https://github.com/foundry-rs/forge-std/releases/tag/v1.14.0) +- Solidity [0.8.34](https://docs.soliditylang.org/en/v0.8.34/) +- OpenZeppelin Contracts (submodule) [v5.6.0](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.6.0) +- CMTAT [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0) ### Access Control @@ -230,6 +236,7 @@ Two access control mechanisms are available depending on which contract you depl The `RuleEngine` contract uses Role-Based Access Control (RBAC) via OpenZeppelin's `AccessControl`. Each module defines the roles useful to restrict its functions. The contract overrides the OpenZeppelin function `hasRole` to give by default all the roles to the `admin`. +`RulesManagementModule` itself is access-control agnostic; RBAC is wired at the concrete `RuleEngine` level. See also [docs.openzeppelin.com - AccessControl](https://docs.openzeppelin.com/contracts/5.x/api/access#AccessControl) @@ -255,6 +262,8 @@ It is set in the constructor when the contract is deployed. > Note: For `RuleEngineOwnable`, all protected functions are controlled by the single `owner` address instead of roles. +> **Warning (role assignment):** Rule contracts should be treated as trusted logic components, but they should not be granted `RULES_MANAGEMENT_ROLE` (or admin privileges). Keep rule-management roles on dedicated operator/admin accounts only. + | | Defined in | 32 bytes identifier | | ----------------------- | -------------------------------- | ------------------------------------------------------------ | | DEFAULT_ADMIN_ROLE | OpenZeppelin
AccessControl | 0x0000000000000000000000000000000000000000000000000000000000000000 | @@ -495,6 +504,8 @@ Must revert if the transfer is invalid. ![IERC7551ComplianceUML](./doc/schema/vscode-uml/IERC7551ComplianceUML.png) +> Note: ERC-7551 is draft (not final). The `IERC7551Compliance` interface used here is a subset interface exposing the compliance check `canTransferFrom`. + ##### canTransferFrom(address spender, address from, address to, uint256 value) -> bool Checks if `spender` can transfer `value` tokens from `from` to `to` under compliance rules. @@ -1222,19 +1233,44 @@ The final report is available in [ABDK_CMTA_CMTATRuleEngine_v_1_0.pdf](https://g ### Tools +#### Nethermind AuditAgent + +> **Note:** This scan was performed by an AI-powered automated tool, not a formal human-led audit. + +| Version | Report | Assessment | +|---------|--------|------------| +| Scan #1 (Feb 2026) | [audit_agent_report_1_v3.0.0-rc1.pdf](./doc/security/audits/tools/nethermind-audit-agent/audit_agent_report_1_v3.0.0-rc1.pdf) | [feedback.md](./doc/security/audits/tools/nethermind-audit-agent/audit_agent_report_1_v3.0.0-rc1-feedback.md) | + +7 findings — 0 High · 1 Medium · 1 Low · 4 Info · 1 Best Practices + +| # | Severity | Finding | Status | +|---|----------|---------|--------| +| 1 | Medium | Cross-token rule state pollution in multi-tenant deployments | NatSpec + README warnings. Interface fix deferred (requires CMTAT coordination). | +| 2 | Low | `RuleEngineOwnable` misreports `IAccessControl` via ERC-165 | Fixed: explicit interface whitelist + negative test added. | +| 3 | Info | Unbounded rules loop — potential permanent DoS | NatSpec + README operator warnings (no hard cap by design). | +| 4 | Info | Restriction code and message can come from different rules | Convention documented in NatSpec and README (no logic change by design). | +| 5 | Info | Re-entrant rule can modify rule set during `transferred()` | NatSpec + README warning — rules must not hold `RULES_MANAGEMENT_ROLE`. | +| 6 | Info | Missing ERC-3643 and IERC7551Compliance interface IDs | Fixed: both IDs added to `supportsInterface` in both contracts, with tests. | +| 7 | Best Practices | `setRules` does not allow an empty array | NatSpec clarification added (behavior unchanged by design). | + #### Slither Here is the list of report performed with [Slither](https://github.com/crytic/slither) -| Version | File | -| ------- | ------------------------------------------------------------ | -| latest | [slither-report.md](./doc/security/audits/tools/slither-report.md) | +| Version | Report | Assessment | +| ------- | ------ | ---------- | +| v3.0.0-rc2 | [slither-report.md](./doc/security/audits/tools/slither-report.md) | [slither-report-feedback.md](./doc/security/audits/tools/slither-report-feedback.md) | ```bash slither . --checklist --filter-paths "openzeppelin-contracts|test|CMTAT|forge-std|mocks" > slither-report.md ``` +2 finding categories — 0 High · 0 Medium · 10 Low · 2 Informational +| ID | Detector | Impact | Instances | Assessment | +|----|----------|--------|-----------|------------| +| 0–9 | `calls-loop` | Low | 10 | Accepted by design — fan-out to rule contracts is the core architecture | +| 10–11 | `unindexed-event-address` | Informational | 2 | Accepted — adding `indexed` to `TokenBound`/`TokenUnbound` is interface-breaking | #### Aderyn @@ -1244,9 +1280,23 @@ Here is the list of report performed with [Aderyn](https://github.com/Cyfrin/ade aderyn -x mocks --output aderyn-report.md ``` -| Version | File | -| ------- | ------------------------------------------------------------ | -| latest | [aderyn-report.md](./doc/security/audits/tools/aderyn-report.md) | +| Version | Report | Assessment | +| ------- | ------ | ---------- | +| v3.0.0-rc2 | [aderyn-report.md](./doc/security/audits/tools/aderyn-report.md) | [aderyn-report-feedback.md](./doc/security/audits/tools/aderyn-report-feedback.md) | + +Report scope: 14 Solidity files, 425 nSLOC. + +0 High · 7 Low + +| ID | Finding | Instances | Assessment | +|----|---------|-----------|------------| +| L-1 | Centralization Risk | 6 | Accepted by design — privileged compliance tool | +| L-2 | Unspecific Solidity Pragma | 12 | Accepted by design — intentional for library reusability | +| L-3 | PUSH0 Opcode | 14 | Not applicable — project targets Prague EVM | +| L-4 | Empty Block | 4 | Accepted by design — access-control hook pattern | +| L-5 | Loop Contains `require`/`revert` | 1 | Accepted by design — `setRules` is an atomic batch operation | +| L-6 | Costly Operations Inside Loop | 1 | Accepted — unavoidable `SSTORE` in `setRules` | +| L-7 | Unchecked Return | 1 | Accepted — `_grantRole` return is irrelevant in constructor | ## Documentation @@ -1268,12 +1318,12 @@ See also [Taurus - Token Transfer Management: How to Apply Restrictions with CMT Here are the settings for [Hardhat](https://hardhat.org) and [Foundry](https://getfoundry.sh). - `hardhat.config.js` - - Solidity [v0.8.33](https://docs.soliditylang.org/en/v0.8.33/) + - Solidity [v0.8.34](https://docs.soliditylang.org/en/v0.8.34/) - EVM version: Prague (Pectra upgrade) - Optimizer: true, 200 runs - `foundry.toml` - - Solidity [v0.8.33](https://docs.soliditylang.org/en/v0.8.33/) + - Solidity [v0.8.34](https://docs.soliditylang.org/en/v0.8.34/) - EVM version: Prague (Pectra upgrade) - Optimizer: true, 200 runs @@ -1312,8 +1362,8 @@ The official documentation is available in the Foundry [website](https://book.ge forge build # Build specific contract -forge build --contracts src/RuleEngine.sol -forge build --contracts src/RuleEngineOwnable.sol +forge build --contracts src/deployment/RuleEngine.sol +forge build --contracts src/deployment/RuleEngineOwnable.sol ``` ### Contract size @@ -1321,11 +1371,14 @@ forge build --contracts src/RuleEngineOwnable.sol forge build --sizes ``` -Both `RuleEngine` and `RuleEngineOwnable` have similar bytecode sizes since they share the same base functionality. The `RuleEngineOwnable` contract is slightly smaller as `Ownable` has less overhead than `AccessControl`. - +Latest output (`2026-03-18`) for the main RuleEngine contracts: +| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) | +|----------|------------------:|------------------:|--------------------:|---------------------:| +| RuleEngine | 6,756 | 7,805 | 17,820 | 41,347 | +| RuleEngineOwnable | 6,170 | 6,833 | 18,406 | 42,319 | -![contract-size](./doc/compilation/contract-size.png) +Both `RuleEngine` and `RuleEngineOwnable` remain well below the EIP-170 runtime limit. `RuleEngineOwnable` is slightly smaller because `Ownable` has less overhead than `AccessControl`. ### Testing diff --git a/doc/TOOLCHAIN.md b/doc/TOOLCHAIN.md index 90f8204..e36c8a4 100644 --- a/doc/TOOLCHAIN.md +++ b/doc/TOOLCHAIN.md @@ -42,12 +42,20 @@ Utility tool for smart contract systems. **[OpenZeppelin Contracts](https://github.com/OpenZeppelin/openzeppelin-contracts)** OpenZeppelin Contracts -The version of the library used is available in the [READEME](../README.md) +The version of the library used is available in the [README](../README.md) Warning: - Submodules are not automatically updated when the host repository is updated. - Only update the module to a specific version, not an intermediary commit. +## Tested versions + +The current tested baseline is: + +- Solidity: [0.8.34](https://docs.soliditylang.org/en/v0.8.34/) +- OpenZeppelin Contracts (submodule): [v5.6.0](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.6.0) +- CMTAT: [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0) + ## Generate documentation @@ -122,10 +130,14 @@ npm run-script surya:report >Slither is a Solidity static analysis framework written in Python3 ```bash -slither . --checklist --filter-paths "openzeppelin-contracts|test|CMTAT|forge-std" > slither-report.md +slither . --checklist --filter-paths "openzeppelin-contracts|test|mocks|CMTAT|forge-std" > slither-report.md ``` +### [Aderyn](https://github.com/Cyfrin/aderyn) +```bash +aderyn -x mocks --output aderyn-report.md +``` ## Code style guidelines diff --git a/doc/compilation/contract-size.png b/doc/compilation/contract-size.png deleted file mode 100644 index 01f8e46..0000000 Binary files a/doc/compilation/contract-size.png and /dev/null differ diff --git a/doc/coverage/code-coverage.png b/doc/coverage/code-coverage.png index 0c33ce6..0686ade 100644 Binary files a/doc/coverage/code-coverage.png and b/doc/coverage/code-coverage.png differ diff --git a/doc/coverage/coverage/index-sort-b.html b/doc/coverage/coverage/index-sort-b.html index b996809..37cda05 100644 --- a/doc/coverage/coverage/index-sort-b.html +++ b/doc/coverage/coverage/index-sort-b.html @@ -31,13 +31,13 @@ lcov.info Lines: - 158 - 158 + 163 + 163 100.0 % Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 53 @@ -99,11 +99,23 @@
100.0%
100.0 % - 79 / 79 + 42 / 42 + 100.0 % + 13 / 13 + 100.0 % + 4 / 4 + + + src/deployment + +
100.0%
+ + 100.0 % + 42 / 42 100.0 % - 28 / 28 + 15 / 15 100.0 % - 9 / 9 + 5 / 5 diff --git a/doc/coverage/coverage/index-sort-f.html b/doc/coverage/coverage/index-sort-f.html index bed08a3..4e0950c 100644 --- a/doc/coverage/coverage/index-sort-f.html +++ b/doc/coverage/coverage/index-sort-f.html @@ -31,13 +31,13 @@ lcov.info Lines: - 158 - 158 + 163 + 163 100.0 % Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 53 @@ -82,28 +82,40 @@ Branches Sort by branch coverage - src/modules + src
100.0%
100.0 % - 79 / 79 + 42 / 42 100.0 % - 25 / 25 - 81.5 % - 22 / 27 + 13 / 13 + 100.0 % + 4 / 4 - src + src/deployment
100.0%
100.0 % - 79 / 79 + 42 / 42 100.0 % - 28 / 28 + 15 / 15 100.0 % - 9 / 9 + 5 / 5 + + + src/modules + +
100.0%
+ + 100.0 % + 79 / 79 + 100.0 % + 25 / 25 + 81.5 % + 22 / 27 diff --git a/doc/coverage/coverage/index-sort-l.html b/doc/coverage/coverage/index-sort-l.html index a9cbdac..130e77b 100644 --- a/doc/coverage/coverage/index-sort-l.html +++ b/doc/coverage/coverage/index-sort-l.html @@ -31,13 +31,13 @@ lcov.info Lines: - 158 - 158 + 163 + 163 100.0 % Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 53 @@ -87,11 +87,23 @@
100.0%
100.0 % - 79 / 79 + 42 / 42 + 100.0 % + 13 / 13 + 100.0 % + 4 / 4 + + + src/deployment + +
100.0%
+ + 100.0 % + 42 / 42 100.0 % - 28 / 28 + 15 / 15 100.0 % - 9 / 9 + 5 / 5 src/modules diff --git a/doc/coverage/coverage/index.html b/doc/coverage/coverage/index.html index 723208f..9e873ee 100644 --- a/doc/coverage/coverage/index.html +++ b/doc/coverage/coverage/index.html @@ -31,13 +31,13 @@ lcov.info Lines: - 158 - 158 + 163 + 163 100.0 % Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 53 @@ -87,11 +87,23 @@
100.0%
100.0 % - 79 / 79 + 42 / 42 + 100.0 % + 13 / 13 + 100.0 % + 4 / 4 + + + src/deployment + +
100.0%
+ + 100.0 % + 42 / 42 100.0 % - 28 / 28 + 15 / 15 100.0 % - 9 / 9 + 5 / 5 src/modules diff --git a/doc/coverage/coverage/src/RuleEngineBase.sol.func-sort-c.html b/doc/coverage/coverage/src/RuleEngineBase.sol.func-sort-c.html index 47c8feb..3814568 100644 --- a/doc/coverage/coverage/src/RuleEngineBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/RuleEngineBase.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 13 @@ -85,15 +85,15 @@ 18 - RuleEngineBase._messageForTransferRestriction - 19 + RuleEngineBase.transferred.1 + 18 - RuleEngineBase.messageForTransferRestriction + RuleEngineBase._messageForTransferRestriction 19 - RuleEngineBase.transferred.1 + RuleEngineBase.messageForTransferRestriction 19 @@ -117,8 +117,8 @@ 59 - RuleEngineBase._checkRule - 215 + RuleEngineBase._checkRule + 217
diff --git a/doc/coverage/coverage/src/RuleEngineBase.sol.func.html b/doc/coverage/coverage/src/RuleEngineBase.sol.func.html index 7790ab3..95e4dbb 100644 --- a/doc/coverage/coverage/src/RuleEngineBase.sol.func.html +++ b/doc/coverage/coverage/src/RuleEngineBase.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 13 @@ -69,8 +69,8 @@ Hit count Sort by hit count - RuleEngineBase._checkRule - 215 + RuleEngineBase._checkRule + 217 RuleEngineBase._detectTransferRestriction @@ -81,7 +81,7 @@ 39 - RuleEngineBase._messageForTransferRestriction + RuleEngineBase._messageForTransferRestriction 19 @@ -118,7 +118,7 @@ RuleEngineBase.transferred.1 - 19 + 18
diff --git a/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html b/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html index e2afa7c..9a123d7 100644 --- a/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html +++ b/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 13 @@ -118,13 +118,13 @@ 47 : : /** 48 : : * @inheritdoc IERC3643IComplianceContract 49 : : */ - 50 : 19 : function transferred(address from, address to, uint256 value) + 50 : 18 : function transferred(address from, address to, uint256 value) 51 : : public 52 : : virtual 53 : : override(IERC3643IComplianceContract) 54 : : onlyBoundToken 55 : : { - 56 : 17 : _transferred(from, to, value); + 56 : 16 : _transferred(from, to, value); 57 : : } 58 : : 59 : : /// @inheritdoc IERC3643Compliance @@ -238,27 +238,32 @@ 167 : 10 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); 168 : : } 169 : : - 170 : 19 : function _messageForTransferRestriction(uint8 restrictionCode) internal view virtual returns (string memory) { - 171 : : // - 172 : 19 : uint256 rulesLength = rulesCount(); - 173 : 19 : for (uint256 i = 0; i < rulesLength; ++i) { - 174 [ + ]: 16 : if (IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)) { - 175 : 14 : return IRule(rule(i)).messageForTransferRestriction(restrictionCode); - 176 : : } - 177 : : } - 178 : 5 : return "Unknown restriction code"; - 179 : : } - 180 : : - 181 : : /** - 182 : : * @dev Override to add ERC-165 interface check for the full IRule hierarchy. - 183 : : */ - 184 : 215 : function _checkRule(address rule_) internal view virtual override { - 185 : 215 : super._checkRule(rule_); - 186 [ + ]: 206 : if (!ERC165Checker.supportsInterface(rule_, RuleInterfaceId.IRULE_INTERFACE_ID)) { - 187 : 6 : revert RuleEngine_RuleInvalidInterface(); - 188 : : } - 189 : : } - 190 : : } + 170 : : /** + 171 : : * @dev This function returns the message from the first rule claiming the code. + 172 : : * Rule designers should keep restriction codes unique across rules. + 173 : : * If a code is shared intentionally, all rules using that code should return + 174 : : * the same message to avoid ambiguous operator feedback. + 175 : : */ + 176 : 19 : function _messageForTransferRestriction(uint8 restrictionCode) internal view virtual returns (string memory) { + 177 : 19 : uint256 rulesLength = rulesCount(); + 178 : 19 : for (uint256 i = 0; i < rulesLength; ++i) { + 179 [ + ]: 16 : if (IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)) { + 180 : 14 : return IRule(rule(i)).messageForTransferRestriction(restrictionCode); + 181 : : } + 182 : : } + 183 : 5 : return "Unknown restriction code"; + 184 : : } + 185 : : + 186 : : /** + 187 : : * @dev Override to add ERC-165 interface check for the full IRule hierarchy. + 188 : : */ + 189 : 217 : function _checkRule(address rule_) internal view virtual override { + 190 : 217 : super._checkRule(rule_); + 191 [ + ]: 208 : if (!ERC165Checker.supportsInterface(rule_, RuleInterfaceId.IRULE_INTERFACE_ID)) { + 192 : 6 : revert RuleEngine_RuleInvalidInterface(); + 193 : : } + 194 : : } + 195 : : } diff --git a/doc/coverage/coverage/src/RuleEngine.sol.func-sort-c.html b/doc/coverage/coverage/src/deployment/RuleEngine.sol.func-sort-c.html similarity index 63% rename from doc/coverage/coverage/src/RuleEngine.sol.func-sort-c.html rename to doc/coverage/coverage/src/deployment/RuleEngine.sol.func-sort-c.html index c156b2b..c376ad7 100644 --- a/doc/coverage/coverage/src/RuleEngine.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/deployment/RuleEngine.sol.func-sort-c.html @@ -4,22 +4,22 @@ - LCOV - lcov.info - src/RuleEngine.sol - functions - + LCOV - lcov.info - src/deployment/RuleEngine.sol - functions + - + - +
LCOV - code coverage report
- + @@ -31,13 +31,13 @@ - - + + - + @@ -53,58 +53,58 @@ - +
Current view:top level - src - RuleEngine.sol (source / functions)top level - src/deployment - RuleEngine.sol (source / functions) Hitlcov.info Lines:22222424 100.0 %
Date:2026-02-16 13:49:212026-03-18 19:08:29 Functions: 84 100.0 %
- - + + - + - - + + - + - - + + - - + + - - + + - - + + - - + +

Function Name Sort by function nameHit count Sort by hit countFunction Name Sort by function nameHit count Sort by hit count
RuleEngine._msgDataRuleEngine._msgData 1
RuleEngine.supportsInterface4RuleEngine.supportsInterface6
RuleEngine._onlyComplianceManagerRuleEngine._onlyComplianceManager 15
RuleEngine.constructor121RuleEngine.constructor127
RuleEngine.hasRole129RuleEngine.hasRole135
RuleEngine._onlyRulesManager168RuleEngine._onlyRulesManager170
RuleEngine._msgSender359RuleEngine._msgSender366
RuleEngine._contextSuffixLength360RuleEngine._contextSuffixLength367

- +
Generated by: LCOV version 1.16

diff --git a/doc/coverage/coverage/src/RuleEngine.sol.func.html b/doc/coverage/coverage/src/deployment/RuleEngine.sol.func.html similarity index 63% rename from doc/coverage/coverage/src/RuleEngine.sol.func.html rename to doc/coverage/coverage/src/deployment/RuleEngine.sol.func.html index c4cc7f4..ebb8602 100644 --- a/doc/coverage/coverage/src/RuleEngine.sol.func.html +++ b/doc/coverage/coverage/src/deployment/RuleEngine.sol.func.html @@ -4,22 +4,22 @@ - LCOV - lcov.info - src/RuleEngine.sol - functions - + LCOV - lcov.info - src/deployment/RuleEngine.sol - functions + - + - +
LCOV - code coverage report
- + @@ -31,13 +31,13 @@ - - + + - + @@ -53,58 +53,58 @@ - +
Current view:top level - src - RuleEngine.sol (source / functions)top level - src/deployment - RuleEngine.sol (source / functions) Hitlcov.info Lines:22222424 100.0 %
Date:2026-02-16 13:49:212026-03-18 19:08:29 Functions: 84 100.0 %
- - + + - - + + - + - - + + - + - - + + - - + + - - + + - - + +

Function Name Sort by function nameHit count Sort by hit countFunction Name Sort by function nameHit count Sort by hit count
RuleEngine._contextSuffixLength360RuleEngine._contextSuffixLength367
RuleEngine._msgDataRuleEngine._msgData 1
RuleEngine._msgSender359RuleEngine._msgSender366
RuleEngine._onlyComplianceManagerRuleEngine._onlyComplianceManager 15
RuleEngine._onlyRulesManager168RuleEngine._onlyRulesManager170
RuleEngine.constructor121RuleEngine.constructor127
RuleEngine.hasRole129RuleEngine.hasRole135
RuleEngine.supportsInterface4RuleEngine.supportsInterface6

- +
Generated by: LCOV version 1.16

diff --git a/doc/coverage/coverage/src/RuleEngine.sol.gcov.html b/doc/coverage/coverage/src/deployment/RuleEngine.sol.gcov.html similarity index 78% rename from doc/coverage/coverage/src/RuleEngine.sol.gcov.html rename to doc/coverage/coverage/src/deployment/RuleEngine.sol.gcov.html index b19bca7..63031d1 100644 --- a/doc/coverage/coverage/src/RuleEngine.sol.gcov.html +++ b/doc/coverage/coverage/src/deployment/RuleEngine.sol.gcov.html @@ -4,22 +4,22 @@ - LCOV - lcov.info - src/RuleEngine.sol - + LCOV - lcov.info - src/deployment/RuleEngine.sol + - + - +
LCOV - code coverage report
- + @@ -31,13 +31,13 @@ - - + + - + @@ -53,12 +53,12 @@ - +
Current view:top level - src - RuleEngine.sol (source / functions)top level - src/deployment - RuleEngine.sol (source / functions) Hitlcov.info Lines:22222424 100.0 %
Date:2026-02-16 13:49:212026-03-18 19:08:29 Functions: 84 100.0 %
@@ -81,77 +81,80 @@ 10 : : import {AccessControl} from "OZ/access/AccessControl.sol"; 11 : : import {IERC165} from "OZ/utils/introspection/ERC165.sol"; 12 : : /* ==== Modules === */ - 13 : : import {ERC2771ModuleStandalone, ERC2771Context} from "./modules/ERC2771ModuleStandalone.sol"; + 13 : : import {ERC2771ModuleStandalone, ERC2771Context} from "../modules/ERC2771ModuleStandalone.sol"; 14 : : /* ==== Base contract === */ - 15 : : import {RuleEngineBase} from "./RuleEngineBase.sol"; - 16 : : - 17 : : /** - 18 : : * @title Implementation of a ruleEngine as defined by the CMTAT - 19 : : */ - 20 : : contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase { - 21 : : /** - 22 : : * @param admin Address of the contract (Access Control) - 23 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support - 24 : : */ - 25 : 121 : constructor(address admin, address forwarderIrrevocable, address tokenContract) - 26 : : ERC2771ModuleStandalone(forwarderIrrevocable) - 27 : : { - 28 [ + ]: 121 : if (admin == address(0)) { - 29 : 1 : revert RuleEngine_AdminWithAddressZeroNotAllowed(); - 30 : : } - 31 [ + ]: 120 : if (tokenContract != address(0)) { - 32 : 30 : _bindToken(tokenContract); - 33 : : } - 34 : 120 : _grantRole(DEFAULT_ADMIN_ROLE, admin); - 35 : : } - 36 : : - 37 : : /* ============ ACCESS CONTROL ============ */ - 38 : : /** - 39 : : * @notice Returns `true` if `account` has been granted `role`. - 40 : : * @dev The Default Admin has all roles - 41 : : */ - 42 : 129 : function hasRole(bytes32 role, address account) public view virtual override(AccessControl) returns (bool) { - 43 [ + + ]: 342 : if (AccessControl.hasRole(DEFAULT_ADMIN_ROLE, account)) { - 44 : 183 : return true; - 45 : : } else { - 46 : 159 : return AccessControl.hasRole(role, account); - 47 : : } - 48 : : } - 49 : : - 50 : : /* ============ ERC-165 ============ */ - 51 : 4 : function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, IERC165) returns (bool) { - 52 : 4 : return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID - 53 : 3 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID - 54 : 2 : || AccessControl.supportsInterface(interfaceId); - 55 : : } - 56 : : - 57 : : /*////////////////////////////////////////////////////////////// - 58 : : ERC-2771 - 59 : : //////////////////////////////////////////////////////////////*/ - 60 : 15 : function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} - 61 : 168 : function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} - 62 : : - 63 : : /** - 64 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 65 : : */ - 66 : 359 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { - 67 : 359 : return ERC2771Context._msgSender(); - 68 : : } - 69 : : - 70 : : /** - 71 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 72 : : */ - 73 : 1 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { - 74 : 1 : return ERC2771Context._msgData(); - 75 : : } - 76 : : - 77 : : /** - 78 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 79 : : */ - 80 : 360 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { - 81 : 360 : return ERC2771Context._contextSuffixLength(); - 82 : : } - 83 : : } + 15 : : import {RuleEngineBase} from "../RuleEngineBase.sol"; + 16 : : import {ComplianceInterfaceId} from "../modules/library/ComplianceInterfaceId.sol"; + 17 : : + 18 : : /** + 19 : : * @title Implementation of a ruleEngine as defined by the CMTAT + 20 : : */ + 21 : : contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase, AccessControl { + 22 : : /** + 23 : : * @param admin Address of the contract (Access Control) + 24 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + 25 : : */ + 26 : 127 : constructor(address admin, address forwarderIrrevocable, address tokenContract) + 27 : : ERC2771ModuleStandalone(forwarderIrrevocable) + 28 : : { + 29 [ + ]: 127 : if (admin == address(0)) { + 30 : 1 : revert RuleEngine_AdminWithAddressZeroNotAllowed(); + 31 : : } + 32 [ + ]: 126 : if (tokenContract != address(0)) { + 33 : 31 : _bindToken(tokenContract); + 34 : : } + 35 : 126 : _grantRole(DEFAULT_ADMIN_ROLE, admin); + 36 : : } + 37 : : + 38 : : /* ============ ACCESS CONTROL ============ */ + 39 : : /** + 40 : : * @notice Returns `true` if `account` has been granted `role`. + 41 : : * @dev The Default Admin has all roles + 42 : : */ + 43 : 135 : function hasRole(bytes32 role, address account) public view virtual override(AccessControl) returns (bool) { + 44 [ + + ]: 350 : if (AccessControl.hasRole(DEFAULT_ADMIN_ROLE, account)) { + 45 : 185 : return true; + 46 : : } else { + 47 : 165 : return AccessControl.hasRole(role, account); + 48 : : } + 49 : : } + 50 : : + 51 : : /* ============ ERC-165 ============ */ + 52 : 6 : function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, IERC165) returns (bool) { + 53 : 6 : return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + 54 : 5 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + 55 : 4 : || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID + 56 : 3 : || interfaceId == ComplianceInterfaceId.IERC7551_COMPLIANCE_INTERFACE_ID + 57 : 2 : || AccessControl.supportsInterface(interfaceId); + 58 : : } + 59 : : + 60 : : /*////////////////////////////////////////////////////////////// + 61 : : ERC-2771 + 62 : : //////////////////////////////////////////////////////////////*/ + 63 : 15 : function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + 64 : 170 : function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + 65 : : + 66 : : /** + 67 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 68 : : */ + 69 : 366 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { + 70 : 366 : return ERC2771Context._msgSender(); + 71 : : } + 72 : : + 73 : : /** + 74 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 75 : : */ + 76 : 1 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { + 77 : 1 : return ERC2771Context._msgData(); + 78 : : } + 79 : : + 80 : : /** + 81 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 82 : : */ + 83 : 367 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { + 84 : 367 : return ERC2771Context._contextSuffixLength(); + 85 : : } + 86 : : } @@ -159,7 +162,7 @@
- +
Generated by: LCOV version 1.16

diff --git a/doc/coverage/coverage/src/RuleEngineOwnable.sol.func-sort-c.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func-sort-c.html similarity index 70% rename from doc/coverage/coverage/src/RuleEngineOwnable.sol.func-sort-c.html rename to doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func-sort-c.html index 8dc4707..cac79cd 100644 --- a/doc/coverage/coverage/src/RuleEngineOwnable.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func-sort-c.html @@ -4,22 +4,22 @@ - LCOV - lcov.info - src/RuleEngineOwnable.sol - functions - + LCOV - lcov.info - src/deployment/RuleEngineOwnable.sol - functions + - + - +
LCOV - code coverage report
- + @@ -31,13 +31,13 @@ - - + + - + @@ -53,28 +53,28 @@ - +
Current view:top level - src - RuleEngineOwnable.sol (source / functions)top level - src/deployment - RuleEngineOwnable.sol (source / functions) Hitlcov.info Lines:15151818 100.0 %
Date:2026-02-16 13:49:212026-03-18 19:08:29 Functions: 71 100.0 %
- - + + - + - + @@ -86,21 +86,21 @@ - + - + - +

Function Name Sort by function nameHit count Sort by hit countFunction Name Sort by function nameHit count Sort by hit count
RuleEngineOwnable._msgDataRuleEngineOwnable._msgData 1
RuleEngineOwnable.supportsInterface69
RuleEngineOwnable._onlyComplianceManager
RuleEngineOwnable.constructor7177
RuleEngineOwnable._msgSenderRuleEngineOwnable._msgSender 108
RuleEngineOwnable._contextSuffixLengthRuleEngineOwnable._contextSuffixLength 109

- +
Generated by: LCOV version 1.16

diff --git a/doc/coverage/coverage/src/RuleEngineOwnable.sol.func.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func.html similarity index 69% rename from doc/coverage/coverage/src/RuleEngineOwnable.sol.func.html rename to doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func.html index e01a844..2714e56 100644 --- a/doc/coverage/coverage/src/RuleEngineOwnable.sol.func.html +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func.html @@ -4,22 +4,22 @@ - LCOV - lcov.info - src/RuleEngineOwnable.sol - functions - + LCOV - lcov.info - src/deployment/RuleEngineOwnable.sol - functions + - + - +
LCOV - code coverage report
- + @@ -31,13 +31,13 @@ - - + + - + @@ -53,31 +53,31 @@ - +
Current view:top level - src - RuleEngineOwnable.sol (source / functions)top level - src/deployment - RuleEngineOwnable.sol (source / functions) Hitlcov.info Lines:15151818 100.0 %
Date:2026-02-16 13:49:212026-03-18 19:08:29 Functions: 71 100.0 %
- - + + - + - + - + @@ -90,17 +90,17 @@ - + - +

Function Name Sort by function nameHit count Sort by hit countFunction Name Sort by function nameHit count Sort by hit count
RuleEngineOwnable._contextSuffixLengthRuleEngineOwnable._contextSuffixLength 109
RuleEngineOwnable._msgDataRuleEngineOwnable._msgData 1
RuleEngineOwnable._msgSenderRuleEngineOwnable._msgSender 108
RuleEngineOwnable.constructor7177
RuleEngineOwnable.supportsInterface69

- +
Generated by: LCOV version 1.16

diff --git a/doc/coverage/coverage/src/RuleEngineOwnable.sol.gcov.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.gcov.html similarity index 84% rename from doc/coverage/coverage/src/RuleEngineOwnable.sol.gcov.html rename to doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.gcov.html index 6dac2ac..86ca861 100644 --- a/doc/coverage/coverage/src/RuleEngineOwnable.sol.gcov.html +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.gcov.html @@ -4,22 +4,22 @@ - LCOV - lcov.info - src/RuleEngineOwnable.sol - + LCOV - lcov.info - src/deployment/RuleEngineOwnable.sol + - + - +
LCOV - code coverage report
- + @@ -31,13 +31,13 @@ - - + + - + @@ -53,12 +53,12 @@ - +
Current view:top level - src - RuleEngineOwnable.sol (source / functions)top level - src/deployment - RuleEngineOwnable.sol (source / functions) Hitlcov.info Lines:15151818 100.0 %
Date:2026-02-16 13:49:212026-03-18 19:08:29 Functions: 71 100.0 %
@@ -80,11 +80,11 @@ 9 : : import {Context} from "OZ/utils/Context.sol"; 10 : : import {Ownable} from "OZ/access/Ownable.sol"; 11 : : import {IERC165} from "OZ/utils/introspection/IERC165.sol"; - 12 : : import {AccessControl} from "OZ/access/AccessControl.sol"; - 13 : : /* ==== Modules === */ - 14 : : import {ERC2771ModuleStandalone, ERC2771Context} from "./modules/ERC2771ModuleStandalone.sol"; - 15 : : /* ==== Base contract === */ - 16 : : import {RuleEngineBase} from "./RuleEngineBase.sol"; + 12 : : /* ==== Modules === */ + 13 : : import {ERC2771ModuleStandalone, ERC2771Context} from "../modules/ERC2771ModuleStandalone.sol"; + 14 : : /* ==== Base contract === */ + 15 : : import {RuleEngineBase} from "../RuleEngineBase.sol"; + 16 : : import {ComplianceInterfaceId} from "../modules/library/ComplianceInterfaceId.sol"; 17 : : 18 : : /** 19 : : * @title Implementation of a ruleEngine with ERC-173 Ownable access control @@ -97,13 +97,13 @@ 26 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support 27 : : * @param tokenContract Address of the token contract to bind (can be zero address) 28 : : */ - 29 : 71 : constructor(address owner_, address forwarderIrrevocable, address tokenContract) + 29 : 77 : constructor(address owner_, address forwarderIrrevocable, address tokenContract) 30 : : ERC2771ModuleStandalone(forwarderIrrevocable) 31 : : Ownable(owner_) 32 : : { 33 : : // Note: zero-address check for owner_ is handled by Ownable(owner_), 34 : : // which reverts with OwnableInvalidOwner(address(0)) before reaching here. - 35 [ + ]: 70 : if (tokenContract != address(0)) { + 35 [ + ]: 76 : if (tokenContract != address(0)) { 36 : 1 : _bindToken(tokenContract); 37 : : } 38 : : } @@ -120,37 +120,40 @@ 49 : 21 : function _onlyComplianceManager() internal virtual override onlyOwner {} 50 : : 51 : : /* ============ ERC-165 ============ */ - 52 : 6 : function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, IERC165) returns (bool) { - 53 : 6 : return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID - 54 : 5 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID || interfaceId == ERC173_INTERFACE_ID - 55 : 2 : || AccessControl.supportsInterface(interfaceId); - 56 : : } - 57 : : - 58 : : /*////////////////////////////////////////////////////////////// - 59 : : ERC-2771 - 60 : : //////////////////////////////////////////////////////////////*/ - 61 : : - 62 : : /** - 63 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 64 : : */ - 65 : 108 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { - 66 : 108 : return ERC2771Context._msgSender(); - 67 : : } - 68 : : - 69 : : /** - 70 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 71 : : */ - 72 : 1 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { - 73 : 1 : return ERC2771Context._msgData(); - 74 : : } - 75 : : - 76 : : /** - 77 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 78 : : */ - 79 : 109 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { - 80 : 109 : return ERC2771Context._contextSuffixLength(); - 81 : : } - 82 : : } + 52 : 9 : function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165) returns (bool) { + 53 : 9 : return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + 54 : 8 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + 55 : 7 : || interfaceId == ERC173_INTERFACE_ID + 56 : 5 : || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID + 57 : 4 : || interfaceId == ComplianceInterfaceId.IERC7551_COMPLIANCE_INTERFACE_ID + 58 : 3 : || interfaceId == type(IERC165).interfaceId; + 59 : : } + 60 : : + 61 : : /*////////////////////////////////////////////////////////////// + 62 : : ERC-2771 + 63 : : //////////////////////////////////////////////////////////////*/ + 64 : : + 65 : : /** + 66 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 67 : : */ + 68 : 108 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { + 69 : 108 : return ERC2771Context._msgSender(); + 70 : : } + 71 : : + 72 : : /** + 73 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 74 : : */ + 75 : 1 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { + 76 : 1 : return ERC2771Context._msgData(); + 77 : : } + 78 : : + 79 : : /** + 80 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 81 : : */ + 82 : 109 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { + 83 : 109 : return ERC2771Context._contextSuffixLength(); + 84 : : } + 85 : : } @@ -158,7 +161,7 @@
- +
Generated by: LCOV version 1.16

diff --git a/doc/coverage/coverage/src/deployment/index-sort-b.html b/doc/coverage/coverage/src/deployment/index-sort-b.html new file mode 100644 index 0000000..f15808a --- /dev/null +++ b/doc/coverage/coverage/src/deployment/index-sort-b.html @@ -0,0 +1,119 @@ + + + + + + + LCOV - lcov.info - src/deployment + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deploymentHitTotalCoverage
Test:lcov.infoLines:4242100.0 %
Date:2026-03-18 19:08:29Functions:1515100.0 %
Branches:55100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
RuleEngineOwnable.sol +
100.0%
+
100.0 %18 / 18100.0 %7 / 7100.0 %1 / 1
RuleEngine.sol +
100.0%
+
100.0 %24 / 24100.0 %8 / 8100.0 %4 / 4
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/deployment/index-sort-f.html b/doc/coverage/coverage/src/deployment/index-sort-f.html new file mode 100644 index 0000000..c49a9ca --- /dev/null +++ b/doc/coverage/coverage/src/deployment/index-sort-f.html @@ -0,0 +1,119 @@ + + + + + + + LCOV - lcov.info - src/deployment + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deploymentHitTotalCoverage
Test:lcov.infoLines:4242100.0 %
Date:2026-03-18 19:08:29Functions:1515100.0 %
Branches:55100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
RuleEngineOwnable.sol +
100.0%
+
100.0 %18 / 18100.0 %7 / 7100.0 %1 / 1
RuleEngine.sol +
100.0%
+
100.0 %24 / 24100.0 %8 / 8100.0 %4 / 4
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/deployment/index-sort-l.html b/doc/coverage/coverage/src/deployment/index-sort-l.html new file mode 100644 index 0000000..298c674 --- /dev/null +++ b/doc/coverage/coverage/src/deployment/index-sort-l.html @@ -0,0 +1,119 @@ + + + + + + + LCOV - lcov.info - src/deployment + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deploymentHitTotalCoverage
Test:lcov.infoLines:4242100.0 %
Date:2026-03-18 19:08:29Functions:1515100.0 %
Branches:55100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
RuleEngineOwnable.sol +
100.0%
+
100.0 %18 / 18100.0 %7 / 7100.0 %1 / 1
RuleEngine.sol +
100.0%
+
100.0 %24 / 24100.0 %8 / 8100.0 %4 / 4
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/deployment/index.html b/doc/coverage/coverage/src/deployment/index.html new file mode 100644 index 0000000..c7383b6 --- /dev/null +++ b/doc/coverage/coverage/src/deployment/index.html @@ -0,0 +1,119 @@ + + + + + + + LCOV - lcov.info - src/deployment + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deploymentHitTotalCoverage
Test:lcov.infoLines:4242100.0 %
Date:2026-03-18 19:08:29Functions:1515100.0 %
Branches:55100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
RuleEngine.sol +
100.0%
+
100.0 %24 / 24100.0 %8 / 8100.0 %4 / 4
RuleEngineOwnable.sol +
100.0%
+
100.0 %18 / 18100.0 %7 / 7100.0 %1 / 1
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/index-sort-b.html b/doc/coverage/coverage/src/index-sort-b.html index 35c348b..9424617 100644 --- a/doc/coverage/coverage/src/index-sort-b.html +++ b/doc/coverage/coverage/src/index-sort-b.html @@ -31,17 +31,17 @@ lcov.info Lines: - 79 - 79 + 42 + 42 100.0 % Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: - 28 - 28 + 13 + 13 100.0 % @@ -49,8 +49,8 @@ Branches: - 9 - 9 + 4 + 4 100.0 % @@ -81,30 +81,6 @@ Functions Sort by function coverage Branches Sort by branch coverage - - RuleEngineOwnable.sol - -
100.0%
- - 100.0 % - 15 / 15 - 100.0 % - 7 / 7 - 100.0 % - 1 / 1 - - - RuleEngine.sol - -
100.0%
- - 100.0 % - 22 / 22 - 100.0 % - 8 / 8 - 100.0 % - 4 / 4 - RuleEngineBase.sol diff --git a/doc/coverage/coverage/src/index-sort-f.html b/doc/coverage/coverage/src/index-sort-f.html index c8dc45e..b39eba9 100644 --- a/doc/coverage/coverage/src/index-sort-f.html +++ b/doc/coverage/coverage/src/index-sort-f.html @@ -31,17 +31,17 @@ lcov.info Lines: - 79 - 79 + 42 + 42 100.0 % Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: - 28 - 28 + 13 + 13 100.0 % @@ -49,8 +49,8 @@ Branches: - 9 - 9 + 4 + 4 100.0 % @@ -81,30 +81,6 @@ Functions Sort by function coverage Branches Sort by branch coverage - - RuleEngineOwnable.sol - -
100.0%
- - 100.0 % - 15 / 15 - 100.0 % - 7 / 7 - 100.0 % - 1 / 1 - - - RuleEngine.sol - -
100.0%
- - 100.0 % - 22 / 22 - 100.0 % - 8 / 8 - 100.0 % - 4 / 4 - RuleEngineBase.sol diff --git a/doc/coverage/coverage/src/index-sort-l.html b/doc/coverage/coverage/src/index-sort-l.html index 20e49f7..b0d14a3 100644 --- a/doc/coverage/coverage/src/index-sort-l.html +++ b/doc/coverage/coverage/src/index-sort-l.html @@ -31,17 +31,17 @@ lcov.info Lines: - 79 - 79 + 42 + 42 100.0 % Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: - 28 - 28 + 13 + 13 100.0 % @@ -49,8 +49,8 @@ Branches: - 9 - 9 + 4 + 4 100.0 % @@ -81,30 +81,6 @@ Functions Sort by function coverage Branches Sort by branch coverage - - RuleEngineOwnable.sol - -
100.0%
- - 100.0 % - 15 / 15 - 100.0 % - 7 / 7 - 100.0 % - 1 / 1 - - - RuleEngine.sol - -
100.0%
- - 100.0 % - 22 / 22 - 100.0 % - 8 / 8 - 100.0 % - 4 / 4 - RuleEngineBase.sol diff --git a/doc/coverage/coverage/src/index.html b/doc/coverage/coverage/src/index.html index fbba042..26bcfa8 100644 --- a/doc/coverage/coverage/src/index.html +++ b/doc/coverage/coverage/src/index.html @@ -31,17 +31,17 @@ lcov.info Lines: - 79 - 79 + 42 + 42 100.0 % Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: - 28 - 28 + 13 + 13 100.0 % @@ -49,8 +49,8 @@ Branches: - 9 - 9 + 4 + 4 100.0 % @@ -81,18 +81,6 @@ Functions Sort by function coverage Branches Sort by branch coverage - - RuleEngine.sol - -
100.0%
- - 100.0 % - 22 / 22 - 100.0 % - 8 / 8 - 100.0 % - 4 / 4 - RuleEngineBase.sol @@ -105,18 +93,6 @@ 100.0 % 4 / 4 - - RuleEngineOwnable.sol - -
100.0%
- - 100.0 % - 15 / 15 - 100.0 % - 7 / 7 - 100.0 % - 1 / 1 -
diff --git a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func-sort-c.html b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func-sort-c.html index db87ba1..1447d4e 100644 --- a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 10 @@ -69,11 +69,11 @@ Hit count Sort by hit count - ERC3643ComplianceModule.getTokenBounds + ERC3643ComplianceModule.getTokenBounds 4 - ERC3643ComplianceModule.getTokenBound + ERC3643ComplianceModule.getTokenBound 5 @@ -81,19 +81,19 @@ 5 - ERC3643ComplianceModule._unbindToken + ERC3643ComplianceModule._unbindToken 9 - ERC3643ComplianceModule.unbindToken + ERC3643ComplianceModule.unbindToken 10 - ERC3643ComplianceModule.isTokenBound + ERC3643ComplianceModule.isTokenBound 15 - ERC3643ComplianceModule.bindToken + ERC3643ComplianceModule.bindToken 26 @@ -101,12 +101,12 @@ 26 - ERC3643ComplianceModule._checkBoundToken - 32 + ERC3643ComplianceModule._checkBoundToken + 31 - ERC3643ComplianceModule._bindToken - 56 + ERC3643ComplianceModule._bindToken + 57
diff --git a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html index 290bfba..53bed1c 100644 --- a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html +++ b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 10 @@ -69,31 +69,31 @@ Hit count Sort by hit count - ERC3643ComplianceModule._bindToken - 56 + ERC3643ComplianceModule._bindToken + 57 - ERC3643ComplianceModule._checkBoundToken - 32 + ERC3643ComplianceModule._checkBoundToken + 31 - ERC3643ComplianceModule._unbindToken + ERC3643ComplianceModule._unbindToken 9 - ERC3643ComplianceModule.bindToken + ERC3643ComplianceModule.bindToken 26 - ERC3643ComplianceModule.getTokenBound + ERC3643ComplianceModule.getTokenBound 5 - ERC3643ComplianceModule.getTokenBounds + ERC3643ComplianceModule.getTokenBounds 4 - ERC3643ComplianceModule.isTokenBound + ERC3643ComplianceModule.isTokenBound 15 @@ -105,7 +105,7 @@ 26 - ERC3643ComplianceModule.unbindToken + ERC3643ComplianceModule.unbindToken 10 diff --git a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html index f2bcda4..0dfe087 100644 --- a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html +++ b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 10 @@ -112,65 +112,75 @@ 41 : : //////////////////////////////////////////////////////////////*/ 42 : : 43 : : /* ============ State functions ============ */ - 44 : : /// @inheritdoc IERC3643Compliance - 45 : 26 : function bindToken(address token) public virtual override onlyComplianceManager { - 46 : 25 : _bindToken(token); - 47 : : } - 48 : : - 49 : : /// @inheritdoc IERC3643Compliance - 50 : 10 : function unbindToken(address token) public virtual override onlyComplianceManager { - 51 : 9 : _unbindToken(token); + 44 : : /** + 45 : : * @inheritdoc IERC3643Compliance + 46 : : * @dev Operator warning: "multi-tenant" means one RuleEngine is shared by + 47 : : * multiple token contracts. In that setup, bind only tokens that are equally + 48 : : * trusted and governed together. + 49 : : */ + 50 : 26 : function bindToken(address token) public virtual override onlyComplianceManager { + 51 : 25 : _bindToken(token); 52 : : } 53 : : - 54 : : /// @inheritdoc IERC3643Compliance - 55 : 15 : function isTokenBound(address token) public view virtual override returns (bool) { - 56 : 15 : return _boundTokens.contains(token); - 57 : : } - 58 : : - 59 : : /// @inheritdoc IERC3643Compliance - 60 : 5 : function getTokenBound() public view virtual override returns (address) { - 61 [ + + ]: 5 : if (_boundTokens.length() > 0) { - 62 : : // Note that there are no guarantees on the ordering of values inside the array, - 63 : : // and it may change when more values are added or removed. - 64 : 3 : return _boundTokens.at(0); - 65 : : } else { - 66 : 2 : return address(0); - 67 : : } - 68 : : } - 69 : : - 70 : : /// @inheritdoc IERC3643Compliance - 71 : 4 : function getTokenBounds() public view override returns (address[] memory) { - 72 : 4 : return _boundTokens.values(); - 73 : : } - 74 : : - 75 : : /*////////////////////////////////////////////////////////////// - 76 : : INTERNAL/PRIVATE FUNCTIONS - 77 : : //////////////////////////////////////////////////////////////*/ - 78 : : - 79 : 9 : function _unbindToken(address token) internal { - 80 [ + + ]: 9 : require(_boundTokens.contains(token), RuleEngine_ERC3643Compliance_TokenNotBound()); - 81 : : // Should never revert because we check if the token address is already set before - 82 [ # + ]: 7 : require(_boundTokens.remove(token), RuleEngine_ERC3643Compliance_OperationNotSuccessful()); - 83 : : - 84 : 7 : emit TokenUnbound(token); - 85 : : } - 86 : : - 87 : 56 : function _bindToken(address token) internal { - 88 [ + + ]: 56 : require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); - 89 [ + + ]: 54 : require(!_boundTokens.contains(token), RuleEngine_ERC3643Compliance_TokenAlreadyBound()); - 90 : : // Should never revert because we check if the token address is already set before - 91 [ # + ]: 52 : require(_boundTokens.add(token), RuleEngine_ERC3643Compliance_OperationNotSuccessful()); - 92 : 52 : emit TokenBound(token); - 93 : : } - 94 : : - 95 : 32 : function _checkBoundToken() internal view virtual{ - 96 [ + ]: 32 : if (!_boundTokens.contains(_msgSender())) { - 97 : 7 : revert RuleEngine_ERC3643Compliance_UnauthorizedCaller(); - 98 : : } - 99 : : } - 100 : : - 101 : : function _onlyComplianceManager() internal virtual; - 102 : : } + 54 : : /** + 55 : : * @inheritdoc IERC3643Compliance + 56 : : * @dev Operator warning: unbinding is an administrative operation and does not + 57 : : * erase any state already stored by external rule contracts in a previously + 58 : : * shared ("multi-tenant") setup. + 59 : : */ + 60 : 10 : function unbindToken(address token) public virtual override onlyComplianceManager { + 61 : 9 : _unbindToken(token); + 62 : : } + 63 : : + 64 : : /// @inheritdoc IERC3643Compliance + 65 : 15 : function isTokenBound(address token) public view virtual override returns (bool) { + 66 : 15 : return _boundTokens.contains(token); + 67 : : } + 68 : : + 69 : : /// @inheritdoc IERC3643Compliance + 70 : 5 : function getTokenBound() public view virtual override returns (address) { + 71 [ + + ]: 5 : if (_boundTokens.length() > 0) { + 72 : : // Note that there are no guarantees on the ordering of values inside the array, + 73 : : // and it may change when more values are added or removed. + 74 : 3 : return _boundTokens.at(0); + 75 : : } else { + 76 : 2 : return address(0); + 77 : : } + 78 : : } + 79 : : + 80 : : /// @inheritdoc IERC3643Compliance + 81 : 4 : function getTokenBounds() public view override returns (address[] memory) { + 82 : 4 : return _boundTokens.values(); + 83 : : } + 84 : : + 85 : : /*////////////////////////////////////////////////////////////// + 86 : : INTERNAL/PRIVATE FUNCTIONS + 87 : : //////////////////////////////////////////////////////////////*/ + 88 : : + 89 : 9 : function _unbindToken(address token) internal { + 90 [ + + ]: 9 : require(_boundTokens.contains(token), RuleEngine_ERC3643Compliance_TokenNotBound()); + 91 : : // Should never revert because we check if the token address is already set before + 92 [ # + ]: 7 : require(_boundTokens.remove(token), RuleEngine_ERC3643Compliance_OperationNotSuccessful()); + 93 : : + 94 : 7 : emit TokenUnbound(token); + 95 : : } + 96 : : + 97 : 57 : function _bindToken(address token) internal { + 98 [ + + ]: 57 : require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); + 99 [ + + ]: 55 : require(!_boundTokens.contains(token), RuleEngine_ERC3643Compliance_TokenAlreadyBound()); + 100 : : // Should never revert because we check if the token address is already set before + 101 [ # + ]: 53 : require(_boundTokens.add(token), RuleEngine_ERC3643Compliance_OperationNotSuccessful()); + 102 : 53 : emit TokenBound(token); + 103 : : } + 104 : : + 105 : 31 : function _checkBoundToken() internal view virtual{ + 106 [ + ]: 31 : if (!_boundTokens.contains(_msgSender())) { + 107 : 7 : revert RuleEngine_ERC3643Compliance_UnauthorizedCaller(); + 108 : : } + 109 : : } + 110 : : + 111 : : function _onlyComplianceManager() internal virtual; + 112 : : } diff --git a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func-sort-c.html b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func-sort-c.html index c791411..9c2e3a2 100644 --- a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 14 @@ -69,60 +69,60 @@ Hit count Sort by hit count - RulesManagementModule._transferred.1 + RulesManagementModule._transferred.1 4 - RulesManagementModule.rule + RulesManagementModule.rule 5 - RulesManagementModule._removeRule + RulesManagementModule._removeRule 13 - RulesManagementModule.clearRules + RulesManagementModule.clearRules 15 - RulesManagementModule.onlyRulesManager + RulesManagementModule.onlyRulesManager 15 - RulesManagementModule.rules + RulesManagementModule.rules 15 - RulesManagementModule.removeRule + RulesManagementModule.removeRule 18 - RulesManagementModule._transferred.0 - 21 + RulesManagementModule._transferred.0 + 20 - RulesManagementModule._clearRules + RulesManagementModule._clearRules 48 - RulesManagementModule.setRules + RulesManagementModule.setRules 49 - RulesManagementModule.containsRule + RulesManagementModule.containsRule 71 - RulesManagementModule.addRule - 150 + RulesManagementModule.addRule + 152 - RulesManagementModule.rulesCount + RulesManagementModule.rulesCount 161 - RulesManagementModule._checkRule - 215 + RulesManagementModule._checkRule + 217
diff --git a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html index 62ea7f1..2a8a1b0 100644 --- a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html +++ b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 14 @@ -69,59 +69,59 @@ Hit count Sort by hit count - RulesManagementModule._checkRule - 215 + RulesManagementModule._checkRule + 217 - RulesManagementModule._clearRules + RulesManagementModule._clearRules 48 - RulesManagementModule._removeRule + RulesManagementModule._removeRule 13 - RulesManagementModule._transferred.0 - 21 + RulesManagementModule._transferred.0 + 20 - RulesManagementModule._transferred.1 + RulesManagementModule._transferred.1 4 - RulesManagementModule.addRule - 150 + RulesManagementModule.addRule + 152 - RulesManagementModule.clearRules + RulesManagementModule.clearRules 15 - RulesManagementModule.containsRule + RulesManagementModule.containsRule 71 - RulesManagementModule.onlyRulesManager + RulesManagementModule.onlyRulesManager 15 - RulesManagementModule.removeRule + RulesManagementModule.removeRule 18 - RulesManagementModule.rule + RulesManagementModule.rule 5 - RulesManagementModule.rules + RulesManagementModule.rules 15 - RulesManagementModule.rulesCount + RulesManagementModule.rulesCount 161 - RulesManagementModule.setRules + RulesManagementModule.setRules 49 diff --git a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html index 399cd91..97c1d96 100644 --- a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html +++ b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 14 @@ -75,186 +75,200 @@ 4 : : 5 : : /* ==== OpenZeppelin === */ 6 : : import {EnumerableSet} from "OZ/utils/structs/EnumerableSet.sol"; - 7 : : import {AccessControl} from "OZ/access/AccessControl.sol"; - 8 : : /* ==== Interface and other library === */ - 9 : : import {IRulesManagementModule} from "../interfaces/IRulesManagementModule.sol"; - 10 : : import {IRule} from "../interfaces/IRule.sol"; - 11 : : import {RulesManagementModuleInvariantStorage} from "./library/RulesManagementModuleInvariantStorage.sol"; - 12 : : - 13 : : /** - 14 : : * @title RuleEngine - part - 15 : : */ - 16 : : abstract contract RulesManagementModule is - 17 : : AccessControl, - 18 : : RulesManagementModuleInvariantStorage, - 19 : : IRulesManagementModule - 20 : : { - 21 : 15 : modifier onlyRulesManager() { - 22 : 15 : _onlyRulesManager(); - 23 : : _; - 24 : : } - 25 : : - 26 : : /* ==== Type declaration === */ - 27 : : using EnumerableSet for EnumerableSet.AddressSet; - 28 : : - 29 : : /* ==== State Variables === */ - 30 : : /// @dev Array of rules - 31 : : EnumerableSet.AddressSet internal _rules; - 32 : : - 33 : : /*////////////////////////////////////////////////////////////// - 34 : : PUBLIC/EXTERNAL FUNCTIONS - 35 : : //////////////////////////////////////////////////////////////*/ - 36 : : - 37 : : /* ============ State functions ============ */ - 38 : : - 39 : : /** - 40 : : * @inheritdoc IRulesManagementModule - 41 : : */ - 42 : 49 : function setRules(IRule[] calldata rules_) public virtual override(IRulesManagementModule) onlyRulesManager { - 43 [ + ]: 47 : if (rules_.length == 0) { - 44 : 6 : revert RuleEngine_RulesManagementModule_ArrayIsEmpty(); - 45 : : } - 46 [ + ]: 41 : if (_rules.length() > 0) { - 47 : 35 : _clearRules(); + 7 : : /* ==== Interface and other library === */ + 8 : : import {IRulesManagementModule} from "../interfaces/IRulesManagementModule.sol"; + 9 : : import {IRule} from "../interfaces/IRule.sol"; + 10 : : import {RulesManagementModuleInvariantStorage} from "./library/RulesManagementModuleInvariantStorage.sol"; + 11 : : + 12 : : /** + 13 : : * @title RuleEngine - part + 14 : : */ + 15 : : abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage, IRulesManagementModule { + 16 : 15 : modifier onlyRulesManager() { + 17 : 15 : _onlyRulesManager(); + 18 : : _; + 19 : : } + 20 : : + 21 : : /* ==== Type declaration === */ + 22 : : using EnumerableSet for EnumerableSet.AddressSet; + 23 : : + 24 : : /* ==== State Variables === */ + 25 : : /// @dev Array of rules + 26 : : EnumerableSet.AddressSet internal _rules; + 27 : : + 28 : : /*////////////////////////////////////////////////////////////// + 29 : : PUBLIC/EXTERNAL FUNCTIONS + 30 : : //////////////////////////////////////////////////////////////*/ + 31 : : + 32 : : /* ============ State functions ============ */ + 33 : : + 34 : : /** + 35 : : * @inheritdoc IRulesManagementModule + 36 : : * @dev Replaces the entire rule set atomically. + 37 : : * Reverts if `rules_` is empty. Use {clearRules} to remove all rules explicitly. + 38 : : * To transition from one non-empty set to another without an enforcement gap, + 39 : : * call this function directly with the new set. + 40 : : * No on-chain maximum number of rules is enforced. Operators are responsible + 41 : : * for keeping the rule set size compatible with the target chain gas limits. + 42 : : * Security convention: rule contracts should be treated as trusted business logic, + 43 : : * but should not also be granted {RULES_MANAGEMENT_ROLE}. + 44 : : */ + 45 : 49 : function setRules(IRule[] calldata rules_) public virtual override(IRulesManagementModule) onlyRulesManager { + 46 [ + ]: 47 : if (rules_.length == 0) { + 47 : 6 : revert RuleEngine_RulesManagementModule_ArrayIsEmpty(); 48 : : } - 49 : 41 : for (uint256 i = 0; i < rules_.length; ++i) { - 50 : 69 : _checkRule(address(rules_[i])); - 51 : : // Should never revert because we check the presence of the rule before - 52 [ # + ]: 64 : require(_rules.add(address(rules_[i])), RuleEngine_RulesManagementModule_OperationNotSuccessful()); - 53 : 64 : emit AddRule(rules_[i]); - 54 : : } - 55 : : } - 56 : : - 57 : : /** - 58 : : * @inheritdoc IRulesManagementModule - 59 : : */ - 60 : 15 : function clearRules() public virtual override(IRulesManagementModule) onlyRulesManager { - 61 : 13 : _clearRules(); - 62 : : } - 63 : : - 64 : : /** - 65 : : * @inheritdoc IRulesManagementModule - 66 : : */ - 67 : 150 : function addRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { - 68 : 146 : _checkRule(address(rule_)); - 69 [ # + ]: 136 : require(_rules.add(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); - 70 : 136 : emit AddRule(rule_); - 71 : : } - 72 : : - 73 : : /** - 74 : : * @inheritdoc IRulesManagementModule - 75 : : */ - 76 : 18 : function removeRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { - 77 [ + + ]: 16 : require(_rules.contains(address(rule_)), RuleEngine_RulesManagementModule_RuleDoNotMatch()); - 78 : 13 : _removeRule(rule_); - 79 : : } - 80 : : - 81 : : /* ============ View functions ============ */ - 82 : : - 83 : : /** - 84 : : * @inheritdoc IRulesManagementModule - 85 : : */ - 86 : 161 : function rulesCount() public view virtual override(IRulesManagementModule) returns (uint256) { - 87 : 278 : return _rules.length(); - 88 : : } - 89 : : - 90 : : /** - 91 : : * @inheritdoc IRulesManagementModule - 92 : : */ - 93 : 71 : function containsRule(IRule rule_) public view virtual override(IRulesManagementModule) returns (bool) { - 94 : 71 : return _rules.contains(address(rule_)); - 95 : : } - 96 : : - 97 : : /** - 98 : : * @inheritdoc IRulesManagementModule - 99 : : */ - 100 : 5 : function rule(uint256 ruleId) public view virtual override(IRulesManagementModule) returns (address) { - 101 [ + + ]: 133 : if (ruleId < _rules.length()) { - 102 : : // Note that there are no guarantees on the ordering of values inside the array, - 103 : : // and it may change when more values are added or removed. - 104 : 131 : return _rules.at(ruleId); - 105 : : } else { - 106 : 2 : return address(0); - 107 : : } - 108 : : } - 109 : : - 110 : : /** - 111 : : * @inheritdoc IRulesManagementModule - 112 : : */ - 113 : 15 : function rules() public view virtual override(IRulesManagementModule) returns (address[] memory) { - 114 : 15 : return _rules.values(); - 115 : : } - 116 : : - 117 : : /*////////////////////////////////////////////////////////////// - 118 : : INTERNAL/PRIVATE FUNCTIONS - 119 : : //////////////////////////////////////////////////////////////*/ - 120 : : /** - 121 : : * @notice Clear all the rules of the array of rules - 122 : : * - 123 : : */ - 124 : 48 : function _clearRules() internal virtual { - 125 : 48 : emit ClearRules(); - 126 : 48 : _rules.clear(); - 127 : : } - 128 : : - 129 : : /** - 130 : : * @notice Remove a rule from the array of rules - 131 : : * Revert if the rule found at the specified index does not match the rule in argument - 132 : : * @param rule_ address of the target rule - 133 : : * - 134 : : * - 135 : : */ - 136 : 13 : function _removeRule(IRule rule_) internal virtual { - 137 : : // Should never revert because we check the presence of the rule before - 138 [ # + ]: 13 : require(_rules.remove(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); - 139 : 13 : emit RemoveRule(rule_); - 140 : : } - 141 : : - 142 : : /** - 143 : : * @dev check if a rule is valid, revert otherwise - 144 : : */ - 145 : 215 : function _checkRule(address rule_) internal view virtual { - 146 [ + ]: 215 : if (rule_ == address(0x0)) { - 147 : 3 : revert RuleEngine_RulesManagementModule_RuleAddressZeroNotAllowed(); - 148 : : } - 149 [ + ]: 212 : if (_rules.contains(rule_)) { - 150 : 6 : revert RuleEngine_RulesManagementModule_RuleAlreadyExists(); - 151 : : } - 152 : : } - 153 : : - 154 : : /* ============ Transferred functions ============ */ - 155 : : - 156 : : /** - 157 : : * @notice Go through all the rule to know if a restriction exists on the transfer - 158 : : * @param from the origin address - 159 : : * @param to the destination address - 160 : : * @param value to transfer - 161 : : * - 162 : : */ - 163 : 21 : function _transferred(address from, address to, uint256 value) internal virtual { - 164 : 21 : uint256 rulesLength = _rules.length(); - 165 : 21 : for (uint256 i = 0; i < rulesLength; ++i) { - 166 : 15 : IRule(_rules.at(i)).transferred(from, to, value); - 167 : : } - 168 : : } - 169 : : - 170 : : /** - 171 : : * @notice Go through all the rule to know if a restriction exists on the transfer - 172 : : * @param spender the spender address (transferFrom) - 173 : : * @param from the origin address - 174 : : * @param to the destination address - 175 : : * @param value to transfer - 176 : : * - 177 : : */ - 178 : 4 : function _transferred(address spender, address from, address to, uint256 value) internal virtual { - 179 : 4 : uint256 rulesLength = _rules.length(); - 180 : 4 : for (uint256 i = 0; i < rulesLength; ++i) { - 181 : 4 : IRule(_rules.at(i)).transferred(spender, from, to, value); - 182 : : } - 183 : : } - 184 : : - 185 : : function _onlyRulesManager() internal virtual; - 186 : : } + 49 [ + ]: 41 : if (_rules.length() > 0) { + 50 : 35 : _clearRules(); + 51 : : } + 52 : 41 : for (uint256 i = 0; i < rules_.length; ++i) { + 53 : 69 : _checkRule(address(rules_[i])); + 54 : : // Should never revert because we check the presence of the rule before + 55 [ # + ]: 64 : require(_rules.add(address(rules_[i])), RuleEngine_RulesManagementModule_OperationNotSuccessful()); + 56 : 64 : emit AddRule(rules_[i]); + 57 : : } + 58 : : } + 59 : : + 60 : : /** + 61 : : * @inheritdoc IRulesManagementModule + 62 : : */ + 63 : 15 : function clearRules() public virtual override(IRulesManagementModule) onlyRulesManager { + 64 : 13 : _clearRules(); + 65 : : } + 66 : : + 67 : : /** + 68 : : * @inheritdoc IRulesManagementModule + 69 : : * @dev No on-chain maximum number of rules is enforced. Adding too many rules + 70 : : * can increase transfer-time gas usage because rule checks are linear in rule count. + 71 : : * Security convention: do not grant {RULES_MANAGEMENT_ROLE} to rule contracts. + 72 : : */ + 73 : 152 : function addRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { + 74 : 148 : _checkRule(address(rule_)); + 75 [ # + ]: 138 : require(_rules.add(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); + 76 : 138 : emit AddRule(rule_); + 77 : : } + 78 : : + 79 : : /** + 80 : : * @inheritdoc IRulesManagementModule + 81 : : */ + 82 : 18 : function removeRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { + 83 [ + + ]: 16 : require(_rules.contains(address(rule_)), RuleEngine_RulesManagementModule_RuleDoNotMatch()); + 84 : 13 : _removeRule(rule_); + 85 : : } + 86 : : + 87 : : /* ============ View functions ============ */ + 88 : : + 89 : : /** + 90 : : * @inheritdoc IRulesManagementModule + 91 : : */ + 92 : 161 : function rulesCount() public view virtual override(IRulesManagementModule) returns (uint256) { + 93 : 278 : return _rules.length(); + 94 : : } + 95 : : + 96 : : /** + 97 : : * @inheritdoc IRulesManagementModule + 98 : : */ + 99 : 71 : function containsRule(IRule rule_) public view virtual override(IRulesManagementModule) returns (bool) { + 100 : 71 : return _rules.contains(address(rule_)); + 101 : : } + 102 : : + 103 : : /** + 104 : : * @inheritdoc IRulesManagementModule + 105 : : */ + 106 : 5 : function rule(uint256 ruleId) public view virtual override(IRulesManagementModule) returns (address) { + 107 [ + + ]: 133 : if (ruleId < _rules.length()) { + 108 : : // Note that there are no guarantees on the ordering of values inside the array, + 109 : : // and it may change when more values are added or removed. + 110 : 131 : return _rules.at(ruleId); + 111 : : } else { + 112 : 2 : return address(0); + 113 : : } + 114 : : } + 115 : : + 116 : : /** + 117 : : * @inheritdoc IRulesManagementModule + 118 : : */ + 119 : 15 : function rules() public view virtual override(IRulesManagementModule) returns (address[] memory) { + 120 : 15 : return _rules.values(); + 121 : : } + 122 : : + 123 : : /*////////////////////////////////////////////////////////////// + 124 : : INTERNAL/PRIVATE FUNCTIONS + 125 : : //////////////////////////////////////////////////////////////*/ + 126 : : /** + 127 : : * @notice Clear all the rules of the array of rules + 128 : : * + 129 : : */ + 130 : 48 : function _clearRules() internal virtual { + 131 : 48 : emit ClearRules(); + 132 : 48 : _rules.clear(); + 133 : : } + 134 : : + 135 : : /** + 136 : : * @notice Remove a rule from the array of rules + 137 : : * Revert if the rule found at the specified index does not match the rule in argument + 138 : : * @param rule_ address of the target rule + 139 : : * + 140 : : * + 141 : : */ + 142 : 13 : function _removeRule(IRule rule_) internal virtual { + 143 : : // Should never revert because we check the presence of the rule before + 144 [ # + ]: 13 : require(_rules.remove(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); + 145 : 13 : emit RemoveRule(rule_); + 146 : : } + 147 : : + 148 : : /** + 149 : : * @dev check if a rule is valid, revert otherwise + 150 : : */ + 151 : 217 : function _checkRule(address rule_) internal view virtual { + 152 [ + ]: 217 : if (rule_ == address(0x0)) { + 153 : 3 : revert RuleEngine_RulesManagementModule_RuleAddressZeroNotAllowed(); + 154 : : } + 155 [ + ]: 214 : if (_rules.contains(rule_)) { + 156 : 6 : revert RuleEngine_RulesManagementModule_RuleAlreadyExists(); + 157 : : } + 158 : : } + 159 : : + 160 : : /* ============ Transferred functions ============ */ + 161 : : + 162 : : /** + 163 : : * @notice Go through all the rule to know if a restriction exists on the transfer + 164 : : * @dev Complexity is O(number of configured rules). Large rule sets can make + 165 : : * transfers too expensive on chains with lower block gas limits. + 166 : : * Security convention: rule contracts are expected to be trusted and must not + 167 : : * hold {RULES_MANAGEMENT_ROLE}. + 168 : : * @param from the origin address + 169 : : * @param to the destination address + 170 : : * @param value to transfer + 171 : : * + 172 : : */ + 173 : 20 : function _transferred(address from, address to, uint256 value) internal virtual { + 174 : 20 : uint256 rulesLength = _rules.length(); + 175 : 20 : for (uint256 i = 0; i < rulesLength; ++i) { + 176 : 14 : IRule(_rules.at(i)).transferred(from, to, value); + 177 : : } + 178 : : } + 179 : : + 180 : : /** + 181 : : * @notice Go through all the rule to know if a restriction exists on the transfer + 182 : : * @dev Complexity is O(number of configured rules). Large rule sets can make + 183 : : * transfers too expensive on chains with lower block gas limits. + 184 : : * Security convention: rule contracts are expected to be trusted and must not + 185 : : * hold {RULES_MANAGEMENT_ROLE}. + 186 : : * @param spender the spender address (transferFrom) + 187 : : * @param from the origin address + 188 : : * @param to the destination address + 189 : : * @param value to transfer + 190 : : * + 191 : : */ + 192 : 4 : function _transferred(address spender, address from, address to, uint256 value) internal virtual { + 193 : 4 : uint256 rulesLength = _rules.length(); + 194 : 4 : for (uint256 i = 0; i < rulesLength; ++i) { + 195 : 4 : IRule(_rules.at(i)).transferred(spender, from, to, value); + 196 : : } + 197 : : } + 198 : : + 199 : : function _onlyRulesManager() internal virtual; + 200 : : } diff --git a/doc/coverage/coverage/src/modules/VersionModule.sol.func-sort-c.html b/doc/coverage/coverage/src/modules/VersionModule.sol.func-sort-c.html index ef40376..b953c88 100644 --- a/doc/coverage/coverage/src/modules/VersionModule.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/modules/VersionModule.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 1 diff --git a/doc/coverage/coverage/src/modules/VersionModule.sol.func.html b/doc/coverage/coverage/src/modules/VersionModule.sol.func.html index e09188d..f4517d4 100644 --- a/doc/coverage/coverage/src/modules/VersionModule.sol.func.html +++ b/doc/coverage/coverage/src/modules/VersionModule.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 1 diff --git a/doc/coverage/coverage/src/modules/VersionModule.sol.gcov.html b/doc/coverage/coverage/src/modules/VersionModule.sol.gcov.html index cfba628..cf339dc 100644 --- a/doc/coverage/coverage/src/modules/VersionModule.sol.gcov.html +++ b/doc/coverage/coverage/src/modules/VersionModule.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 1 diff --git a/doc/coverage/coverage/src/modules/index-sort-b.html b/doc/coverage/coverage/src/modules/index-sort-b.html index 4e2500b..b6a5a03 100644 --- a/doc/coverage/coverage/src/modules/index-sort-b.html +++ b/doc/coverage/coverage/src/modules/index-sort-b.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 25 diff --git a/doc/coverage/coverage/src/modules/index-sort-f.html b/doc/coverage/coverage/src/modules/index-sort-f.html index ea7db50..4252b69 100644 --- a/doc/coverage/coverage/src/modules/index-sort-f.html +++ b/doc/coverage/coverage/src/modules/index-sort-f.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 25 diff --git a/doc/coverage/coverage/src/modules/index-sort-l.html b/doc/coverage/coverage/src/modules/index-sort-l.html index d6d0b93..db2c56d 100644 --- a/doc/coverage/coverage/src/modules/index-sort-l.html +++ b/doc/coverage/coverage/src/modules/index-sort-l.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 25 diff --git a/doc/coverage/coverage/src/modules/index.html b/doc/coverage/coverage/src/modules/index.html index 3aadc68..f103df9 100644 --- a/doc/coverage/coverage/src/modules/index.html +++ b/doc/coverage/coverage/src/modules/index.html @@ -37,7 +37,7 @@ Date: - 2026-02-16 13:49:21 + 2026-03-18 19:08:29 Functions: 25 diff --git a/doc/coverage/lcov.info b/doc/coverage/lcov.info index 83ac545..b64b64b 100644 --- a/doc/coverage/lcov.info +++ b/doc/coverage/lcov.info @@ -1,64 +1,13 @@ TN: -SF:src/RuleEngine.sol -DA:25,121 -FN:25,RuleEngine.constructor -FNDA:121,RuleEngine.constructor -DA:28,121 -BRDA:28,0,0,1 -DA:29,1 -DA:31,120 -BRDA:31,1,0,30 -DA:32,30 -DA:34,120 -DA:42,129 -FN:42,RuleEngine.hasRole -FNDA:129,RuleEngine.hasRole -DA:43,342 -BRDA:43,2,0,183 -BRDA:43,2,1,159 -DA:44,183 -DA:46,159 -DA:51,4 -FN:51,RuleEngine.supportsInterface -FNDA:4,RuleEngine.supportsInterface -DA:52,4 -DA:53,3 -DA:54,2 -DA:60,15 -FN:60,RuleEngine._onlyComplianceManager -FNDA:15,RuleEngine._onlyComplianceManager -DA:61,168 -FN:61,RuleEngine._onlyRulesManager -FNDA:168,RuleEngine._onlyRulesManager -DA:66,359 -FN:66,RuleEngine._msgSender -FNDA:359,RuleEngine._msgSender -DA:67,359 -DA:73,1 -FN:73,RuleEngine._msgData -FNDA:1,RuleEngine._msgData -DA:74,1 -DA:80,360 -FN:80,RuleEngine._contextSuffixLength -FNDA:360,RuleEngine._contextSuffixLength -DA:81,360 -FNF:8 -FNH:8 -LF:22 -LH:22 -BRF:4 -BRH:4 -end_of_record -TN: SF:src/RuleEngineBase.sol DA:37,5 FN:37,RuleEngineBase.transferred.0 FNDA:5,RuleEngineBase.transferred.0 DA:44,4 -DA:50,19 +DA:50,18 FN:50,RuleEngineBase.transferred.1 -FNDA:19,RuleEngineBase.transferred.1 -DA:56,17 +FNDA:18,RuleEngineBase.transferred.1 +DA:56,16 DA:60,4 FN:60,RuleEngineBase.created FNDA:4,RuleEngineBase.created @@ -107,22 +56,22 @@ DA:163,39 BRDA:163,1,0,29 DA:164,29 DA:167,10 -DA:170,19 -FN:170,RuleEngineBase._messageForTransferRestriction +DA:176,19 +FN:176,RuleEngineBase._messageForTransferRestriction FNDA:19,RuleEngineBase._messageForTransferRestriction -DA:172,19 -DA:173,19 -DA:174,16 -BRDA:174,2,0,14 -DA:175,14 -DA:178,5 -DA:184,215 -FN:184,RuleEngineBase._checkRule -FNDA:215,RuleEngineBase._checkRule -DA:185,215 -DA:186,206 -BRDA:186,3,0,6 -DA:187,6 +DA:177,19 +DA:178,19 +DA:179,16 +BRDA:179,2,0,14 +DA:180,14 +DA:183,5 +DA:189,217 +FN:189,RuleEngineBase._checkRule +FNDA:217,RuleEngineBase._checkRule +DA:190,217 +DA:191,208 +BRDA:191,3,0,6 +DA:192,6 FNF:13 FNH:13 LF:42 @@ -131,11 +80,64 @@ BRF:4 BRH:4 end_of_record TN: -SF:src/RuleEngineOwnable.sol -DA:29,71 +SF:src/deployment/RuleEngine.sol +DA:26,127 +FN:26,RuleEngine.constructor +FNDA:127,RuleEngine.constructor +DA:29,127 +BRDA:29,0,0,1 +DA:30,1 +DA:32,126 +BRDA:32,1,0,31 +DA:33,31 +DA:35,126 +DA:43,135 +FN:43,RuleEngine.hasRole +FNDA:135,RuleEngine.hasRole +DA:44,350 +BRDA:44,2,0,185 +BRDA:44,2,1,165 +DA:45,185 +DA:47,165 +DA:52,6 +FN:52,RuleEngine.supportsInterface +FNDA:6,RuleEngine.supportsInterface +DA:53,6 +DA:54,5 +DA:55,4 +DA:56,3 +DA:57,2 +DA:63,15 +FN:63,RuleEngine._onlyComplianceManager +FNDA:15,RuleEngine._onlyComplianceManager +DA:64,170 +FN:64,RuleEngine._onlyRulesManager +FNDA:170,RuleEngine._onlyRulesManager +DA:69,366 +FN:69,RuleEngine._msgSender +FNDA:366,RuleEngine._msgSender +DA:70,366 +DA:76,1 +FN:76,RuleEngine._msgData +FNDA:1,RuleEngine._msgData +DA:77,1 +DA:83,367 +FN:83,RuleEngine._contextSuffixLength +FNDA:367,RuleEngine._contextSuffixLength +DA:84,367 +FNF:8 +FNH:8 +LF:24 +LH:24 +BRF:4 +BRH:4 +end_of_record +TN: +SF:src/deployment/RuleEngineOwnable.sol +DA:29,77 FN:29,RuleEngineOwnable.constructor -FNDA:71,RuleEngineOwnable.constructor -DA:35,70 +FNDA:77,RuleEngineOwnable.constructor +DA:35,76 BRDA:35,0,0,1 DA:36,1 DA:44,64 @@ -144,28 +146,31 @@ FNDA:64,RuleEngineOwnable._onlyRulesManager DA:49,21 FN:49,RuleEngineOwnable._onlyComplianceManager FNDA:21,RuleEngineOwnable._onlyComplianceManager -DA:52,6 +DA:52,9 FN:52,RuleEngineOwnable.supportsInterface -FNDA:6,RuleEngineOwnable.supportsInterface -DA:53,6 -DA:54,5 -DA:55,2 -DA:65,108 -FN:65,RuleEngineOwnable._msgSender +FNDA:9,RuleEngineOwnable.supportsInterface +DA:53,9 +DA:54,8 +DA:55,7 +DA:56,5 +DA:57,4 +DA:58,3 +DA:68,108 +FN:68,RuleEngineOwnable._msgSender FNDA:108,RuleEngineOwnable._msgSender -DA:66,108 -DA:72,1 -FN:72,RuleEngineOwnable._msgData +DA:69,108 +DA:75,1 +FN:75,RuleEngineOwnable._msgData FNDA:1,RuleEngineOwnable._msgData -DA:73,1 -DA:79,109 -FN:79,RuleEngineOwnable._contextSuffixLength +DA:76,1 +DA:82,109 +FN:82,RuleEngineOwnable._contextSuffixLength FNDA:109,RuleEngineOwnable._contextSuffixLength -DA:80,109 +DA:83,109 FNF:7 FNH:7 -LF:15 -LH:15 +LF:18 +LH:18 BRF:1 BRH:1 end_of_record @@ -179,59 +184,59 @@ DA:34,26 FN:34,ERC3643ComplianceModule.onlyComplianceManager FNDA:26,ERC3643ComplianceModule.onlyComplianceManager DA:35,26 -DA:45,26 -FN:45,ERC3643ComplianceModule.bindToken +DA:50,26 +FN:50,ERC3643ComplianceModule.bindToken FNDA:26,ERC3643ComplianceModule.bindToken -DA:46,25 -DA:50,10 -FN:50,ERC3643ComplianceModule.unbindToken +DA:51,25 +DA:60,10 +FN:60,ERC3643ComplianceModule.unbindToken FNDA:10,ERC3643ComplianceModule.unbindToken -DA:51,9 -DA:55,15 -FN:55,ERC3643ComplianceModule.isTokenBound +DA:61,9 +DA:65,15 +FN:65,ERC3643ComplianceModule.isTokenBound FNDA:15,ERC3643ComplianceModule.isTokenBound -DA:56,15 -DA:60,5 -FN:60,ERC3643ComplianceModule.getTokenBound +DA:66,15 +DA:70,5 +FN:70,ERC3643ComplianceModule.getTokenBound FNDA:5,ERC3643ComplianceModule.getTokenBound -DA:61,5 -BRDA:61,0,0,3 -BRDA:61,0,1,2 -DA:64,3 -DA:66,2 -DA:71,4 -FN:71,ERC3643ComplianceModule.getTokenBounds +DA:71,5 +BRDA:71,0,0,3 +BRDA:71,0,1,2 +DA:74,3 +DA:76,2 +DA:81,4 +FN:81,ERC3643ComplianceModule.getTokenBounds FNDA:4,ERC3643ComplianceModule.getTokenBounds -DA:72,4 -DA:79,9 -FN:79,ERC3643ComplianceModule._unbindToken +DA:82,4 +DA:89,9 +FN:89,ERC3643ComplianceModule._unbindToken FNDA:9,ERC3643ComplianceModule._unbindToken -DA:80,9 -BRDA:80,1,0,2 -BRDA:80,1,1,7 -DA:82,7 -BRDA:82,2,0,- -BRDA:82,2,1,7 -DA:84,7 -DA:87,56 -FN:87,ERC3643ComplianceModule._bindToken -FNDA:56,ERC3643ComplianceModule._bindToken -DA:88,56 -BRDA:88,3,0,2 -BRDA:88,3,1,54 -DA:89,54 -BRDA:89,4,0,2 -BRDA:89,4,1,52 -DA:91,52 -BRDA:91,5,0,- -BRDA:91,5,1,52 -DA:92,52 -DA:95,32 -FN:95,ERC3643ComplianceModule._checkBoundToken -FNDA:32,ERC3643ComplianceModule._checkBoundToken -DA:96,32 -BRDA:96,6,0,7 -DA:97,7 +DA:90,9 +BRDA:90,1,0,2 +BRDA:90,1,1,7 +DA:92,7 +BRDA:92,2,0,- +BRDA:92,2,1,7 +DA:94,7 +DA:97,57 +FN:97,ERC3643ComplianceModule._bindToken +FNDA:57,ERC3643ComplianceModule._bindToken +DA:98,57 +BRDA:98,3,0,2 +BRDA:98,3,1,55 +DA:99,55 +BRDA:99,4,0,2 +BRDA:99,4,1,53 +DA:101,53 +BRDA:101,5,0,- +BRDA:101,5,1,53 +DA:102,53 +DA:105,31 +FN:105,ERC3643ComplianceModule._checkBoundToken +FNDA:31,ERC3643ComplianceModule._checkBoundToken +DA:106,31 +BRDA:106,6,0,7 +DA:107,7 FNF:10 FNH:10 LF:28 @@ -241,97 +246,97 @@ BRH:11 end_of_record TN: SF:src/modules/RulesManagementModule.sol -DA:21,15 -FN:21,RulesManagementModule.onlyRulesManager +DA:16,15 +FN:16,RulesManagementModule.onlyRulesManager FNDA:15,RulesManagementModule.onlyRulesManager -DA:22,15 -DA:42,49 -FN:42,RulesManagementModule.setRules +DA:17,15 +DA:45,49 +FN:45,RulesManagementModule.setRules FNDA:49,RulesManagementModule.setRules -DA:43,47 -BRDA:43,0,0,6 -DA:44,6 -DA:46,41 -BRDA:46,1,0,35 -DA:47,35 +DA:46,47 +BRDA:46,0,0,6 +DA:47,6 DA:49,41 -DA:50,69 -DA:52,64 -BRDA:52,2,0,- -BRDA:52,2,1,64 -DA:53,64 -DA:60,15 -FN:60,RulesManagementModule.clearRules +BRDA:49,1,0,35 +DA:50,35 +DA:52,41 +DA:53,69 +DA:55,64 +BRDA:55,2,0,- +BRDA:55,2,1,64 +DA:56,64 +DA:63,15 +FN:63,RulesManagementModule.clearRules FNDA:15,RulesManagementModule.clearRules -DA:61,13 -DA:67,150 -FN:67,RulesManagementModule.addRule -FNDA:150,RulesManagementModule.addRule -DA:68,146 -DA:69,136 -BRDA:69,3,0,- -BRDA:69,3,1,136 -DA:70,136 -DA:76,18 -FN:76,RulesManagementModule.removeRule +DA:64,13 +DA:73,152 +FN:73,RulesManagementModule.addRule +FNDA:152,RulesManagementModule.addRule +DA:74,148 +DA:75,138 +BRDA:75,3,0,- +BRDA:75,3,1,138 +DA:76,138 +DA:82,18 +FN:82,RulesManagementModule.removeRule FNDA:18,RulesManagementModule.removeRule -DA:77,16 -BRDA:77,4,0,3 -BRDA:77,4,1,13 -DA:78,13 -DA:86,161 -FN:86,RulesManagementModule.rulesCount +DA:83,16 +BRDA:83,4,0,3 +BRDA:83,4,1,13 +DA:84,13 +DA:92,161 +FN:92,RulesManagementModule.rulesCount FNDA:161,RulesManagementModule.rulesCount -DA:87,278 -DA:93,71 -FN:93,RulesManagementModule.containsRule +DA:93,278 +DA:99,71 +FN:99,RulesManagementModule.containsRule FNDA:71,RulesManagementModule.containsRule -DA:94,71 -DA:100,5 -FN:100,RulesManagementModule.rule +DA:100,71 +DA:106,5 +FN:106,RulesManagementModule.rule FNDA:5,RulesManagementModule.rule -DA:101,133 -BRDA:101,5,0,131 -BRDA:101,5,1,2 -DA:104,131 -DA:106,2 -DA:113,15 -FN:113,RulesManagementModule.rules +DA:107,133 +BRDA:107,5,0,131 +BRDA:107,5,1,2 +DA:110,131 +DA:112,2 +DA:119,15 +FN:119,RulesManagementModule.rules FNDA:15,RulesManagementModule.rules -DA:114,15 -DA:124,48 -FN:124,RulesManagementModule._clearRules +DA:120,15 +DA:130,48 +FN:130,RulesManagementModule._clearRules FNDA:48,RulesManagementModule._clearRules -DA:125,48 -DA:126,48 -DA:136,13 -FN:136,RulesManagementModule._removeRule +DA:131,48 +DA:132,48 +DA:142,13 +FN:142,RulesManagementModule._removeRule FNDA:13,RulesManagementModule._removeRule -DA:138,13 -BRDA:138,6,0,- -BRDA:138,6,1,13 -DA:139,13 -DA:145,215 -FN:145,RulesManagementModule._checkRule -FNDA:215,RulesManagementModule._checkRule -DA:146,215 -BRDA:146,7,0,3 -DA:147,3 -DA:149,212 -BRDA:149,8,0,6 -DA:150,6 -DA:163,21 -FN:163,RulesManagementModule._transferred.0 -FNDA:21,RulesManagementModule._transferred.0 -DA:164,21 -DA:165,21 -DA:166,15 -DA:178,4 -FN:178,RulesManagementModule._transferred.1 +DA:144,13 +BRDA:144,6,0,- +BRDA:144,6,1,13 +DA:145,13 +DA:151,217 +FN:151,RulesManagementModule._checkRule +FNDA:217,RulesManagementModule._checkRule +DA:152,217 +BRDA:152,7,0,3 +DA:153,3 +DA:155,214 +BRDA:155,8,0,6 +DA:156,6 +DA:173,20 +FN:173,RulesManagementModule._transferred.0 +FNDA:20,RulesManagementModule._transferred.0 +DA:174,20 +DA:175,20 +DA:176,14 +DA:192,4 +FN:192,RulesManagementModule._transferred.1 FNDA:4,RulesManagementModule._transferred.1 -DA:179,4 -DA:180,4 -DA:181,4 +DA:193,4 +DA:194,4 +DA:195,4 FNF:14 FNH:14 LF:49 diff --git a/doc/security/audits/tools/aderyn-report-feedback.md b/doc/security/audits/tools/aderyn-report-feedback.md new file mode 100644 index 0000000..7211d8d --- /dev/null +++ b/doc/security/audits/tools/aderyn-report-feedback.md @@ -0,0 +1,160 @@ +# Aderyn Report — Assessment Feedback + +**Tool:** [Aderyn](https://github.com/Cyfrin/aderyn) +**Report file:** `aderyn-report.md` +**Codebase version:** v3.0.0-rc2 (14 files, 425 nSLOC) +**Assessment date:** 2026-03-18 + +> Aderyn was run with `aderyn -x mocks`. Findings cover production source only (`src/`). + +--- + +## Summary + +| ID | Finding | Severity | Assessment | +|----|---------|----------|------------| +| L-1 | Centralization Risk | Low | Accepted by design | +| L-2 | Unspecific Solidity Pragma | Low | Accepted by design | +| L-3 | PUSH0 Opcode | Low | Not applicable — EVM target is Prague | +| L-4 | Empty Block | Low | Accepted by design (access-control hook pattern) | +| L-5 | Loop Contains `require`/`revert` | Low | Accepted by design (atomic batch validation) | +| L-6 | Costly Operations Inside Loop | Low | Accepted — unavoidable `SSTORE` in `setRules` | +| L-7 | Unchecked Return | Low | Accepted — return value is irrelevant in constructor | + +No High findings. + +--- + +## L-1: Centralization Risk + +### What Aderyn reports + +6 instances: `AccessControl` + role-gated hooks in `RuleEngine`, and `Ownable` + owner-gated hooks in `RuleEngineOwnable`. + +### Assessment + +**Accepted by design. No action required.** + +The RuleEngine is an administrative compliance contract for tokenized securities. Privileged access control is an intentional and necessary feature: + +- `RuleEngine` uses RBAC (`AccessControl`) with distinct `RULES_MANAGEMENT_ROLE` and `COMPLIANCE_MANAGER_ROLE`, allowing fine-grained delegation to different operators. +- `RuleEngineOwnable` uses ERC-173 `Ownable` for simpler single-owner deployments, as recommended by the ERC-3643 specification. + +Both variants are provided precisely to give deployers a choice of trust model. Centralization at the operator level is expected and required for a compliance tool managing transfer restrictions on regulated assets. + +--- + +## L-2: Unspecific Solidity Pragma + +### What Aderyn reports + +12 instances of `pragma solidity ^0.8.20;` across all source files. + +### Assessment + +**Accepted by design. No action required.** + +The floating `^0.8.20` pragma is intentional. The RuleEngine is designed to be used as a library/dependency by integrators who may compile with different Solidity versions ≥ 0.8.20. Locking to a specific patch version would unnecessarily restrict integrators. + +The project itself always compiles with a pinned version: Solidity `0.8.34` as specified in `foundry.toml` and `hardhat.config.js`. The pragma floor of `0.8.20` captures the minimum required language features (custom errors, EnumerableSet improvements, etc.). + +--- + +## L-3: PUSH0 Opcode + +### What Aderyn reports + +14 instances: `pragma solidity ^0.8.20` may generate `PUSH0` opcodes (introduced in Shanghai), which are unsupported on some chains. + +### Assessment + +**Not applicable. No action required.** + +The project explicitly targets the **Prague EVM** (`evm_version = "prague"` in both `foundry.toml` and `hardhat.config.js`). `PUSH0` was introduced in the Shanghai upgrade (EIP-3855); it is supported by all EVM versions from Shanghai onwards, including Cancun and Prague. Any chain that supports Prague also supports `PUSH0`. + +If the project were ever deployed to a pre-Shanghai chain, this would require attention — but that is not a supported target. + +--- + +## L-4: Empty Block + +### What Aderyn reports + +4 instances: `_onlyComplianceManager()` and `_onlyRulesManager()` in both `RuleEngine` and `RuleEngineOwnable` have empty function bodies. + +### Assessment + +**Accepted by design. No action required.** + +These functions implement the **access-control hook pattern** used throughout the codebase (documented in `CLAUDE.md`). The access control check is enforced entirely by the modifier on the function declaration line — e.g.: + +```solidity +function _onlyRulesManager() internal virtual override onlyOwner {} +``` + +The modifier (`onlyOwner` / `onlyRole(...)`) executes before the empty body. The body is intentionally empty because the entire semantics are carried by the modifier. This pattern is necessary to allow abstract modules to define virtual hooks that concrete contracts override with different access control mechanisms. + +Removing or rewriting these functions would break the hook pattern. + +--- + +## L-5: Loop Contains `require`/`revert` + +### What Aderyn reports + +1 instance: `setRules` loop at `RulesManagementModule.sol` line 57 — `_checkRule` inside the loop can revert. + +### Assessment + +**Accepted by design. No action required.** + +`setRules` is an **atomic batch replacement** operation: it clears the existing rule set and registers a new one in a single transaction. If any rule in the input array is invalid (zero address, duplicate, or fails ERC-165 check), the entire operation must revert to prevent partial registration, which would leave the engine in an inconsistent compliance state. + +The "forgive on fail" pattern suggested by Aderyn (skip invalid entries and return them post-loop) is inappropriate here: silently skipping an invalid rule would give the operator a false impression that all rules were registered when in fact some were not, potentially creating a compliance gap. + +The revert-on-invalid behavior is intentional and correct. + +--- + +## L-6: Costly Operations Inside Loop + +### What Aderyn reports + +1 instance: `setRules` loop at `RulesManagementModule.sol` line 57 — `_rules.add()` performs an `SSTORE`. + +### Assessment + +**Accepted — unavoidable. No action required.** + +The purpose of `setRules` is to persist each rule to storage atomically. `EnumerableSet.add()` must write to storage by definition — there is no way to register rules without `SSTORE`. The suggestion to use a local variable to defer the storage write does not apply here because the goal of the loop body IS the storage write. + +The gas cost is bounded by the number of rules being registered, which is under operator control and bounded by practical constraints (see NatSpec and README warnings on rule-count limits). + +--- + +## L-7: Unchecked Return + +### What Aderyn reports + +1 instance: `_grantRole(DEFAULT_ADMIN_ROLE, admin)` in `RuleEngine` constructor (line 35) — the `bool` return value is ignored. + +### Assessment + +**Accepted — return value is irrelevant in this context. No action required.** + +`AccessControl._grantRole()` (OpenZeppelin v5) returns `true` if the role was newly granted, `false` if the account already held the role. In the constructor, `admin` cannot already hold `DEFAULT_ADMIN_ROLE` (the contract was just deployed; no roles have been assigned yet), so the call always returns `true`. + +Even if somehow `false` were returned, it would not represent an error — `_grantRole` does not revert on a no-op. Checking and branching on this value in a constructor would add meaningless code. + +This pattern (ignoring the return of `_grantRole` in a constructor) is standard across all OpenZeppelin-based contracts. + +--- + +## Overall conclusion + +All 7 Aderyn findings are **accepted**: + +- **L-1, L-2, L-3**: Reflect intentional design choices (privileged compliance model, library-friendly pragma, Prague EVM target). +- **L-4, L-5, L-6, L-7**: Are false-positive patterns generated by Aderyn that do not apply to this codebase's architecture. + +No code changes are required for v3.0.0-rc2 based on this report. diff --git a/doc/security/audits/tools/aderyn-report.md b/doc/security/audits/tools/aderyn-report.md index 4f824c7..d408f9d 100644 --- a/doc/security/audits/tools/aderyn-report.md +++ b/doc/security/audits/tools/aderyn-report.md @@ -11,7 +11,10 @@ This report was generated by [Aderyn](https://github.com/Cyfrin/aderyn), a stati - [L-1: Centralization Risk](#l-1-centralization-risk) - [L-2: Unspecific Solidity Pragma](#l-2-unspecific-solidity-pragma) - [L-3: PUSH0 Opcode](#l-3-push0-opcode) - - [L-4: Loop Contains `require`/`revert`](#l-4-loop-contains-requirerevert) + - [L-4: Empty Block](#l-4-empty-block) + - [L-5: Loop Contains `require`/`revert`](#l-5-loop-contains-requirerevert) + - [L-6: Costly operations inside loop](#l-6-costly-operations-inside-loop) + - [L-7: Unchecked Return](#l-7-unchecked-return) # Summary @@ -20,26 +23,29 @@ This report was generated by [Aderyn](https://github.com/Cyfrin/aderyn), a stati | Key | Value | | --- | --- | -| .sol Files | 11 | -| Total nSLOC | 466 | +| .sol Files | 14 | +| Total nSLOC | 425 | ## Files Details | Filepath | nSLOC | | --- | --- | -| src/RuleEngine.sol | 46 | -| src/RuleEngineBase.sol | 129 | -| src/interfaces/IERC3643Compliance.sol | 16 | -| src/interfaces/IRule.sol | 7 | +| src/RuleEngineBase.sol | 127 | +| src/deployment/RuleEngine.sol | 47 | +| src/deployment/RuleEngineOwnable.sol | 39 | +| src/interfaces/IERC3643Compliance.sol | 13 | +| src/interfaces/IRule.sol | 5 | | src/interfaces/IRulesManagementModule.sol | 12 | | src/modules/ERC2771ModuleStandalone.sol | 6 | -| src/modules/ERC3643ComplianceModule.sol | 72 | -| src/modules/RulesManagementModule.sol | 146 | -| src/modules/VersionModule.sol | 14 | -| src/modules/library/RuleEngineInvariantStorage.sol | 4 | -| src/modules/library/RulesManagementModuleInvariantStorage.sol | 14 | -| **Total** | **466** | +| src/modules/ERC3643ComplianceModule.sol | 58 | +| src/modules/RulesManagementModule.sol | 83 | +| src/modules/VersionModule.sol | 8 | +| src/modules/library/ComplianceInterfaceId.sol | 5 | +| src/modules/library/RuleEngineInvariantStorage.sol | 5 | +| src/modules/library/RuleInterfaceId.sol | 4 | +| src/modules/library/RulesManagementModuleInvariantStorage.sol | 13 | +| **Total** | **425** | ## Issue Summary @@ -47,67 +53,52 @@ This report was generated by [Aderyn](https://github.com/Cyfrin/aderyn), a stati | Category | No. of Issues | | --- | --- | | High | 0 | -| Low | 4 | +| Low | 7 | # Low Issues ## L-1: Centralization Risk -> Acknowledge -> Admin and the different operators are considered as trusted. - Contracts have owners with privileged rights to perform admin tasks and need to be trusted to not perform malicious updates or drain funds. -
8 Found Instances - - -- Found in src/modules/ERC3643ComplianceModule.sol [Line: 11](src/modules/ERC3643ComplianceModule.sol#L11) +
6 Found Instances - ```solidity - abstract contract ERC3643ComplianceModule is IERC3643Compliance, AccessControl { - ``` -- Found in src/modules/ERC3643ComplianceModule.sol [Line: 44](src/modules/ERC3643ComplianceModule.sol#L44) +- Found in src/deployment/RuleEngine.sol [Line: 21](src/deployment/RuleEngine.sol#L21) ```solidity - ) public virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) { + contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase, AccessControl { ``` -- Found in src/modules/ERC3643ComplianceModule.sol [Line: 51](src/modules/ERC3643ComplianceModule.sol#L51) +- Found in src/deployment/RuleEngine.sol [Line: 63](src/deployment/RuleEngine.sol#L63) ```solidity - ) public virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) { + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} ``` -- Found in src/modules/RulesManagementModule.sol [Line: 17](src/modules/RulesManagementModule.sol#L17) +- Found in src/deployment/RuleEngine.sol [Line: 64](src/deployment/RuleEngine.sol#L64) ```solidity - AccessControl, + function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} ``` -- Found in src/modules/RulesManagementModule.sol [Line: 43](src/modules/RulesManagementModule.sol#L43) +- Found in src/deployment/RuleEngineOwnable.sol [Line: 21](src/deployment/RuleEngineOwnable.sol#L21) ```solidity - onlyRole(RULES_MANAGEMENT_ROLE) + contract RuleEngineOwnable is ERC2771ModuleStandalone, RuleEngineBase, Ownable { ``` -- Found in src/modules/RulesManagementModule.sol [Line: 69](src/modules/RulesManagementModule.sol#L69) +- Found in src/deployment/RuleEngineOwnable.sol [Line: 44](src/deployment/RuleEngineOwnable.sol#L44) ```solidity - onlyRole(RULES_MANAGEMENT_ROLE) + function _onlyRulesManager() internal virtual override onlyOwner {} ``` -- Found in src/modules/RulesManagementModule.sol [Line: 83](src/modules/RulesManagementModule.sol#L83) +- Found in src/deployment/RuleEngineOwnable.sol [Line: 49](src/deployment/RuleEngineOwnable.sol#L49) ```solidity - onlyRole(RULES_MANAGEMENT_ROLE) - ``` - -- Found in src/modules/RulesManagementModule.sol [Line: 102](src/modules/RulesManagementModule.sol#L102) - - ```solidity - onlyRole(RULES_MANAGEMENT_ROLE) + function _onlyComplianceManager() internal virtual override onlyOwner {} ```
@@ -116,25 +107,24 @@ Contracts have owners with privileged rights to perform admin tasks and need to ## L-2: Unspecific Solidity Pragma -> One potential use of RuleEngine is to be used as a library, similar to OpenZeppelin library. -> -> In this sense, we use the same convention of OpenZeppelin which for the moment only imposes that the version is higher than 0.8.20: -> pragma solidity ^0.8.20; -> -> A fixed version is set in the config file (0.8.30). Users are free to use these or conduct their own research before switching to another. - Consider using a specific version of Solidity in your contracts instead of a wide version. For example, instead of `pragma solidity ^0.8.0;`, use `pragma solidity 0.8.0;` -
11 Found Instances +
12 Found Instances + +- Found in src/RuleEngineBase.sol [Line: 3](src/RuleEngineBase.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` -- Found in src/RuleEngine.sol [Line: 3](src/RuleEngine.sol#L3) +- Found in src/deployment/RuleEngine.sol [Line: 3](src/deployment/RuleEngine.sol#L3) ```solidity pragma solidity ^0.8.20; ``` -- Found in src/RuleEngineBase.sol [Line: 3](src/RuleEngineBase.sol#L3) +- Found in src/deployment/RuleEngineOwnable.sol [Line: 3](src/deployment/RuleEngineOwnable.sol#L3) ```solidity pragma solidity ^0.8.20; @@ -200,20 +190,24 @@ Consider using a specific version of Solidity in your contracts instead of a wid ## L-3: PUSH0 Opcode -> Acknowledge - Solc compiler version 0.8.20 switches the default target EVM version to Shanghai, which means that the generated bytecode will include PUSH0 opcodes. Be sure to select the appropriate EVM version in case you intend to deploy on a chain other than mainnet like L2 chains that may not support PUSH0, otherwise deployment of your contracts will fail. -
11 Found Instances +
14 Found Instances -- Found in src/RuleEngine.sol [Line: 3](src/RuleEngine.sol#L3) +- Found in src/RuleEngineBase.sol [Line: 3](src/RuleEngineBase.sol#L3) ```solidity pragma solidity ^0.8.20; ``` -- Found in src/RuleEngineBase.sol [Line: 3](src/RuleEngineBase.sol#L3) +- Found in src/deployment/RuleEngine.sol [Line: 3](src/deployment/RuleEngine.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 3](src/deployment/RuleEngineOwnable.sol#L3) ```solidity pragma solidity ^0.8.20; @@ -261,12 +255,24 @@ Solc compiler version 0.8.20 switches the default target EVM version to Shanghai pragma solidity ^0.8.20; ``` +- Found in src/modules/library/ComplianceInterfaceId.sol [Line: 3](src/modules/library/ComplianceInterfaceId.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + - Found in src/modules/library/RuleEngineInvariantStorage.sol [Line: 3](src/modules/library/RuleEngineInvariantStorage.sol#L3) ```solidity pragma solidity ^0.8.20; ``` +- Found in src/modules/library/RuleInterfaceId.sol [Line: 3](src/modules/library/RuleInterfaceId.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + - Found in src/modules/library/RulesManagementModuleInvariantStorage.sol [Line: 3](src/modules/library/RulesManagementModuleInvariantStorage.sol#L3) ```solidity @@ -277,17 +283,66 @@ Solc compiler version 0.8.20 switches the default target EVM version to Shanghai -## L-4: Loop Contains `require`/`revert` +## L-4: Empty Block + +Consider removing empty blocks. + +
4 Found Instances + -> Acknowledge -> The number of configured rules will be probably low (between 1-10) +- Found in src/deployment/RuleEngine.sol [Line: 63](src/deployment/RuleEngine.sol#L63) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/deployment/RuleEngine.sol [Line: 64](src/deployment/RuleEngine.sol#L64) + + ```solidity + function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 44](src/deployment/RuleEngineOwnable.sol#L44) + + ```solidity + function _onlyRulesManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 49](src/deployment/RuleEngineOwnable.sol#L49) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +
+ + + +## L-5: Loop Contains `require`/`revert` Avoid `require` / `revert` statements in a loop because a single bad item can cause the whole transaction to fail. It's better to forgive on fail and return failed elements post processing of the loop
1 Found Instances -- Found in src/modules/RulesManagementModule.sol [Line: 51](src/modules/RulesManagementModule.sol#L51) +- Found in src/modules/RulesManagementModule.sol [Line: 52](src/modules/RulesManagementModule.sol#L52) + + ```solidity + for (uint256 i = 0; i < rules_.length; ++i) { + ``` + +
+ + + +## L-6: Costly operations inside loop + +Invoking `SSTORE` operations in loops may waste gas. Use a local variable to hold the loop computation result. + +
1 Found Instances + + +- Found in src/modules/RulesManagementModule.sol [Line: 52](src/modules/RulesManagementModule.sol#L52) ```solidity for (uint256 i = 0; i < rules_.length; ++i) { @@ -297,3 +352,20 @@ Avoid `require` / `revert` statements in a loop because a single bad item can ca +## L-7: Unchecked Return + +Function returns a value but it is ignored. Consider checking the return value. + +
1 Found Instances + + +- Found in src/deployment/RuleEngine.sol [Line: 35](src/deployment/RuleEngine.sol#L35) + + ```solidity + _grantRole(DEFAULT_ADMIN_ROLE, admin); + ``` + +
+ + + diff --git a/doc/security/audits/tools/nethermind-audit-agent/audit_agent_report_1_v3.0.0-rc1-feedback.md b/doc/security/audits/tools/nethermind-audit-agent/audit_agent_report_1_v3.0.0-rc1-feedback.md new file mode 100644 index 0000000..0bec46a --- /dev/null +++ b/doc/security/audits/tools/nethermind-audit-agent/audit_agent_report_1_v3.0.0-rc1-feedback.md @@ -0,0 +1,287 @@ +# Remediation Assessment — Nethermind AuditAgent Report #1 + +**Report:** `audit_agent_report_1_v3.0.0-rc1.pdf` +**Scan date:** February 20, 2026 +**Assessment date:** March 17, 2026 +**Scope commit range:** `f3e27c19…eb2d2b2c` (main branch, scanned) +**Remediation commits:** see per-finding details below +**Findings:** 7 total — 0 High · 1 Medium · 1 Low · 4 Info · 1 Best Practices + +> **Disclaimer:** This report was generated by an AI-powered automated tool, not a formal human-led audit. Findings are treated as a useful checklist; severity assessments may differ from a manual review. + +--- + +## Finding 1 — Medium: Cross-token rule state pollution + +**File(s):** `src/RuleEngineBase.sol`, `src/modules/ERC3643ComplianceModule.sol`, `src/modules/RulesManagementModule.sol` + +**Remediation commit:** `a4740b2` — `ID-1: docs(compliance) - Add trust warnings` + +### Finding summary + +`destroyed()`, `created()`, and `transferred()` are gated by `onlyBoundToken`, but the token address is never forwarded to rules. In a multi-tenant deployment (multiple bound tokens sharing one engine), a bound token can forge burn/mint/transfer callbacks that corrupt stateful rule state intended for another token. + +### Developer assessment + +**Disagree with severity framing.** The engine correctly enforces `onlyBoundToken`. The exploit requires a bound token to be either malicious or compromised — both are governance/operational failures, not smart contract bugs in the engine. The ERC-3643 standard itself omits the token address from compliance callbacks; this is a standard-level limitation, not a contract defect. Medium severity only applies in deliberately multi-tenant, mutually-untrusting deployments — which is not the primary deployment model. + +The long-term fix (adding a `token` parameter to `IRule` callbacks) is an interface-breaking change requiring CMTAT coordination and is out of scope for this repository alone. + +### Implementation + +Short-term documentation-only mitigation was applied: + +- **`src/interfaces/IERC3643Compliance.sol`** — `bindToken` NatSpec warns that all tokens sharing an engine must be equally trusted and governed together. `unbindToken` NatSpec warns that unbinding does not retroactively isolate previously shared rule state. +- **`src/modules/ERC3643ComplianceModule.sol`** — `bindToken` and `unbindToken` operator warnings repeated on the concrete implementations. +- **`README.md`** — "Contract Variants" section now includes a multi-tenant trust warning. + +No interface or runtime behavior was changed. + +### Verification + +Code check confirmed: +- `IERC3643Compliance.sol` lines 29–34: multi-tenant trust warning present on `bindToken`. ✓ +- `IERC3643Compliance.sol` lines 40–45: retroactive-state warning present on `unbindToken`. ✓ +- `ERC3643ComplianceModule.sol` lines 47–48 and 55–58: `@dev Operator warning` present on both functions. ✓ +- `README.md` line 29: explicit multi-tenant warning block present. ✓ + +**Status: Implemented (doc/NatSpec — short-term). Interface-breaking fix deferred pending CMTAT coordination.** + +--- + +## Finding 2 — Low: RuleEngineOwnable misreports AccessControl interface support + +**File(s):** `src/RuleEngineOwnable.sol` + +**Remediation commit:** `3e030a6` — `ID-2: remove IAccessControl ERC165 advertisement and document Finding 2 implementation` + +### Finding summary + +`RuleEngineOwnable.supportsInterface` previously fell back to `AccessControl.supportsInterface(interfaceId)`, causing it to return `true` for `IAccessControl`. The contract uses `onlyOwner` for all access control — `IAccessControl` methods exist in the ABI but have no effect on privilege, misleading off-chain tooling. + +### Developer assessment + +**Agree.** The `AccessControl` fallback was an oversight introduced by the inheritance chain. Removing it is a one-line fix with no side effects. The ERC-173 interface ID was already explicitly declared. + +### Implementation + +- **`src/RuleEngineOwnable.sol`** — `supportsInterface` rewritten as an explicit whitelist (no `AccessControl.supportsInterface` call): + - `RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID` + - `ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID` + - `ERC173_INTERFACE_ID` (`0x7f5828d0`) + - `ERC3643_COMPLIANCE_INTERFACE_ID` (`0x3144991c`) — added in conjunction with Finding 6 + - `IERC7551_COMPLIANCE_INTERFACE_ID` (`0x7157797f`) — added in conjunction with Finding 6 + - `type(IERC165).interfaceId` +- **`test/RuleEngineOwnable/RuleEngineOwnableCoverage.t.sol`** — added `testDoesNotSupportIAccessControlInterface()` asserting `supportsInterface(type(IAccessControl).interfaceId) == false`. + +### Verification + +Code check confirmed: +- `RuleEngineOwnable.sol` lines 54–61: `supportsInterface` is an explicit whitelist; `AccessControl.supportsInterface` call is absent. ✓ +- `RuleEngineOwnableCoverage.t.sol` line 60–62: negative test for `IAccessControl` interface ID present and correct. ✓ + +**Status: Implemented and verified.** + +--- + +## Finding 3 — Info: Unbounded rules loop can cause permanent DoS + +**File(s):** `src/modules/RulesManagementModule.sol`, `src/RuleEngineBase.sol` + +**Remediation commit:** `1caf4ea` — `Id-3: docs(rules-management): clarify operator-owned rule-count risk and blockchain-dependent gas limits` + +### Finding summary + +`addRule`/`setRules` have no cap on rule count. `_transferred` and `_detectTransferRestriction` loop over all rules per transfer. Adding too many rules can push gas cost above the block limit, permanently freezing token operations. + +### Developer assessment + +**Partially agree.** The risk is real as an operational concern. However, a fixed on-chain maximum would be deployment-dependent and potentially overly restrictive across different blockchains and rule complexities. Operator responsibility is retained; the mitigation is explicit documentation. + +### Implementation + +- **`src/modules/RulesManagementModule.sol`**: + - `setRules`: "No on-chain maximum number of rules is enforced. Operators are responsible for keeping the rule set size compatible with the target chain gas limits." + - `addRule`: "No on-chain maximum number of rules is enforced. Adding too many rules can increase transfer-time gas usage because rule checks are linear in rule count." + - `_transferred` (both overloads): "Complexity is O(number of configured rules). Large rule sets can make transfers too expensive on chains with lower block gas limits." +- **`README.md`** — explicit "How it works" warning paragraph added about gas scaling and absence of an on-chain rule count cap. + +No runtime logic was changed. + +### Verification + +Code check confirmed: +- `RulesManagementModule.sol` line 46: `setRules` NatSpec — gas/cap warning present. ✓ +- `RulesManagementModule.sol` line 75: `addRule` NatSpec — linear gas warning present. ✓ +- `RulesManagementModule.sol` lines 169–173 and 188–192: `_transferred` overloads — O(n) and block-gas warning present. ✓ +- `README.md` line 69: gas-limit warning block present. ✓ + +**Status: Implemented (warnings only — no hard cap by design).** + +--- + +## Finding 4 — Info: Restriction code and message can come from different rules + +**File(s):** `src/RuleEngineBase.sol` + +**Remediation commit:** `3fff502` — `ID-4: docs(restrictions): enforce unique-code convention or identical messages for shared ERC-1404 codes` + +### Finding summary + +`_detectTransferRestriction` returns the first non-zero code from rule N; `_messageForTransferRestriction` returns the message from the first rule claiming that code — potentially a different rule. `EnumerableSet` swap-and-pop ordering makes code/message pairs unstable when rules are added or removed. + +### Developer assessment + +**Partially agree.** This is a rule-design and integration convention issue, not a security vulnerability. The two-pass approach is intentional for gas efficiency. The practical risk is mitigated by requiring unique restriction codes across rules, or identical messages for shared codes. + +### Implementation + +- **`src/interfaces/IRule.sol`** — `canReturnTransferRestrictionCode` NatSpec: "Rule authors should use unique restriction codes across rules when possible. If a code is intentionally shared by multiple rules, all of them should return the same message for that code." +- **`src/RuleEngineBase.sol`** — `_messageForTransferRestriction` comment: documents first-match behavior and the shared-code message requirement. +- **`README.md`** — "restriction code conventions" warning block added. + +No behavior change was made to restriction code/message resolution. + +### Verification + +Code check confirmed: +- `IRule.sol` lines 13–16: unique-code convention documented on `canReturnTransferRestrictionCode`. ✓ +- `RuleEngineBase.sol` lines 172–175: `_messageForTransferRestriction` comment explains first-match behavior and shared-code requirement. ✓ +- `README.md` line 71: restriction code conventions warning present. ✓ + +**Status: Implemented (convention documentation only — no logic change by design).** + +--- + +## Finding 5 — Info: Re-entrant rule can modify rule set during `transferred()` + +**File(s):** `src/RuleEngineBase.sol`, `src/modules/RulesManagementModule.sol` + +**Remediation commit:** `33ff6fa` — `ID5: docs(access-control): warn that rule contracts must never hold RULES_MANAGEMENT_ROLE` + +### Finding summary + +`_transferred` iterates `_rules` while making external calls to each rule with no reentrancy guard. A rule that also holds `RULES_MANAGEMENT_ROLE` could re-enter the engine during iteration, mutating `_rules` mid-loop and potentially skipping rules or panicking. + +### Developer assessment + +**Disagree with exploit framing; agree on governance risk.** The scenario requires granting `RULES_MANAGEMENT_ROLE` to an external rule contract — a severe governance misconfiguration. Rules are trusted logic components; they should never be granted management privileges. The key mitigation is making this constraint explicit in code and documentation. + +### Implementation + +- **`src/modules/RulesManagementModule.sol`**: + - `setRules` and `addRule`: "Security convention: rule contracts should be treated as trusted business logic, but should not also be granted `{RULES_MANAGEMENT_ROLE}`." + - `_transferred` (both overloads): "Security convention: rule contracts are expected to be trusted and must not hold `{RULES_MANAGEMENT_ROLE}`." +- **`README.md`** — "role assignment" warning added: rule contracts must not be granted `RULES_MANAGEMENT_ROLE` or admin privileges. + +No reentrancy guard was added (would add gas overhead to every transfer; not warranted given the trust model). + +### Verification + +Code check confirmed: +- `RulesManagementModule.sol` line 48: `setRules` — role-grant warning present. ✓ +- `RulesManagementModule.sol` line 76: `addRule` — role-grant warning present. ✓ +- `RulesManagementModule.sol` lines 173 and 193: both `_transferred` overloads — must-not-hold-role warning present. ✓ +- `README.md` line 264: role assignment warning block present. ✓ + +**Status: Implemented (warnings only — no reentrancy guard by design).** + +--- + +## Finding 6 — Info: Missing ERC-3643 and IERC7551Compliance interface IDs in `supportsInterface` + +**File(s):** `src/RuleEngine.sol`, `src/RuleEngineOwnable.sol` + +**Remediation commit:** `4ef6cd5` — `ID-6: feat(erc165): advertise ERC-3643 and IERC7551 subset IDs with draft-status docs and coverage tests` + +### Finding summary + +Both contracts implement ERC-3643 compliance entrypoints and `canTransferFrom` (IERC7551Compliance subset), but did not advertise those interface IDs via ERC-165, causing capability-detection queries to incorrectly return `false`. + +### Developer assessment + +**Agree.** Both IDs should be explicitly advertised. ERC-7551 must be documented as draft-status and as a subset interface in this project. + +### Implementation + +- **`src/RuleEngine.sol`**: + - Constants added: `ERC3643_COMPLIANCE_INTERFACE_ID = 0x3144991c`, `IERC7551_COMPLIANCE_INTERFACE_ID = 0x7157797f`. + - `supportsInterface` updated to include both IDs. +- **`src/RuleEngineOwnable.sol`**: + - Same constants and `supportsInterface` update (as part of the Finding 2 rewrite of the explicit whitelist). +- **`test/mocks/ICompliance.sol`** — test-only interface for ERC-3643 `type(ICompliance).interfaceId` check. +- **`test/mocks/IERC7551ComplianceSubset.sol`** — test-only interface for `type(IERC7551ComplianceSubset).interfaceId` check. +- **`test/RuleEngine/RuleEngineCoverage.t.sol`** — added: + - `testSupportsERC3643ComplianceInterface()` + - `testSupportsIERC7551ComplianceSubsetInterface()` +- **`test/RuleEngineOwnable/RuleEngineOwnableCoverage.t.sol`** — added: + - `testSupportsERC3643ComplianceInterface()` + - `testSupportsIERC7551ComplianceSubsetInterface()` +- **`README.md`** — note clarifying ERC-7551 is draft (not final) and that `IERC7551Compliance` is a subset interface. + +### Verification + +Code check confirmed: +- `RuleEngine.sol` lines 21–22: both interface ID constants declared. ✓ +- `RuleEngine.sol` lines 57–58: both IDs included in `supportsInterface`. ✓ +- `RuleEngineOwnable.sol` lines 23–24: both constants declared. ✓ +- `RuleEngineOwnable.sol` lines 58–59: both IDs included in the explicit whitelist. ✓ +- `test/mocks/ICompliance.sol` and `test/mocks/IERC7551ComplianceSubset.sol`: both mock interfaces present. ✓ +- `RuleEngineCoverage.t.sol` lines 42–48: both positive ERC-165 tests present for RBAC variant. ✓ +- `RuleEngineOwnableCoverage.t.sol` lines 48–54: both positive ERC-165 tests present for Ownable variant. ✓ +- `README.md` line 506: draft-status and subset-interface note present. ✓ + +**Status: Implemented and verified.** + +--- + +## Finding 7 — Best Practices: `setRules` does not allow an empty array + +**File(s):** `src/modules/RulesManagementModule.sol` + +**Remediation commit:** `b709d1d` — `ID-7: docs(rules-management): clarify setRules non-empty design and document Finding 7 implementation` + +### Finding summary + +`setRules([])` reverts with `RuleEngine_RulesManagementModule_ArrayIsEmpty`. The report argues this creates an inconsistency — `clearRules()` achieves the same end state — and prevents atomic "replace with empty set" in a single transaction. + +### Developer assessment + +**Disagree with changing the behavior.** The empty-array rejection is intentional: `setRules` is an atomic replacement function for a non-empty rule set; `clearRules` is the explicit function to remove all rules. Allowing `setRules([])` would create semantic overlap and risk accidental mass-clearing. The design is correct; a NatSpec clarification is the appropriate response. + +### Implementation + +- **`src/modules/RulesManagementModule.sol`** — `setRules` NatSpec updated: + - "Replaces the entire rule set atomically." + - "Reverts if `rules_` is empty. Use `{clearRules}` to remove all rules explicitly." + - "To transition from one non-empty set to another without an enforcement gap, call this function directly with the new set." + +No runtime logic was changed; the empty-array check remains. + +### Verification + +Code check confirmed: +- `RulesManagementModule.sol` lines 41–49: `setRules` NatSpec documents the design rationale and refers to `clearRules`. ✓ +- Runtime check at line 51–53: empty-array revert still present. ✓ + +**Status: Implemented (NatSpec clarification only — behavior unchanged by design).** + +--- + +## Summary + +| # | Severity | Finding | Fix type | Commit | Status | +|---|----------|---------|----------|--------|--------| +| 1 | Medium | Cross-token rule state pollution | NatSpec + README warnings | `a4740b2` | Implemented (short-term). Interface fix deferred (CMTAT coordination required). | +| 2 | Low | RuleEngineOwnable misreports IAccessControl | Code fix: explicit ERC-165 whitelist + negative test | `3e030a6` | Implemented and verified. | +| 3 | Info | Unbounded rules loop can cause permanent DoS | NatSpec + README warnings | `1caf4ea` | Implemented (warnings only — no hard cap by design). | +| 4 | Info | Restriction code and message can come from different rules | NatSpec + README convention | `3fff502` | Implemented (convention doc only — no logic change by design). | +| 5 | Info | Re-entrant rule can modify rule set during `transferred()` | NatSpec + README warnings | `33ff6fa` | Implemented (warnings only — no reentrancy guard by design). | +| 6 | Info | Missing ERC-3643 and IERC7551Compliance interface IDs | Code fix: add interface IDs + tests (both variants) | `4ef6cd5` | Implemented and verified. | +| 7 | Best Practices | `setRules` does not allow an empty array | NatSpec clarification | `b709d1d` | Implemented (NatSpec only — behavior unchanged by design). | + +### Notes + +- Findings **2** and **6** are fully resolved in code with test coverage. +- Findings **1**, **3**, **4**, **5**, **7** are mitigated through documentation and NatSpec. No runtime logic was changed; the design decisions are intentional and documented. +- The long-term fix for Finding **1** (adding a `token` parameter to `IRule` callbacks) is tracked as a future coordination item with the CMTAT repository. diff --git a/doc/security/audits/tools/nethermind-audit-agent/audit_agent_report_1_v3.0.0-rc1.pdf b/doc/security/audits/tools/nethermind-audit-agent/audit_agent_report_1_v3.0.0-rc1.pdf new file mode 100644 index 0000000..f020a01 Binary files /dev/null and b/doc/security/audits/tools/nethermind-audit-agent/audit_agent_report_1_v3.0.0-rc1.pdf differ diff --git a/doc/security/audits/tools/slither-report-feedback.md b/doc/security/audits/tools/slither-report-feedback.md new file mode 100644 index 0000000..6c2f217 --- /dev/null +++ b/doc/security/audits/tools/slither-report-feedback.md @@ -0,0 +1,80 @@ +# Slither Report — Assessment Feedback + +**Tool:** [Slither](https://github.com/crytic/slither) +**Report file:** `slither-report.md` +**Codebase version:** v3.0.0-rc2 +**Assessment date:** 2026-03-18 + +> Slither was run with `--show-ignored-findings` suppressed; this checklist reflects only non-ignored findings. + +--- + +## Summary + +| ID | Detector | Impact | Confidence | Assessment | +|----|----------|--------|------------|------------| +| 0–9 | `calls-loop` | Low | Medium | Accepted by design — see below | +| 10–11 | `unindexed-event-address` | Informational | High | Accepted — interface-breaking to fix | + +--- + +## calls-loop (ID-0 to ID-9) — Low / Medium confidence + +### What Slither reports + +Ten instances of external calls inside loops, covering every call path through the rule-dispatch layer: + +| ID | Function | Called from | +|----|----------|-------------| +| 0 | `_messageForTransferRestriction` — `canReturnTransferRestrictionCode` | `messageForTransferRestriction` | +| 1 | `_detectTransferRestrictionFrom` | `detectTransferRestrictionFrom` | +| 2 | `_messageForTransferRestriction` — `messageForTransferRestriction` | `messageForTransferRestriction` | +| 3 | `_transferred(spender, from, to, value)` | `transferred(address,address,address,uint256)` | +| 4 | `_detectTransferRestrictionFrom` | `canTransferFrom` → `detectTransferRestrictionFrom` | +| 5 | `_detectTransferRestriction` | `detectTransferRestriction` | +| 6 | `_transferred(from, to, value)` | `created` | +| 7 | `_detectTransferRestriction` | `canTransfer` → `detectTransferRestriction` | +| 8 | `_transferred(from, to, value)` | `transferred(address,address,uint256)` | +| 9 | `_transferred(from, to, value)` | `destroyed` | + +### Assessment + +**Accepted by design. No action required.** + +The 10 results are all expressions of the same fundamental architecture: the RuleEngine fans out each transfer event to every registered rule contract by iterating `_rules` and making an external call per rule. This is the entire purpose of the contract — there is no way to implement a pluggable rule engine without external calls in a loop. + +The typical concern behind this detector is reentrancy risk or gas griefing from a malicious external callee. Both are addressed: + +- **Reentrancy:** Rule contracts are trusted components added by a privileged operator. Granting management roles to rule contracts is explicitly warned against in NatSpec (see also Nethermind AuditAgent finding 5 remediation). A reentrancy guard on every transfer would add significant gas overhead for no benefit given the trust model. +- **Gas griefing / DoS:** Operators are responsible for keeping the rule set sized for the target chain gas limits. This is documented in NatSpec on `addRule`, `setRules`, and `_transferred`, and warned about in the README (see also Nethermind AuditAgent finding 3 remediation). + +These findings are expected and the pattern is inherent to the design. No code change is needed. + +--- + +## unindexed-event-address (ID-10 to ID-11) — Informational / High confidence + +### What Slither reports + +- `IERC3643Compliance.TokenBound(address token)` — `token` is not indexed. +- `IERC3643Compliance.TokenUnbound(address token)` — `token` is not indexed. + +### Assessment + +**Valid observation. Not fixed — interface-breaking change.** + +Adding `indexed` to the `token` parameter would allow off-chain tooling to filter `TokenBound` / `TokenUnbound` events by token address efficiently using a Bloom filter (topic-based filtering). Without `indexed`, a listener must fetch and decode all events of that type and filter client-side. + +However, adding `indexed` changes the event's ABI signature (the topic hash), which is a breaking change for any off-chain application already listening for these events. + +Given that: +- These events are emitted infrequently (only during administrative `bindToken` / `unbindToken` calls), so the filtering cost is negligible in practice. +- Changing the event signature breaks existing integrations. + +**Decision: accepted as-is.** If the interface is ever revised for another reason, the `indexed` keyword should be added at the same time. + +--- + +## Overall conclusion + +Both finding categories are **accepted by design** and require no code changes for v3.0.0-rc2. The `calls-loop` pattern is inherent to the RuleEngine architecture; the `unindexed-event-address` finding is noted and deferred to a future interface revision. diff --git a/doc/security/audits/tools/slither-report.md b/doc/security/audits/tools/slither-report.md index d5eab1b..75c678b 100644 --- a/doc/security/audits/tools/slither-report.md +++ b/doc/security/audits/tools/slither-report.md @@ -1,49 +1,104 @@ **THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results. Summary - - [calls-loop](#calls-loop) (4 results) (Low) - - [dead-code](#dead-code) (1 results) (Informational) + - [calls-loop](#calls-loop) (10 results) (Low) + - [unindexed-event-address](#unindexed-event-address) (2 results) (Informational) ## calls-loop - -> Acknowledge -> Rule contracts are considered as trusted - Impact: Low Confidence: Medium - [ ] ID-0 -[RuleEngineBase.detectTransferRestriction(address,address,uint256)](src/RuleEngineBase.sol#L76-L90) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestriction(from,to,value)](src/RuleEngineBase.sol#L83-L84) +[RuleEngineBase._messageForTransferRestriction(uint8)](src/RuleEngineBase.sol#L176-L184) has external calls inside a loop: [IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)](src/RuleEngineBase.sol#L179) + Calls stack containing the loop: + RuleEngineBase.messageForTransferRestriction(uint8) -src/RuleEngineBase.sol#L76-L90 +src/RuleEngineBase.sol#L176-L184 - [ ] ID-1 -[RuleEngineBase.messageForTransferRestriction(uint8)](src/RuleEngineBase.sol#L116-L132) has external calls inside a loop: [IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)](src/RuleEngineBase.sol#L123-L124) +[RuleEngineBase._detectTransferRestrictionFrom(address,address,address,uint256)](src/RuleEngineBase.sol#L154-L168) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestrictionFrom(spender,from,to,value)](src/RuleEngineBase.sol#L162) + Calls stack containing the loop: + RuleEngineBase.detectTransferRestrictionFrom(address,address,address,uint256) -src/RuleEngineBase.sol#L116-L132 +src/RuleEngineBase.sol#L154-L168 - [ ] ID-2 -[RuleEngineBase.messageForTransferRestriction(uint8)](src/RuleEngineBase.sol#L116-L132) has external calls inside a loop: [IRule(rule(i)).messageForTransferRestriction(restrictionCode)](src/RuleEngineBase.sol#L126-L128) +[RuleEngineBase._messageForTransferRestriction(uint8)](src/RuleEngineBase.sol#L176-L184) has external calls inside a loop: [IRule(rule(i)).messageForTransferRestriction(restrictionCode)](src/RuleEngineBase.sol#L180) + Calls stack containing the loop: + RuleEngineBase.messageForTransferRestriction(uint8) -src/RuleEngineBase.sol#L116-L132 +src/RuleEngineBase.sol#L176-L184 - [ ] ID-3 -[RuleEngineBase.detectTransferRestrictionFrom(address,address,address,uint256)](src/RuleEngineBase.sol#L95-L111) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestrictionFrom(spender,from,to,value)](src/RuleEngineBase.sol#L103-L104) +[RulesManagementModule._transferred(address,address,address,uint256)](src/modules/RulesManagementModule.sol#L192-L197) has external calls inside a loop: [IRule(_rules.at(i)).transferred(spender,from,to,value)](src/modules/RulesManagementModule.sol#L195) + Calls stack containing the loop: + RuleEngineBase.transferred(address,address,address,uint256) + +src/modules/RulesManagementModule.sol#L192-L197 + + + - [ ] ID-4 +[RuleEngineBase._detectTransferRestrictionFrom(address,address,address,uint256)](src/RuleEngineBase.sol#L154-L168) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestrictionFrom(spender,from,to,value)](src/RuleEngineBase.sol#L162) + Calls stack containing the loop: + RuleEngineBase.canTransferFrom(address,address,address,uint256) + RuleEngineBase.detectTransferRestrictionFrom(address,address,address,uint256) + +src/RuleEngineBase.sol#L154-L168 + + + - [ ] ID-5 +[RuleEngineBase._detectTransferRestriction(address,address,uint256)](src/RuleEngineBase.sol#L143-L152) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestriction(from,to,value)](src/RuleEngineBase.sol#L146) + Calls stack containing the loop: + RuleEngineBase.detectTransferRestriction(address,address,uint256) + +src/RuleEngineBase.sol#L143-L152 + + + - [ ] ID-6 +[RulesManagementModule._transferred(address,address,uint256)](src/modules/RulesManagementModule.sol#L173-L178) has external calls inside a loop: [IRule(_rules.at(i)).transferred(from,to,value)](src/modules/RulesManagementModule.sol#L176) + Calls stack containing the loop: + RuleEngineBase.created(address,uint256) + +src/modules/RulesManagementModule.sol#L173-L178 -src/RuleEngineBase.sol#L95-L111 -## dead-code + - [ ] ID-7 +[RuleEngineBase._detectTransferRestriction(address,address,uint256)](src/RuleEngineBase.sol#L143-L152) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestriction(from,to,value)](src/RuleEngineBase.sol#L146) + Calls stack containing the loop: + RuleEngineBase.canTransfer(address,address,uint256) + RuleEngineBase.detectTransferRestriction(address,address,uint256) -> - Implemented to be gasless compatible (see MetaTxModule) -> -> - If we remove this function, we will have the following error: -> -> "Derived contract must override function "_msgData". Two or more base classes define function with same name and parameter types." +src/RuleEngineBase.sol#L143-L152 + + - [ ] ID-8 +[RulesManagementModule._transferred(address,address,uint256)](src/modules/RulesManagementModule.sol#L173-L178) has external calls inside a loop: [IRule(_rules.at(i)).transferred(from,to,value)](src/modules/RulesManagementModule.sol#L176) + Calls stack containing the loop: + RuleEngineBase.transferred(address,address,uint256) + +src/modules/RulesManagementModule.sol#L173-L178 + + + - [ ] ID-9 +[RulesManagementModule._transferred(address,address,uint256)](src/modules/RulesManagementModule.sol#L173-L178) has external calls inside a loop: [IRule(_rules.at(i)).transferred(from,to,value)](src/modules/RulesManagementModule.sol#L176) + Calls stack containing the loop: + RuleEngineBase.destroyed(address,uint256) + +src/modules/RulesManagementModule.sol#L173-L178 + + +## unindexed-event-address Impact: Informational -Confidence: Medium - - [ ] ID-4 -[RuleEngine._msgData()](src/RuleEngine.sol#L56-L64) is never used and should be removed +Confidence: High + - [ ] ID-10 +Event [IERC3643Compliance.TokenBound(address)](src/interfaces/IERC3643Compliance.sol#L14) has address parameters but no indexed parameters + +src/interfaces/IERC3643Compliance.sol#L14 + + + - [ ] ID-11 +Event [IERC3643Compliance.TokenUnbound(address)](src/interfaces/IERC3643Compliance.sol#L20) has address parameters but no indexed parameters + +src/interfaces/IERC3643Compliance.sol#L20 -src/RuleEngine.sol#L56-L64 diff --git a/doc/specification/README.pdf b/doc/specification/README.pdf new file mode 100644 index 0000000..7ea2e38 Binary files /dev/null and b/doc/specification/README.pdf differ diff --git a/doc/specification/RuleEngine_specificationv3.0.0-rc0.odg b/doc/specification/RuleEngine_specificationv3.0.0-rc0.odg index 5d0e7d0..c164da2 100644 Binary files a/doc/specification/RuleEngine_specificationv3.0.0-rc0.odg and b/doc/specification/RuleEngine_specificationv3.0.0-rc0.odg differ diff --git a/doc/specification/RuleEngine_specificationv3.0.0-rc1.pdf b/doc/specification/RuleEngine_specificationv3.0.0-rc1.pdf new file mode 100644 index 0000000..66c8188 Binary files /dev/null and b/doc/specification/RuleEngine_specificationv3.0.0-rc1.pdf differ diff --git a/doc/specification/cover_page.pdf b/doc/specification/cover_page.pdf new file mode 100644 index 0000000..45822f4 Binary files /dev/null and b/doc/specification/cover_page.pdf differ diff --git a/foundry.toml b/foundry.toml index 621aac1..6c528c9 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -solc = "0.8.33" +solc = "0.8.34" src = 'src' out = 'out' libs = ['lib'] diff --git a/hardhat.config.js b/hardhat.config.js index 56c8008..35d92be 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -1,7 +1,7 @@ /** @type import('hardhat/config').HardhatUserConfig */ require("@nomicfoundation/hardhat-foundry"); module.exports = { - solidity: "0.8.33", + solidity: "0.8.34", settings: { optimizer: { enabled: true, diff --git a/lib/CMTAT b/lib/CMTAT index 78e6e48..49544f4 160000 --- a/lib/CMTAT +++ b/lib/CMTAT @@ -1 +1 @@ -Subproject commit 78e6e48e491e01e853e6490e061189875d4785ea +Subproject commit 49544f4de1993008acfc9e848d0bf03bd31d8579 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index fcbae53..56a3de2 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit fcbae5394ae8ad52d8e580a3477db99814b9d565 +Subproject commit 56a3de2cea907c9a500d32e70c275f68393b7ba6 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index aa677e9..c9f48f0 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit aa677e9d28ed78fc427ec47ba2baef2030c58e7c +Subproject commit c9f48f0d6c470a3edab20dcbb3cc35dcfc7f4f1f diff --git a/script/CMTATWithRuleEngineScript.s.sol b/script/CMTATWithRuleEngineScript.s.sol index f1eb618..9302135 100644 --- a/script/CMTATWithRuleEngineScript.s.sol +++ b/script/CMTATWithRuleEngineScript.s.sol @@ -8,7 +8,7 @@ import {Script, console} from "forge-std/Script.sol"; import {ICMTATConstructor, CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; -import {RuleEngine} from "src/RuleEngine.sol"; +import {RuleEngine} from "src/deployment/RuleEngine.sol"; import {RuleWhitelist} from "src/mocks/rules/validation/RuleWhitelist.sol"; /** diff --git a/script/RuleEngineScript.s.sol b/script/RuleEngineScript.s.sol index b0e7d61..de1eb0d 100644 --- a/script/RuleEngineScript.s.sol +++ b/script/RuleEngineScript.s.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.17; import {Script, console} from "forge-std/Script.sol"; -import {RuleEngine} from "src/RuleEngine.sol"; +import {RuleEngine} from "src/deployment/RuleEngine.sol"; import {RuleWhitelist} from "src/mocks/rules/validation/RuleWhitelist.sol"; import { ValidationModuleRuleEngine diff --git a/src/RuleEngineBase.sol b/src/RuleEngineBase.sol index 1b10c58..120b945 100644 --- a/src/RuleEngineBase.sol +++ b/src/RuleEngineBase.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.20; /* ==== OpenZeppelin === */ import {ERC165Checker} from "OZ/utils/introspection/ERC165Checker.sol"; +/* ==== CMTAT interface IDs === */ +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; /* ==== CMTAT === */ import {IRuleEngine, IRuleEngineERC1404} from "CMTAT/interfaces/engine/IRuleEngine.sol"; import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; @@ -17,6 +20,7 @@ import {RulesManagementModule} from "./modules/RulesManagementModule.sol"; /* ==== Interface and other library === */ import {IRule} from "./interfaces/IRule.sol"; +import {ComplianceInterfaceId} from "./modules/library/ComplianceInterfaceId.sol"; import {RuleEngineInvariantStorage} from "./modules/library/RuleEngineInvariantStorage.sol"; import {RuleInterfaceId} from "./modules/library/RuleInterfaceId.sol"; @@ -167,8 +171,13 @@ abstract contract RuleEngineBase is return uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @dev This function returns the message from the first rule claiming the code. + * Rule designers should keep restriction codes unique across rules. + * If a code is shared intentionally, all rules using that code should return + * the same message to avoid ambiguous operator feedback. + */ function _messageForTransferRestriction(uint8 restrictionCode) internal view virtual returns (string memory) { - // uint256 rulesLength = rulesCount(); for (uint256 i = 0; i < rulesLength; ++i) { if (IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)) { @@ -187,4 +196,15 @@ abstract contract RuleEngineBase is revert RuleEngine_RuleInvalidInterface(); } } + + /** + * @dev Shared ERC-165 checks common to all RuleEngine deployment variants. + * Concrete deployments can extend this with access-control-specific interfaces. + */ + function _supportsRuleEngineBaseInterface(bytes4 interfaceId) internal pure returns (bool) { + return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID + || interfaceId == ComplianceInterfaceId.IERC7551_COMPLIANCE_INTERFACE_ID; + } } diff --git a/src/RuleEngineOwnable.sol b/src/RuleEngineOwnable.sol deleted file mode 100644 index f926f0d..0000000 --- a/src/RuleEngineOwnable.sol +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -pragma solidity ^0.8.20; - -/* ==== CMTAT === */ -import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; -import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; -/* ==== OpenZeppelin === */ -import {Context} from "OZ/utils/Context.sol"; -import {Ownable} from "OZ/access/Ownable.sol"; -import {IERC165} from "OZ/utils/introspection/IERC165.sol"; -import {AccessControl} from "OZ/access/AccessControl.sol"; -/* ==== Modules === */ -import {ERC2771ModuleStandalone, ERC2771Context} from "./modules/ERC2771ModuleStandalone.sol"; -/* ==== Base contract === */ -import {RuleEngineBase} from "./RuleEngineBase.sol"; - -/** - * @title Implementation of a ruleEngine with ERC-173 Ownable access control - */ -contract RuleEngineOwnable is ERC2771ModuleStandalone, RuleEngineBase, Ownable { - bytes4 private constant ERC173_INTERFACE_ID = 0x7f5828d0; - - /** - * @param owner_ Address of the contract owner (ERC-173) - * @param forwarderIrrevocable Address of the forwarder, required for the gasless support - * @param tokenContract Address of the token contract to bind (can be zero address) - */ - constructor(address owner_, address forwarderIrrevocable, address tokenContract) - ERC2771ModuleStandalone(forwarderIrrevocable) - Ownable(owner_) - { - // Note: zero-address check for owner_ is handled by Ownable(owner_), - // which reverts with OwnableInvalidOwner(address(0)) before reaching here. - if (tokenContract != address(0)) { - _bindToken(tokenContract); - } - } - - /* ============ ACCESS CONTROL ============ */ - /** - * @dev Access control check using Ownable pattern - */ - function _onlyRulesManager() internal virtual override onlyOwner {} - - /** - * @dev Access control check using Ownable pattern - */ - function _onlyComplianceManager() internal virtual override onlyOwner {} - - /* ============ ERC-165 ============ */ - function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, IERC165) returns (bool) { - return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID - || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID || interfaceId == ERC173_INTERFACE_ID - || AccessControl.supportsInterface(interfaceId); - } - - /*////////////////////////////////////////////////////////////// - ERC-2771 - //////////////////////////////////////////////////////////////*/ - - /** - * @dev This surcharge is not necessary if you do not use the MetaTxModule - */ - function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { - return ERC2771Context._msgSender(); - } - - /** - * @dev This surcharge is not necessary if you do not use the MetaTxModule - */ - function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { - return ERC2771Context._msgData(); - } - - /** - * @dev This surcharge is not necessary if you do not use the MetaTxModule - */ - function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { - return ERC2771Context._contextSuffixLength(); - } -} diff --git a/src/RuleEngineOwnableShared.sol b/src/RuleEngineOwnableShared.sol new file mode 100644 index 0000000..f331fd2 --- /dev/null +++ b/src/RuleEngineOwnableShared.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/* ==== OpenZeppelin === */ +import {Context} from "OZ/utils/Context.sol"; +import {IERC165} from "OZ/utils/introspection/IERC165.sol"; +/* ==== Modules === */ +import {ERC2771ModuleStandalone, ERC2771Context} from "./modules/ERC2771ModuleStandalone.sol"; +/* ==== Base contract === */ +import {RuleEngineBase} from "./RuleEngineBase.sol"; + +/** + * @title Shared Ownable deployment logic for RuleEngine variants + * @dev Kept abstract to let child contracts choose the ownership mechanism + * (`Ownable` or `Ownable2Step`) while reusing constructor, ERC-165 and ERC-2771 code. + */ +abstract contract RuleEngineOwnableShared is ERC2771ModuleStandalone, RuleEngineBase { + bytes4 private constant ERC173_INTERFACE_ID = 0x7f5828d0; + + constructor(address forwarderIrrevocable, address tokenContract) ERC2771ModuleStandalone(forwarderIrrevocable) { + if (tokenContract != address(0)) { + _bindToken(tokenContract); + } + } + + /* ============ ERC-165 ============ */ + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return _supportsRuleEngineBaseInterface(interfaceId) + || interfaceId == ERC173_INTERFACE_ID + || interfaceId == type(IERC165).interfaceId; + } + + /*////////////////////////////////////////////////////////////// + ERC-2771 + //////////////////////////////////////////////////////////////*/ + + /** + * @dev This surcharge is not necessary if you do not use the MetaTxModule + */ + function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { + return ERC2771Context._msgSender(); + } + + /** + * @dev This surcharge is not necessary if you do not use the MetaTxModule + */ + function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { + return ERC2771Context._msgData(); + } + + /** + * @dev This surcharge is not necessary if you do not use the MetaTxModule + */ + function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { + return ERC2771Context._contextSuffixLength(); + } +} diff --git a/src/RuleEngine.sol b/src/deployment/RuleEngine.sol similarity index 83% rename from src/RuleEngine.sol rename to src/deployment/RuleEngine.sol index bc54f18..f452e52 100644 --- a/src/RuleEngine.sol +++ b/src/deployment/RuleEngine.sol @@ -2,22 +2,19 @@ pragma solidity ^0.8.20; -/* ==== CMTAT === */ -import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; -import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; /* ==== OpenZeppelin === */ import {Context} from "OZ/utils/Context.sol"; import {AccessControl} from "OZ/access/AccessControl.sol"; import {IERC165} from "OZ/utils/introspection/ERC165.sol"; /* ==== Modules === */ -import {ERC2771ModuleStandalone, ERC2771Context} from "./modules/ERC2771ModuleStandalone.sol"; +import {ERC2771ModuleStandalone, ERC2771Context} from "../modules/ERC2771ModuleStandalone.sol"; /* ==== Base contract === */ -import {RuleEngineBase} from "./RuleEngineBase.sol"; +import {RuleEngineBase} from "../RuleEngineBase.sol"; /** * @title Implementation of a ruleEngine as defined by the CMTAT */ -contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase { +contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase, AccessControl { /** * @param admin Address of the contract (Access Control) * @param forwarderIrrevocable Address of the forwarder, required for the gasless support @@ -49,8 +46,7 @@ contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase { /* ============ ERC-165 ============ */ function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, IERC165) returns (bool) { - return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID - || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + return _supportsRuleEngineBaseInterface(interfaceId) || AccessControl.supportsInterface(interfaceId); } diff --git a/src/deployment/RuleEngineOwnable.sol b/src/deployment/RuleEngineOwnable.sol new file mode 100644 index 0000000..df5c32d --- /dev/null +++ b/src/deployment/RuleEngineOwnable.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +import {Ownable} from "OZ/access/Ownable.sol"; +import {Context} from "OZ/utils/Context.sol"; +import {RuleEngineOwnableShared} from "../RuleEngineOwnableShared.sol"; + +/** + * @title Implementation of a ruleEngine with ERC-173 Ownable access control + */ +contract RuleEngineOwnable is RuleEngineOwnableShared, Ownable { + /** + * @param owner_ Address of the contract owner (ERC-173) + * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + * @param tokenContract Address of the token contract to bind (can be zero address) + */ + constructor(address owner_, address forwarderIrrevocable, address tokenContract) + RuleEngineOwnableShared(forwarderIrrevocable, tokenContract) + Ownable(owner_) + {} + + /* ============ ACCESS CONTROL ============ */ + /** + * @dev Access control check using Ownable pattern + */ + function _onlyRulesManager() internal virtual override onlyOwner {} + + /** + * @dev Access control check using Ownable pattern + */ + function _onlyComplianceManager() internal virtual override onlyOwner {} + + /** + * @dev This surcharge is not necessary if you do not use the MetaTxModule + */ + function _msgSender() internal view virtual override(RuleEngineOwnableShared, Context) returns (address sender) { + return RuleEngineOwnableShared._msgSender(); + } + + /** + * @dev This surcharge is not necessary if you do not use the MetaTxModule + */ + function _msgData() internal view virtual override(RuleEngineOwnableShared, Context) returns (bytes calldata) { + return RuleEngineOwnableShared._msgData(); + } + + /** + * @dev This surcharge is not necessary if you do not use the MetaTxModule + */ + function _contextSuffixLength() internal view virtual override(RuleEngineOwnableShared, Context) returns (uint256) { + return RuleEngineOwnableShared._contextSuffixLength(); + } + +} diff --git a/src/deployment/RuleEngineOwnable2Step.sol b/src/deployment/RuleEngineOwnable2Step.sol new file mode 100644 index 0000000..a37415b --- /dev/null +++ b/src/deployment/RuleEngineOwnable2Step.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/* ==== OpenZeppelin === */ +import {Ownable2Step} from "OZ/access/Ownable2Step.sol"; +import {Ownable} from "OZ/access/Ownable.sol"; +import {Context} from "OZ/utils/Context.sol"; +import {RuleEngineOwnableShared} from "../RuleEngineOwnableShared.sol"; + +/** + * @title Implementation of a ruleEngine with ERC-173 Ownable2Step access control + */ +contract RuleEngineOwnable2Step is RuleEngineOwnableShared, Ownable2Step { + /** + * @param owner_ Address of the contract owner (ERC-173) + * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + * @param tokenContract Address of the token contract to bind (can be zero address) + */ + constructor(address owner_, address forwarderIrrevocable, address tokenContract) + RuleEngineOwnableShared(forwarderIrrevocable, tokenContract) + Ownable(owner_) + {} + + /* ============ ACCESS CONTROL ============ */ + /** + * @dev Access control check using Ownable pattern + */ + function _onlyRulesManager() internal virtual override onlyOwner {} + + /** + * @dev Access control check using Ownable pattern + */ + function _onlyComplianceManager() internal virtual override onlyOwner {} + + /** + * @dev This surcharge is not necessary if you do not use the MetaTxModule + */ + function _msgSender() internal view virtual override(RuleEngineOwnableShared, Context) returns (address sender) { + return RuleEngineOwnableShared._msgSender(); + } + + /** + * @dev This surcharge is not necessary if you do not use the MetaTxModule + */ + function _msgData() internal view virtual override(RuleEngineOwnableShared, Context) returns (bytes calldata) { + return RuleEngineOwnableShared._msgData(); + } + + /** + * @dev This surcharge is not necessary if you do not use the MetaTxModule + */ + function _contextSuffixLength() internal view virtual override(RuleEngineOwnableShared, Context) returns (uint256) { + return RuleEngineOwnableShared._contextSuffixLength(); + } +} diff --git a/src/interfaces/IERC3643Compliance.sol b/src/interfaces/IERC3643Compliance.sol index 39189a4..eb965de 100644 --- a/src/interfaces/IERC3643Compliance.sol +++ b/src/interfaces/IERC3643Compliance.sol @@ -24,6 +24,11 @@ interface IERC3643Compliance is IERC3643ComplianceRead, IERC3643IComplianceContr * @notice Associates a token contract with this compliance contract. * @dev The compliance contract may restrict operations on the bound token * according to the compliance logic. + * Security note: a "multi-tenant" setup means multiple token contracts + * share one RuleEngine instance (all are bound via {bindToken}). + * In that setup, all bound tokens must be equally trusted and governed together. + * ERC-3643 callbacks do not carry the token address to rules, so stateful + * rules with per-address accounting are unsafe across mutually untrusted tokens. * Reverts if the token is already bound. * Complexity: O(1). * @param token The address of the token to bind. @@ -32,7 +37,11 @@ interface IERC3643Compliance is IERC3643ComplianceRead, IERC3643IComplianceContr /** * @notice Removes the association of a token contract from this compliance contract. - * @dev Reverts if the token is not currently bound. + * @dev Security note: unbinding does not retroactively isolate rule state from + * previously shared multi-token operation. "Multi-tenant" means one RuleEngine + * shared by multiple token contracts. Avoid multi-tenant binding unless + * all tokens are equally trusted and governed together. + * Reverts if the token is not currently bound. * Complexity: O(1). * @param token The address of the token to unbind. */ diff --git a/src/interfaces/IRule.sol b/src/interfaces/IRule.sol index bebf961..86554b9 100644 --- a/src/interfaces/IRule.sol +++ b/src/interfaces/IRule.sol @@ -10,6 +10,9 @@ import {IRuleEngineERC1404} from "CMTAT/interfaces/engine/IRuleEngine.sol"; interface IRule is IRuleEngineERC1404 { /** * @dev Returns true if the restriction code exists, and false otherwise. + * Rule authors should use unique restriction codes across rules when possible. + * If a code is intentionally shared by multiple rules, all of them should return + * the same message for that code in `messageForTransferRestriction`. */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external view returns (bool); } diff --git a/src/mocks/ICompliance.sol b/src/mocks/ICompliance.sol new file mode 100644 index 0000000..d2b8902 --- /dev/null +++ b/src/mocks/ICompliance.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +interface ICompliance { + // events + event TokenBound(address _token); + event TokenUnbound(address _token); + + // functions + // initialization of the compliance contract + function bindToken(address _token) external; + function unbindToken(address _token) external; + + // check the parameters of the compliance contract + function isTokenBound(address _token) external view returns (bool); + function getTokenBound() external view returns (address); + + // compliance check and state update + function canTransfer(address _from, address _to, uint256 _amount) external view returns (bool); + function transferred(address _from, address _to, uint256 _amount) external; + function created(address _to, uint256 _amount) external; + function destroyed(address _from, uint256 _amount) external; +} diff --git a/src/mocks/IERC7551ComplianceSubset.sol b/src/mocks/IERC7551ComplianceSubset.sol new file mode 100644 index 0000000..8c5f46a --- /dev/null +++ b/src/mocks/IERC7551ComplianceSubset.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +/** + * @dev Test-only subset interface used to validate the advertised ERC-165 ID. + * ERC-7551 is draft and this interface contains only the compliance method + * currently implemented by RuleEngine. + */ +interface IERC7551ComplianceSubset { + function canTransferFrom(address spender, address from, address to, uint256 value) external view returns (bool); +} diff --git a/src/mocks/RuleEngineExposed.sol b/src/mocks/RuleEngineExposed.sol index cb1a773..948dd0f 100644 --- a/src/mocks/RuleEngineExposed.sol +++ b/src/mocks/RuleEngineExposed.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; -import {RuleEngine} from "../RuleEngine.sol"; -import {RuleEngineOwnable} from "../RuleEngineOwnable.sol"; +import {RuleEngine} from "../deployment/RuleEngine.sol"; +import {RuleEngineOwnable} from "../deployment/RuleEngineOwnable.sol"; /** * @title RuleEngineExposed diff --git a/src/mocks/rules/operation/RuleConditionalTransferLight.sol b/src/mocks/rules/operation/RuleConditionalTransferLight.sol index ce5c846..a3e80e7 100644 --- a/src/mocks/rules/operation/RuleConditionalTransferLight.sol +++ b/src/mocks/rules/operation/RuleConditionalTransferLight.sol @@ -7,7 +7,8 @@ import { } from "./abstract/RuleConditionalTransferLightInvariantStorage.sol"; import {IRule} from "../../../interfaces/IRule.sol"; import {RuleInterfaceId} from "../../../modules/library/RuleInterfaceId.sol"; -import {IERC165, AccessControl} from "OZ/access/AccessControl.sol"; +import {AccessControl} from "OZ/access/AccessControl.sol"; +import {IERC165} from "OZ/utils/introspection/IERC165.sol"; /** * @title TransferApprovalRule diff --git a/src/mocks/rules/operation/RuleOperationRevert.sol b/src/mocks/rules/operation/RuleOperationRevert.sol index ed62f25..9c89f56 100644 --- a/src/mocks/rules/operation/RuleOperationRevert.sol +++ b/src/mocks/rules/operation/RuleOperationRevert.sol @@ -5,7 +5,8 @@ pragma solidity ^0.8.20; import "../validation/abstract/RuleCommonInvariantStorage.sol"; import {IRule} from "../../../interfaces/IRule.sol"; import {RuleInterfaceId} from "../../../modules/library/RuleInterfaceId.sol"; -import {IERC165, AccessControl} from "OZ/access/AccessControl.sol"; +import {AccessControl} from "OZ/access/AccessControl.sol"; +import {IERC165} from "OZ/utils/introspection/IERC165.sol"; /** * @title TransferApprovalRule diff --git a/src/mocks/rules/validation/RuleWhitelist.sol b/src/mocks/rules/validation/RuleWhitelist.sol index 7f67cb2..897ef0b 100644 --- a/src/mocks/rules/validation/RuleWhitelist.sol +++ b/src/mocks/rules/validation/RuleWhitelist.sol @@ -6,7 +6,8 @@ pragma solidity ^0.8.20; import "./abstract/RuleAddressList/RuleAddressList.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "./abstract/RuleWhitelistCommon.sol"; -import {IERC165, AccessControl} from "OZ/access/AccessControl.sol"; +import {AccessControl} from "OZ/access/AccessControl.sol"; +import {IERC165} from "OZ/utils/introspection/IERC165.sol"; import {RuleInterfaceId} from "../../../modules/library/RuleInterfaceId.sol"; //import {ERC165, IERC165} from "@OZ/utils/introspection/ERC165.sol"; diff --git a/src/modules/ERC3643ComplianceModule.sol b/src/modules/ERC3643ComplianceModule.sol index 43b429c..a6b4f0c 100644 --- a/src/modules/ERC3643ComplianceModule.sol +++ b/src/modules/ERC3643ComplianceModule.sol @@ -41,12 +41,22 @@ abstract contract ERC3643ComplianceModule is Context, IERC3643Compliance { //////////////////////////////////////////////////////////////*/ /* ============ State functions ============ */ - /// @inheritdoc IERC3643Compliance + /** + * @inheritdoc IERC3643Compliance + * @dev Operator warning: "multi-tenant" means one RuleEngine is shared by + * multiple token contracts. In that setup, bind only tokens that are equally + * trusted and governed together. + */ function bindToken(address token) public virtual override onlyComplianceManager { _bindToken(token); } - /// @inheritdoc IERC3643Compliance + /** + * @inheritdoc IERC3643Compliance + * @dev Operator warning: unbinding is an administrative operation and does not + * erase any state already stored by external rule contracts in a previously + * shared ("multi-tenant") setup. + */ function unbindToken(address token) public virtual override onlyComplianceManager { _unbindToken(token); } diff --git a/src/modules/RulesManagementModule.sol b/src/modules/RulesManagementModule.sol index e37960b..75a6142 100644 --- a/src/modules/RulesManagementModule.sol +++ b/src/modules/RulesManagementModule.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.20; /* ==== OpenZeppelin === */ import {EnumerableSet} from "OZ/utils/structs/EnumerableSet.sol"; -import {AccessControl} from "OZ/access/AccessControl.sol"; /* ==== Interface and other library === */ import {IRulesManagementModule} from "../interfaces/IRulesManagementModule.sol"; import {IRule} from "../interfaces/IRule.sol"; @@ -13,11 +12,7 @@ import {RulesManagementModuleInvariantStorage} from "./library/RulesManagementMo /** * @title RuleEngine - part */ -abstract contract RulesManagementModule is - AccessControl, - RulesManagementModuleInvariantStorage, - IRulesManagementModule -{ +abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage, IRulesManagementModule { modifier onlyRulesManager() { _onlyRulesManager(); _; @@ -38,6 +33,14 @@ abstract contract RulesManagementModule is /** * @inheritdoc IRulesManagementModule + * @dev Replaces the entire rule set atomically. + * Reverts if `rules_` is empty. Use {clearRules} to remove all rules explicitly. + * To transition from one non-empty set to another without an enforcement gap, + * call this function directly with the new set. + * No on-chain maximum number of rules is enforced. Operators are responsible + * for keeping the rule set size compatible with the target chain gas limits. + * Security convention: rule contracts should be treated as trusted business logic, + * but should not also be granted {RULES_MANAGEMENT_ROLE}. */ function setRules(IRule[] calldata rules_) public virtual override(IRulesManagementModule) onlyRulesManager { if (rules_.length == 0) { @@ -63,6 +66,9 @@ abstract contract RulesManagementModule is /** * @inheritdoc IRulesManagementModule + * @dev No on-chain maximum number of rules is enforced. Adding too many rules + * can increase transfer-time gas usage because rule checks are linear in rule count. + * Security convention: do not grant {RULES_MANAGEMENT_ROLE} to rule contracts. */ function addRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { _checkRule(address(rule_)); @@ -155,6 +161,10 @@ abstract contract RulesManagementModule is /** * @notice Go through all the rule to know if a restriction exists on the transfer + * @dev Complexity is O(number of configured rules). Large rule sets can make + * transfers too expensive on chains with lower block gas limits. + * Security convention: rule contracts are expected to be trusted and must not + * hold {RULES_MANAGEMENT_ROLE}. * @param from the origin address * @param to the destination address * @param value to transfer @@ -169,6 +179,10 @@ abstract contract RulesManagementModule is /** * @notice Go through all the rule to know if a restriction exists on the transfer + * @dev Complexity is O(number of configured rules). Large rule sets can make + * transfers too expensive on chains with lower block gas limits. + * Security convention: rule contracts are expected to be trusted and must not + * hold {RULES_MANAGEMENT_ROLE}. * @param spender the spender address (transferFrom) * @param from the origin address * @param to the destination address diff --git a/src/modules/library/ComplianceInterfaceId.sol b/src/modules/library/ComplianceInterfaceId.sol new file mode 100644 index 0000000..1c604a8 --- /dev/null +++ b/src/modules/library/ComplianceInterfaceId.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/** + * @title ComplianceInterfaceId + * @dev ERC-165 interface IDs used by RuleEngine for compliance interfaces. + */ +library ComplianceInterfaceId { + bytes4 public constant ERC3643_COMPLIANCE_INTERFACE_ID = 0x3144991c; + bytes4 public constant IERC7551_COMPLIANCE_INTERFACE_ID = 0x7157797f; +} diff --git a/test/HelperContract.sol b/test/HelperContract.sol index 1805977..ef59d3d 100644 --- a/test/HelperContract.sol +++ b/test/HelperContract.sol @@ -8,7 +8,7 @@ import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; import {RuleEngineInvariantStorage} from "src/modules/library/RuleEngineInvariantStorage.sol"; import {RulesManagementModuleInvariantStorage} from "src/modules/library/RulesManagementModuleInvariantStorage.sol"; // RuleEngine -import {RuleEngine} from "src/RuleEngine.sol"; +import {RuleEngine} from "src/deployment/RuleEngine.sol"; // forge-lint: disable-next-line(unused-import) import {RulesManagementModule} from "src/RuleEngineBase.sol"; // forge-lint: disable-next-line(unused-import) diff --git a/test/HelperContractOwnable.sol b/test/HelperContractOwnable.sol index 6a31a3e..08c9357 100644 --- a/test/HelperContractOwnable.sol +++ b/test/HelperContractOwnable.sol @@ -8,7 +8,7 @@ import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; import {RuleEngineInvariantStorage} from "src/modules/library/RuleEngineInvariantStorage.sol"; import {RulesManagementModuleInvariantStorage} from "src/modules/library/RulesManagementModuleInvariantStorage.sol"; // RuleEngineOwnable -import {RuleEngineOwnable} from "src/RuleEngineOwnable.sol"; +import {RuleEngineOwnable} from "src/deployment/RuleEngineOwnable.sol"; // forge-lint: disable-next-line(unused-import) import {RulesManagementModule} from "src/RuleEngineBase.sol"; // forge-lint: disable-next-line(unused-import) diff --git a/test/HelperContractOwnable2Step.sol b/test/HelperContractOwnable2Step.sol new file mode 100644 index 0000000..0ed3205 --- /dev/null +++ b/test/HelperContractOwnable2Step.sol @@ -0,0 +1,21 @@ +//SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {RuleEngineOwnable2Step} from "src/deployment/RuleEngineOwnable2Step.sol"; +import {RuleConditionalTransferLight} from "src/mocks/rules/operation/RuleConditionalTransferLight.sol"; + +/** + * @title Constants used by tests for RuleEngineOwnable2Step + */ +abstract contract HelperContractOwnable2Step { + address internal constant ZERO_ADDRESS = address(0); + address internal constant OWNER_ADDRESS = address(1); + address internal constant NEW_OWNER_ADDRESS = address(8); + address internal constant ATTACKER = address(4); + address internal constant CONDITIONAL_TRANSFER_OPERATOR_ADDRESS = address(9); + + RuleEngineOwnable2Step public ruleEngineMock; + RuleConditionalTransferLight public ruleConditionalTransferLight; + + string internal constant ERC2771ForwarderDomain = "ERC2771ForwarderDomain"; +} diff --git a/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol b/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol index ceae2fe..22ec64a 100644 --- a/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol +++ b/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol @@ -9,7 +9,7 @@ import "../../HelperContract.sol"; /** * @title Tests on the Access Control */ -contract RuleEngineAccessControlTest is Test, HelperContract { +contract RuleEngineTest is Test, HelperContract { // Custom error openZeppelin error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); diff --git a/test/RuleEngine/AccessControl/RuleEngineAccessControlOZ.t.sol b/test/RuleEngine/AccessControl/RuleEngineAccessControlOZ.t.sol index 28c4623..94bd300 100644 --- a/test/RuleEngine/AccessControl/RuleEngineAccessControlOZ.t.sol +++ b/test/RuleEngine/AccessControl/RuleEngineAccessControlOZ.t.sol @@ -9,7 +9,7 @@ import "../../HelperContract.sol"; /** * @title Tests on the provided functions by OpenZeppelin */ -contract RuleEngineAccessControlTest is Test, HelperContract, AccessControl { +contract RuleEngineTest is Test, HelperContract, AccessControl { // Arrange function setUp() public { ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); diff --git a/test/RuleEngine/RuleEngineCoverage.t.sol b/test/RuleEngine/RuleEngineCoverage.t.sol index 619e51e..5f9e9e2 100644 --- a/test/RuleEngine/RuleEngineCoverage.t.sol +++ b/test/RuleEngine/RuleEngineCoverage.t.sol @@ -7,6 +7,8 @@ import "../HelperContract.sol"; import {RuleEngineExposed} from "src/mocks/RuleEngineExposed.sol"; import {RuleInvalidMock} from "src/mocks/RuleInvalidMock.sol"; +import {ICompliance} from "src/mocks/ICompliance.sol"; +import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol"; /** * @title Coverage tests for RuleEngine (supportsInterface, _msgData, ERC-165 rule check) @@ -37,6 +39,14 @@ contract RuleEngineCoverageTest is Test, HelperContract { assertTrue(ruleEngineMock.supportsInterface(ERC1404_EXTEND_ID)); } + function testSupportsERC3643ComplianceInterface() public view { + assertTrue(ruleEngineMock.supportsInterface(type(ICompliance).interfaceId)); + } + + function testSupportsIERC7551ComplianceSubsetInterface() public view { + assertTrue(ruleEngineMock.supportsInterface(type(IERC7551ComplianceSubset).interfaceId)); + } + function testSupportsERC165Interface() public view { assertTrue(ruleEngineMock.supportsInterface(ERC165_ID)); } diff --git a/test/RuleEngine/RuleEngineDeployment.t.sol b/test/RuleEngine/RuleEngineDeployment.t.sol index bfe3965..7a18de9 100644 --- a/test/RuleEngine/RuleEngineDeployment.t.sol +++ b/test/RuleEngine/RuleEngineDeployment.t.sol @@ -5,6 +5,12 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../HelperContract.sol"; import {MinimalForwarderMock} from "CMTAT/mocks/MinimalForwarderMock.sol"; +import {IERC165} from "OZ/utils/introspection/IERC165.sol"; +import {IAccessControl} from "OZ/access/IAccessControl.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {ICompliance} from "src/mocks/ICompliance.sol"; +import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol"; /** * @title General functions of the RuleEngine @@ -46,6 +52,19 @@ contract RuleEngineTest is Test, HelperContract { assertEq(ruleEngineMock.version(), "3.0.0"); } + function testSupportsInterfaces() public { + // Arrange + ruleEngineMock = new RuleEngine(RULE_ENGINE_OPERATOR_ADDRESS, address(0x0), ZERO_ADDRESS); + + // Act & Assert + assertTrue(ruleEngineMock.supportsInterface(type(IERC165).interfaceId)); + assertTrue(ruleEngineMock.supportsInterface(type(IAccessControl).interfaceId)); + assertTrue(ruleEngineMock.supportsInterface(RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(ICompliance).interfaceId)); + assertTrue(ruleEngineMock.supportsInterface(type(IERC7551ComplianceSubset).interfaceId)); + } + function testCannotDeployContractifAdminAddressIsZero() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); diff --git a/test/RuleEngineOwnable/RuleEngineOwnableCoverage.t.sol b/test/RuleEngineOwnable/RuleEngineOwnableCoverage.t.sol index c5ef9ba..21f5c8c 100644 --- a/test/RuleEngineOwnable/RuleEngineOwnableCoverage.t.sol +++ b/test/RuleEngineOwnable/RuleEngineOwnableCoverage.t.sol @@ -7,9 +7,12 @@ import "../HelperContractOwnable.sol"; import {RuleEngineOwnableExposed} from "src/mocks/RuleEngineExposed.sol"; import {RuleInvalidMock} from "src/mocks/RuleInvalidMock.sol"; +import {IAccessControl} from "OZ/access/IAccessControl.sol"; +import {ICompliance} from "src/mocks/ICompliance.sol"; +import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol"; /** - * @title Coverage tests for RuleEngineOwnable (supportsInterface fallback, _msgData, ERC-165 rule check) + * @title Coverage tests for RuleEngineOwnable (_msgData, ERC-165 rule check) */ contract RuleEngineOwnableCoverageTest is Test, HelperContractOwnable { RuleEngineOwnableExposed public ruleEngineOwnableExposed; @@ -42,13 +45,23 @@ contract RuleEngineOwnableCoverageTest is Test, HelperContractOwnable { assertTrue(ruleEngineMock.supportsInterface(ERC173_ID)); } - function testSupportsERC165ViaAccessControlFallback() public view { - // This hits line 61: AccessControl.supportsInterface(interfaceId) + function testSupportsERC3643ComplianceInterface() public view { + assertTrue(ruleEngineMock.supportsInterface(type(ICompliance).interfaceId)); + } + + function testSupportsIERC7551ComplianceSubsetInterface() public view { + assertTrue(ruleEngineMock.supportsInterface(type(IERC7551ComplianceSubset).interfaceId)); + } + + function testSupportsERC165() public view { assertTrue(ruleEngineMock.supportsInterface(ERC165_ID)); } + function testDoesNotSupportIAccessControlInterface() public view { + assertFalse(ruleEngineMock.supportsInterface(type(IAccessControl).interfaceId)); + } + function testDoesNotSupportInvalidInterface() public view { - // Falls through all checks including AccessControl.supportsInterface -> false assertFalse(ruleEngineMock.supportsInterface(INVALID_ID)); } diff --git a/test/RuleEngineOwnable/RuleEngineOwnableDeployment.t.sol b/test/RuleEngineOwnable/RuleEngineOwnableDeployment.t.sol index 9e3062e..a5fa77d 100644 --- a/test/RuleEngineOwnable/RuleEngineOwnableDeployment.t.sol +++ b/test/RuleEngineOwnable/RuleEngineOwnableDeployment.t.sol @@ -5,6 +5,11 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../HelperContractOwnable.sol"; import {MinimalForwarderMock} from "CMTAT/mocks/MinimalForwarderMock.sol"; +import {IERC165} from "OZ/utils/introspection/IERC165.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {ICompliance} from "src/mocks/ICompliance.sol"; +import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol"; /** * @title Deployment tests for RuleEngineOwnable @@ -55,12 +60,17 @@ contract RuleEngineOwnableDeploymentTest is Test, HelperContractOwnable { ruleEngineMock = new RuleEngineOwnable(address(0x0), address(forwarder), ZERO_ADDRESS); } - function testSupportsERC173Interface() public { + function testSupportsInterfaces() public { // Arrange ruleEngineMock = new RuleEngineOwnable(OWNER_ADDRESS, address(0x0), ZERO_ADDRESS); - // Act & Assert - ERC-173 interface ID - assertTrue(ruleEngineMock.supportsInterface(0x7f5828d0)); + // Act & Assert + assertTrue(ruleEngineMock.supportsInterface(type(IERC165).interfaceId)); + assertTrue(ruleEngineMock.supportsInterface(0x7f5828d0)); // ERC-173 + assertTrue(ruleEngineMock.supportsInterface(RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(ICompliance).interfaceId)); + assertTrue(ruleEngineMock.supportsInterface(type(IERC7551ComplianceSubset).interfaceId)); } function testDeploymentWithTokenBound() public { diff --git a/test/RuleEngineOwnable2Step/RuleEngineOwnable2Step.t.sol b/test/RuleEngineOwnable2Step/RuleEngineOwnable2Step.t.sol new file mode 100644 index 0000000..4f65418 --- /dev/null +++ b/test/RuleEngineOwnable2Step/RuleEngineOwnable2Step.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Ownable} from "OZ/access/Ownable.sol"; +import {MinimalForwarderMock} from "CMTAT/mocks/MinimalForwarderMock.sol"; +import {IERC165} from "OZ/utils/introspection/IERC165.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {ICompliance} from "src/mocks/ICompliance.sol"; +import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol"; +// forge-lint: disable-next-line(unaliased-plain-import) +import "../HelperContractOwnable2Step.sol"; + +/** + * @title Deployment and ownership tests for RuleEngineOwnable2Step + */ +contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { + bytes4 constant ERC173_ID = 0x7f5828d0; + + function setUp() public { + ruleEngineMock = new RuleEngineOwnable2Step(OWNER_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); + ruleConditionalTransferLight = + new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + } + + function testDeploymentSetsOwner() public view { + assertEq(ruleEngineMock.owner(), OWNER_ADDRESS); + } + + function testCannotDeployContractIfOwnerAddressIsZero() public { + vm.expectRevert(abi.encodeWithSignature("OwnableInvalidOwner(address)", ZERO_ADDRESS)); + new RuleEngineOwnable2Step(ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); + } + + function testTrustedForwarderSetAtDeployment() public { + MinimalForwarderMock forwarder = new MinimalForwarderMock(); + forwarder.initialize(ERC2771ForwarderDomain); + + RuleEngineOwnable2Step ruleEngineWithForwarder = + new RuleEngineOwnable2Step(OWNER_ADDRESS, address(forwarder), ZERO_ADDRESS); + assertTrue(ruleEngineWithForwarder.isTrustedForwarder(address(forwarder))); + } + + function testSupportsOwnableAndComplianceInterfaces() public view { + assertTrue(ruleEngineMock.supportsInterface(type(IERC165).interfaceId)); + assertTrue(ruleEngineMock.supportsInterface(ERC173_ID)); + assertTrue(ruleEngineMock.supportsInterface(RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(ICompliance).interfaceId)); + assertTrue(ruleEngineMock.supportsInterface(type(IERC7551ComplianceSubset).interfaceId)); + } + + function testOwnerCanAddRule() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.addRule(ruleConditionalTransferLight); + assertEq(ruleEngineMock.rulesCount(), 1); + } + + function testNonOwnerCannotAddRule() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, ATTACKER)); + vm.prank(ATTACKER); + ruleEngineMock.addRule(ruleConditionalTransferLight); + } + + function testTransferOwnershipSetsPendingOwner() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.transferOwnership(NEW_OWNER_ADDRESS); + + assertEq(ruleEngineMock.owner(), OWNER_ADDRESS); + assertEq(ruleEngineMock.pendingOwner(), NEW_OWNER_ADDRESS); + } + + function testPendingOwnerCanAcceptOwnership() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.transferOwnership(NEW_OWNER_ADDRESS); + + vm.prank(NEW_OWNER_ADDRESS); + ruleEngineMock.acceptOwnership(); + + assertEq(ruleEngineMock.owner(), NEW_OWNER_ADDRESS); + assertEq(ruleEngineMock.pendingOwner(), ZERO_ADDRESS); + } + + function testNonPendingOwnerCannotAcceptOwnership() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.transferOwnership(NEW_OWNER_ADDRESS); + + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, ATTACKER)); + vm.prank(ATTACKER); + ruleEngineMock.acceptOwnership(); + } + + function testOwnerKeepsRightsUntilAcceptOwnership() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.transferOwnership(NEW_OWNER_ADDRESS); + + vm.prank(OWNER_ADDRESS); + ruleEngineMock.addRule(ruleConditionalTransferLight); + + assertEq(ruleEngineMock.rulesCount(), 1); + } + + function testOldOwnerLosesRightsAfterAcceptOwnership() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.transferOwnership(NEW_OWNER_ADDRESS); + + vm.prank(NEW_OWNER_ADDRESS); + ruleEngineMock.acceptOwnership(); + + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, OWNER_ADDRESS)); + vm.prank(OWNER_ADDRESS); + ruleEngineMock.clearRules(); + } +}