diff --git a/AGENTS.md b/AGENTS.md index 0057a7d..76854b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,7 +107,7 @@ function _onlyComplianceManager() internal virtual override onlyOwner {} 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 +2. **`RuleEngineBase._checkRule()`** — calls `RulesManagementModule._checkRule()` then validates ERC-165 interface ```solidity // RulesManagementModule (generic checks): @@ -118,7 +118,7 @@ function _checkRule(address rule_) internal view virtual { // RuleEngineBase (adds ERC-165 check): function _checkRule(address rule_) internal view virtual override { - super._checkRule(rule_); + RulesManagementModule._checkRule(rule_); if (!ERC165Checker.supportsInterface(rule_, RuleInterfaceId.IRULE_INTERFACE_ID)) revert RuleEngine_RuleInvalidInterface(); } @@ -233,6 +233,7 @@ Key points: - NatSpec comments on all public/external functions - Function ordering: constructor, receive, fallback, external, public, internal, private (view/pure last within each group) - Function declaration order: visibility, mutability, virtual, override, custom modifiers +- In `src/`, avoid `super` calls and prefer explicit parent-contract calls (e.g., `AccessControl.grantRole(...)`) for readability and deterministic inheritance behavior. - Section headers: `/* ============ SECTION ============ */` - Run `forge fmt` before committing diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5a2bd..142f30e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,10 +45,66 @@ forge lint - Update surya doc by running the 3 scripts in [./doc/script](./doc/script) - Update changelog +### v3.0.0-rc3 - 2026-05-06 +### Security + +- Enforce an on-chain maximum rule count in `RulesManagementModule` to mitigate transfer liveness risk from unbounded per-transfer rule iteration (Nethermind AuditAgent finding 3 follow-up). +- Add cap checks in both `addRule` and `setRules`, reverting with `RuleEngine_RulesManagementModule_MaxRulesExceeded(uint256)` when exceeded. +- Enforce on-chain privilege-separation for rule accounts: + - `RuleEngine.grantRole` now reverts for any role when `account` is currently in the rules set. + - `RuleEngineOwnable` and `RuleEngineOwnable2Step` now reject `transferOwnership` targets that are currently in the rules set. +- Add T-REX compatibility path for compliance binding operations: `bindToken(token)` / `unbindToken(token)` now allow token self-calls (`msg.sender == token`) in addition to manager/owner authorization. + +### Added + +- Add `maxRules()` and `setMaxRules(uint256)` to `IRulesManagementModule`. +- Add `DEFAULT_MAX_RULES = 10` and initialize module state with this default cap. +- Add `SetMaxRules(uint256)` event emitted on cap updates. +- Add interface ID libraries: + - `ERC1404InterfaceId` for `IERC1404` (`0xab84a5c8`) + - `OwnableInterfaceId` for `IERC173` (`0x7f5828d0`) + - `Ownable2StepInterfaceId` for Ownable2Step-specific methods (`0x9ab669ef`) +- Add dedicated access-control hook for cap governance: + - `RuleEngine`: `DEFAULT_ADMIN_ROLE` can update cap. + - `RuleEngineOwnable` and `RuleEngineOwnable2Step`: owner can update cap. +- Add `RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges()` error for rule-account privilege/ownership target protection. + +### Changed + +- Ownable variants now rely on OpenZeppelin `ERC165` inheritance in `RuleEngineOwnableShared` for base ERC-165 advertisement and extend it with RuleEngine + ERC-173 interface IDs. +- `supportsInterface` advertisement now explicitly includes `IERC1404` in addition to `IERC1404Extend`. +- `RuleEngineOwnable2Step.supportsInterface` now advertises the Ownable2Step-specific interface ID in addition to inherited RuleEngine/Ownable interfaces. + +### Testing + +- Add tests for default cap value, cap enforcement for `addRule` and `setRules`, and access control on `setMaxRules`. +- Add event-emission coverage for `SetMaxRules`. +- Extend interface advertisement tests to validate interface IDs through both: + - library constants + - `type().interfaceId` + for `IERC1404` and `IERC173`. +- Extend `RuleEngineOwnable2Step` interface support tests to assert: + - library constant support (`Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID`) + - mock interface support (`type(IOwnable2StepSubset).interfaceId`). +- Add RBAC tests ensuring roles cannot be granted to rule accounts. +- Add ownable and ownable2step tests ensuring ownership cannot be transferred to rule accounts. +- Add compliance-binding authorization tests across RBAC/ownable/ownable2step variants for: + - token self-bind + - token self-unbind + - cross-token bind/unbind denial + +### Documentation + +- Clarify README multi-token guidance with explicit data-plane vs control-plane wording: + - data-plane = runtime compliance callbacks (`transferred`, `created`, `destroyed`) + - control-plane = governance/configuration actions (`bindToken`, `unbindToken`, role grants, ownership changes, and rule management) +- Document that token-privilege separation in multi-token setups is an operational recommendation (not enforced on-chain) to preserve integrator flexibility for token-driven control-plane extensions. ### v3.0.0-rc2 - 2026-04-14 +Commit: `ec4a24a96ca30e2ef8f79a06e49846a431e9b4b1` + ### Dependencies - Update CMTAT submodule to [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0). diff --git a/CLAUDE.md b/CLAUDE.md index 0057a7d..76854b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -107,7 +107,7 @@ function _onlyComplianceManager() internal virtual override onlyOwner {} 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 +2. **`RuleEngineBase._checkRule()`** — calls `RulesManagementModule._checkRule()` then validates ERC-165 interface ```solidity // RulesManagementModule (generic checks): @@ -118,7 +118,7 @@ function _checkRule(address rule_) internal view virtual { // RuleEngineBase (adds ERC-165 check): function _checkRule(address rule_) internal view virtual override { - super._checkRule(rule_); + RulesManagementModule._checkRule(rule_); if (!ERC165Checker.supportsInterface(rule_, RuleInterfaceId.IRULE_INTERFACE_ID)) revert RuleEngine_RuleInvalidInterface(); } @@ -233,6 +233,7 @@ Key points: - NatSpec comments on all public/external functions - Function ordering: constructor, receive, fallback, external, public, internal, private (view/pure last within each group) - Function declaration order: visibility, mutability, virtual, override, custom modifiers +- In `src/`, avoid `super` calls and prefer explicit parent-contract calls (e.g., `AccessControl.grantRole(...)`) for readability and deterministic inheritance behavior. - Section headers: `/* ============ SECTION ============ */` - Run `forge fmt` before committing diff --git a/README.md b/README.md index 186cca7..38d8b8a 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ 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. 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:** 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. An on-chain rule cap is enforced (`maxRules`), set to `10` by default, and can be changed by governance (`DEFAULT_ADMIN_ROLE` on `RuleEngine`, owner on ownable variants). A misconfigured or gas-heavy rule can still 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. @@ -77,6 +77,7 @@ This diagram illustrates how a transfer with a CMTAT or ERC-3643 token with a Ru | RuleEngine version | Compatible Versions | | ------------------------------------------------------------ | ------------------------------------------------------------ | +| **v3.0.0-rc3** | CMTAT ≥ v3.0.0
CMTAT target version: [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0) | | **v3.0.0-rc2** | CMTAT ≥ v3.0.0
CMTAT target version: [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0) | | **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
| @@ -163,7 +164,7 @@ The `RuleEngine` base interface is defined in the CMTAT repository. ![cmtat_surya_inheritance_IRuleEngine.sol](./doc/schema/cmtat_surya_inheritance_IRuleEngine.sol.png) -It inherits from several others interfaces: `IERC1404Extend`, `IERC7551Compliance`, `IERC3643ComplianceContract` +It inherits from several others interfaces: `IERC1404`, `IERC1404Extend`, `IERC7551Compliance`, `IERC3643ComplianceContract` ```solidity // IRuleEngine @@ -261,6 +262,28 @@ The `RuleEngineOwnable2Step` contract uses OpenZeppelin's `Ownable2Step`, which See also [docs.openzeppelin.com - Ownable2Step](https://docs.openzeppelin.com/contracts/5.x/api/access#Ownable2Step) +### ERC-165 Support by Deployment Version + +The table below summarizes which ERC-165 interfaces are advertised by each deployment version via `supportsInterface(bytes4)`. + +| Interface | Interface ID | RuleEngine (RBAC deployment) | RuleEngineOwnable deployment | RuleEngineOwnable2Step deployment | +| --- | --- | --- | --- | --- | +| `IERC165` | `0x01ffc9a7` | Yes | Yes | Yes | +| `IRuleEngine` | `0x20c49ce7` | Yes | Yes | Yes | +| `IERC1404` | `0xab84a5c8` | Yes | Yes | Yes | +| `IERC1404Extend` | `0x78a8de7d` | Yes | Yes | Yes | +| `IERC3643Compliance` | `0x3144991c` | Yes | Yes | Yes | +| `IERC7551Compliance` (subset) | `0x7157797f` | Yes | Yes | Yes | +| `IERC173` | `0x7f5828d0` | No | Yes | Yes | +| `Ownable2Step` specific (`pendingOwner()`, `acceptOwnership()`) | `0x9ab669ef` | No | No | Yes | +| `IAccessControl` | `0x7965db0b` | Yes | No | No | +| `IAccessControlEnumerable` | `0x5a05180f` | Yes | No | No | + +Notes: +- `RuleEngine` advertises OpenZeppelin RBAC interfaces because it inherits `AccessControlEnumerable`. +- `RuleEngineOwnable` / `RuleEngineOwnable2Step` intentionally do not advertise `IAccessControl`. +- `Ownable2Step` specific interface ID is defined in `Ownable2StepInterfaceId` and includes only `pendingOwner()` and `acceptOwnership()`. + #### Role list (RuleEngine only) Here is the list of roles and their 32 bytes identifier for the `RuleEngine` contract. @@ -271,7 +294,7 @@ It is set in the constructor when the contract is deployed. > Note: For `RuleEngineOwnable` and `RuleEngineOwnable2Step`, 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. +> **Warning (role assignment):** Rule contracts should be treated as trusted logic components and kept separate from governance/operator identities. The protocol now enforces key protections on-chain: in RBAC deployments, `grantRole` reverts if the target account is in the rule set; in ownable deployments, `transferOwnership` reverts if the new owner is in the rule set. In multi-token deployments, do not grant any governance/operator privileges to token contract addresses (bound tokens should remain data-plane callers only, meaning runtime compliance callbacks such as `transferred`, `created`, and `destroyed`). This token-privilege separation is intentionally documented as an operational constraint (not enforced on-chain) to preserve flexibility for integrators who explicitly want to extend their token and route selected RuleEngine control-plane actions through token logic (`control-plane` here means configuration/governance actions such as `bindToken`, `unbindToken`, role grants, ownership changes, and rule management). | | Defined in | 32 bytes identifier | | ----------------------- | -------------------------------- | ------------------------------------------------------------ | @@ -362,8 +385,9 @@ RuleEngineOwnable2Step **Key differences from RuleEngineOwnable:** - Uses a two-step ownership transfer flow: `transferOwnership()` then `acceptOwnership()` - The current owner retains privileges until the pending owner accepts ownership -- Reuses `RuleEngineOwnableShared` for constructor, ERC-165, and ERC-2771 behavior +- Reuses `RuleEngineOwnableShared` for constructor, ERC-165 (via OpenZeppelin `ERC165`), and ERC-2771 behavior - Implements ERC-173 interface (`supportsInterface(0x7f5828d0)` returns `true`) +- Implements Ownable2Step-specific ERC-165 interface (`supportsInterface(0x9ab669ef)` returns `true`), covering `pendingOwner()` and `acceptOwnership()` @@ -1307,9 +1331,9 @@ The final report is available in [ABDK_CMTA_CMTATRuleEngine_v_1_0.pdf](https://g |---|----------|---------|--------| | 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). | +| 3 | Info | Unbounded rules loop — potential permanent DoS | Fixed in `v3.0.0-rc3`: on-chain configurable cap (`maxRules`) with default `10`, enforced in `addRule` and `setRules`. | | 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`. | +| 5 | Info | Re-entrant rule can modify rule set during `transferred()` | Fixed in `v3.0.0-rc3`: rule accounts cannot receive roles in RBAC `RuleEngine`; ownable variants reject ownership transfer to rule accounts. | | 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). | @@ -1325,12 +1349,14 @@ Here is the list of report performed with [Slither](https://github.com/crytic/sl 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 +4 finding categories — 0 High · 0 Medium · 10 Low · 4 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 | +| 10 | `dead-code` | Informational | 1 | Accepted / no action — `_msgData` override is required by inheritance/context pattern | +| 11 | `naming-convention` | Informational | 1 | Ignored — finding is in external CMTAT dependency code | +| 12–13 | `unindexed-event-address` | Informational | 2 | Deferred — adding `indexed` to `TokenBound`/`TokenUnbound` is interface-breaking | #### Aderyn @@ -1344,19 +1370,20 @@ aderyn -x mocks --output aderyn-report.md | ------- | ------ | ---------- | | 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. +Report scope: 18 Solidity files, 542 nSLOC. -0 High · 7 Low +0 High · 8 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 | +| L-1 | Centralization Risk | 14 | Accepted by design — privileged compliance tool | +| L-2 | Unspecific Solidity Pragma | 14 | Accepted by design — intentional for library reusability | +| L-3 | PUSH0 Opcode | 18 | Not applicable — project targets Prague EVM | +| L-4 | Modifier Invoked Only Once | 1 | Accepted by design — keeps hook-style access-control abstraction | +| L-5 | Empty Block | 9 | Accepted by design — access-control hook pattern | +| L-6 | Loop Contains `require`/`revert` | 1 | Accepted by design — `setRules` is an atomic batch operation | +| L-7 | Costly Operations Inside Loop | 1 | Accepted — unavoidable `SSTORE` in `setRules` | +| L-8 | Unchecked Return | 1 | Accepted — `_grantRole` return is irrelevant in constructor | ## Documentation diff --git a/doc/ERCSpecification/erc-1404.md b/doc/ERCSpecification/erc-1404.md new file mode 100644 index 0000000..af77c59 --- /dev/null +++ b/doc/ERCSpecification/erc-1404.md @@ -0,0 +1,83 @@ +|eip |title | authors| status | discussions-to | type | category | created | +|---------|----|---------|-------|----------------|-----|----------|---------| +| 1404 | Simple Restricted Token Standard|Ron Gierlach <@rongierlach>, James Poole <@pooleja>, Mason Borda <@masonicgit>, Lawson Baker <@lwsnbaker> | Draft | https://github.com/simple-restricted-token/simple-restricted-token/issues | Standards | ERC | 2018-07-27 | + +# Simple Restricted Token Standard + +## Simple Summary + +A simple and interoperable standard for issuing tokens with transfer restrictions. The following draws on input from top issuers, law firms, relevant US regulatory bodies, and exchanges. + +## Abstract + +Current ERC token standards have provided the community with a platform on which to develop a decentralized economy that is focused on building Ethereum applications for the real world. As these applications mature and face consumer adoption, they begin to interface with corporate governance requirements as well as regulations. They must not only be able to meet corporate and regulatory requirements but must also be able to integrate with technology platforms underpinning their associated businesses. What follows is a simple and extendable standard that seeks to ease the burden of integration for wallets, exchanges, and issuers. + +## Motivation + +Token issuers need a way to restrict transfers of ERC-20 tokens to be compliant with securities laws and other contractual obligations. Current implementations do not address these requirements. + +A few emergent examples: + +- Enforcing Token Lock-Up Periods +- Enforcing Passed AML/KYC Checks +- Private Real-Estate Investment Trusts +- Delaware General Corporations Law Shares + +Furthermore, standards adoption amongst token issuers has the potential to evolve into a dynamic and interoperable landscape of automated compliance. + +The following design gives greater freedom / upgradability to token issuers and simultaneously decreases the burden of integration for developers and exchanges. + +Additionally, we see fit to provide a pattern by which human-readable messages may be returned when token transfers are reverted. Transparency as to _why_ a token's transfer was reverted is of equal importance to the successful enforcement of the transfer restriction itself. + +A widely adopted standard for detecting restrictions and messaging errors within token transfers will highly convenience the exchanges, wallets, and issuers of the future. + +## Specification + +The ERC-20 token provides the following basic features: +```solidity +contract ERC20 { + function totalSupply() public view returns (uint256); + function balanceOf(address who) public view returns (uint256); + function transfer(address to, uint256 value) public returns (bool); + function allowance(address owner, address spender) public view returns (uint256); + function transferFrom(address from, address to, uint256 value) public returns (bool); + function approve(address spender, uint256 value) public returns (bool); + event Approval(address indexed owner, address indexed spender, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); +} +``` +The ERC-1404 standard builds on ERC-20's interface, adding two functions: +```solidity +contract ERC1404 is ERC20 { + function detectTransferRestriction (address from, address to, uint256 value) public view returns (uint8); + function messageForTransferRestriction (uint8 restrictionCode) public view returns (string); +} +``` + +The logic of `detectTransferRestriction` and `messageForTransferRestriction` are left up to the issuer. + +The only requirement is that `detectTransferRestriction` must be evaluated inside a token's `transfer` and `transferFrom` methods. + +If, inside these transfer methods, `detectTransferRestriction` returns a value other than `0`, the transaction should be reverted. + +## Rationale + +The standard proposes two functions on top of the ERC-20 standard. Let's discuss the rationale for each. + +1. `detectTransferRestriction` - This function is where an issuer enforces the restriction logic of their token transfers. Some examples of this might include, checking if the token recipient is whitelisted, checking if a sender's tokens are frozen in a lock-up period, etc. Because implementation is up to the issuer, this function serves solely to standardize _where_ execution of such logic should be initiated. Additionally, 3rd parties may publicly call this function to check the expected outcome of a transfer. Because this function returns a `uint8` code rather than a boolean or just reverting, it allows the function caller to know the reason why a transfer might fail and report this to relevant counterparties. +2. `messageForTransferRestriction` - This function is effectively an accessor for the "message", a human-readable explanation as to _why_ a transaction is restricted. By standardizing message look-ups, we empower user interface builders to effectively report errors to users. + +## Backwards Compatibility + +By design ERC-1404 is fully backwards compatible with ERC-20. +Some examples of how it may be integrated with common types of restricted tokens may be found [here](https://github.com/simple-restricted-token/simple-restricted-token-standard#readme). + +## Test Cases & Implementation + +See the reference implementation and tests [here](https://github.com/simple-restricted-token/reference-implementation#readme). +See some examples of common usage patterns for ERC-1404 [here](https://github.com/simple-restricted-token/simple-restricted-token-standard#readme). + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). + diff --git a/doc/ERCSpecification/erc-165.md b/doc/ERCSpecification/erc-165.md new file mode 100644 index 0000000..77e6cbc --- /dev/null +++ b/doc/ERCSpecification/erc-165.md @@ -0,0 +1,235 @@ +--- +eip: 165 +title: Standard Interface Detection +author: Christian Reitwießner , Nick Johnson , Fabian Vogelsteller , Jordi Baylina , Konrad Feldmeier , William Entriken +type: Standards Track +category: ERC +status: Final +created: 2018-01-23 +requires: 214 +--- + +## Simple Summary + +Creates a standard method to publish and detect what interfaces a smart contract implements. + +## Abstract + +Herein, we standardize the following: + +1. How interfaces are identified +2. How a contract will publish the interfaces it implements +3. How to detect if a contract implements ERC-165 +4. How to detect if a contract implements any given interface + +## Motivation + +For some "standard interfaces" like [the ERC-20 token interface](./eip-20.md), it is sometimes useful to query whether a contract supports the interface and if yes, which version of the interface, in order to adapt the way in which the contract is to be interacted with. Specifically for ERC-20, a version identifier has already been proposed. This proposal standardizes the concept of interfaces and standardizes the identification (naming) of interfaces. + +## Specification + +### How Interfaces are Identified + +For this standard, an *interface* is a set of [function selectors as defined by the Ethereum ABI](https://solidity.readthedocs.io/en/develop/abi-spec.html#function-selector). This a subset of [Solidity's concept of interfaces](https://solidity.readthedocs.io/en/develop/abi-spec.html) and the `interface` keyword definition which also defines return types, mutability and events. + +We define the interface identifier as the XOR of all function selectors in the interface. This code example shows how to calculate an interface identifier: + +```solidity +pragma solidity ^0.4.20; + +interface Solidity101 { + function hello() external pure; + function world(int) external pure; +} + +contract Selector { + function calculateSelector() public pure returns (bytes4) { + Solidity101 i; + return i.hello.selector ^ i.world.selector; + } +} +``` + +Note: interfaces do not permit optional functions, therefore, the interface identity will not include them. + +### How a Contract will Publish the Interfaces it Implements + +A contract that is compliant with ERC-165 shall implement the following interface (referred as `ERC165.sol`): + +```solidity +pragma solidity ^0.4.20; + +interface ERC165 { + /// @notice Query if a contract implements an interface + /// @param interfaceID The interface identifier, as specified in ERC-165 + /// @dev Interface identification is specified in ERC-165. This function + /// uses less than 30,000 gas. + /// @return `true` if the contract implements `interfaceID` and + /// `interfaceID` is not 0xffffffff, `false` otherwise + function supportsInterface(bytes4 interfaceID) external view returns (bool); +} +``` + +The interface identifier for this interface is `0x01ffc9a7`. You can calculate this by running `bytes4(keccak256('supportsInterface(bytes4)'));` or using the `Selector` contract above. + +Therefore the implementing contract will have a `supportsInterface` function that returns: + +- `true` when `interfaceID` is `0x01ffc9a7` (EIP165 interface) +- `false` when `interfaceID` is `0xffffffff` +- `true` for any other `interfaceID` this contract implements +- `false` for any other `interfaceID` + +This function must return a bool and use at most 30,000 gas. + +Implementation note, there are several logical ways to implement this function. Please see the example implementations and the discussion on gas usage. + +### How to Detect if a Contract Implements ERC-165 + +1. The source contract makes a `STATICCALL` to the destination address with input data: `0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000` and gas 30,000. This corresponds to `contract.supportsInterface(0x01ffc9a7)`. +2. If the call fails or return false, the destination contract does not implement ERC-165. +3. If the call returns true, a second call is made with input data `0x01ffc9a7ffffffff00000000000000000000000000000000000000000000000000000000`. +4. If the second call fails or returns true, the destination contract does not implement ERC-165. +5. Otherwise it implements ERC-165. + +### How to Detect if a Contract Implements any Given Interface + +1. If you are not sure if the contract implements ERC-165, use the above procedure to confirm. +2. If it does not implement ERC-165, then you will have to see what methods it uses the old-fashioned way. +3. If it implements ERC-165 then just call `supportsInterface(interfaceID)` to determine if it implements an interface you can use. + +## Rationale + +We tried to keep this specification as simple as possible. This implementation is also compatible with the current Solidity version. + +## Backwards Compatibility + +The mechanism described above (with `0xffffffff`) should work with most of the contracts previous to this standard to determine that they do not implement ERC-165. + +Also [the ENS](./eip-137.md) already implements this EIP. + +## Test Cases + +Following is a contract that detects which interfaces other contracts implement. From @fulldecent and @jbaylina. + +```solidity +pragma solidity ^0.4.20; + +contract ERC165Query { + bytes4 constant InvalidID = 0xffffffff; + bytes4 constant ERC165ID = 0x01ffc9a7; + + function doesContractImplementInterface(address _contract, bytes4 _interfaceId) external view returns (bool) { + uint256 success; + uint256 result; + + (success, result) = noThrowCall(_contract, ERC165ID); + if ((success==0)||(result==0)) { + return false; + } + + (success, result) = noThrowCall(_contract, InvalidID); + if ((success==0)||(result!=0)) { + return false; + } + + (success, result) = noThrowCall(_contract, _interfaceId); + if ((success==1)&&(result==1)) { + return true; + } + return false; + } + + function noThrowCall(address _contract, bytes4 _interfaceId) constant internal returns (uint256 success, uint256 result) { + bytes4 erc165ID = ERC165ID; + + assembly { + let x := mload(0x40) // Find empty storage location using "free memory pointer" + mstore(x, erc165ID) // Place signature at beginning of empty storage + mstore(add(x, 0x04), _interfaceId) // Place first argument directly next to signature + + success := staticcall( + 30000, // 30k gas + _contract, // To addr + x, // Inputs are stored at location x + 0x24, // Inputs are 36 bytes long + x, // Store output over input (saves space) + 0x20) // Outputs are 32 bytes long + + result := mload(x) // Load the result + } + } +} +``` + +## Implementation + +This approach uses a `view` function implementation of `supportsInterface`. The execution cost is 586 gas for any input. But contract initialization requires storing each interface (`SSTORE` is 20,000 gas). The `ERC165MappingImplementation` contract is generic and reusable. + +```solidity +pragma solidity ^0.4.20; + +import "./ERC165.sol"; + +contract ERC165MappingImplementation is ERC165 { + /// @dev You must not set element 0xffffffff to true + mapping(bytes4 => bool) internal supportedInterfaces; + + function ERC165MappingImplementation() internal { + supportedInterfaces[this.supportsInterface.selector] = true; + } + + function supportsInterface(bytes4 interfaceID) external view returns (bool) { + return supportedInterfaces[interfaceID]; + } +} + +interface Simpson { + function is2D() external returns (bool); + function skinColor() external returns (string); +} + +contract Lisa is ERC165MappingImplementation, Simpson { + function Lisa() public { + supportedInterfaces[this.is2D.selector ^ this.skinColor.selector] = true; + } + + function is2D() external returns (bool){} + function skinColor() external returns (string){} +} +``` + +Following is a `pure` function implementation of `supportsInterface`. The worst-case execution cost is 236 gas, but increases linearly with a higher number of supported interfaces. + +```solidity +pragma solidity ^0.4.20; + +import "./ERC165.sol"; + +interface Simpson { + function is2D() external returns (bool); + function skinColor() external returns (string); +} + +contract Homer is ERC165, Simpson { + function supportsInterface(bytes4 interfaceID) external view returns (bool) { + return + interfaceID == this.supportsInterface.selector || // ERC165 + interfaceID == this.is2D.selector + ^ this.skinColor.selector; // Simpson + } + + function is2D() external returns (bool){} + function skinColor() external returns (string){} +} +``` + +With three or more supported interfaces (including ERC165 itself as a required supported interface), the mapping approach (in every case) costs less gas than the pure approach (at worst case). + +## Version history +* PR 1640, finalized 2019-01-23 -- This corrects the noThrowCall test case to use 36 bytes rather than the previous 32 bytes. The previous code was an error that still silently worked in Solidity 0.4.x but which was broken by new behavior introduced in Solidity 0.5.0. This change was discussed at [#1640](https://github.com/ethereum/EIPs/pull/1640). + +* EIP 165, finalized 2018-04-20 -- Original published version. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/ERCSpecification/erc-173.md b/doc/ERCSpecification/erc-173.md new file mode 100644 index 0000000..e79033e --- /dev/null +++ b/doc/ERCSpecification/erc-173.md @@ -0,0 +1,97 @@ +--- +eip: 173 +title: Contract Ownership Standard +description: A standard interface for ownership of contracts +author: Nick Mudge (@mudgen), Dan Finlay +discussions-to: https://github.com/ethereum/EIPs/issues/173 +type: Standards Track +category: ERC +status: Final +created: 2018-06-07 +--- + +## Abstract + +This specification defines standard functions for owning or controlling a contract. + +An implementation allows reading the current owner (`owner() returns (address)`) and transferring ownership (`transferOwnership(address newOwner)`) along with a standardized event for when ownership is changed (`OwnershipTransferred(address indexed previousOwner, address indexed newOwner)`). + +## Motivation + +Many smart contracts require that they be owned or controlled in some way. For example to withdraw funds or perform administrative actions. It is so common that the contract interface used to handle contract ownership should be standardized to allow compatibility with user interfaces and contracts that manage contracts. + +Here are some examples of kinds of contracts and applications that can benefit from this standard: +1. Exchanges that buy/sell/auction ethereum contracts. This is only widely possible if there is a standard for getting the owner of a contract and transferring ownership. +2. Contract wallets that hold the ownership of contracts and that can transfer the ownership of contracts. +3. Contract registries. It makes sense for some registries to only allow the owners of contracts to add/remove their contracts. A standard must exist for these contract registries to verify that a contract is being submitted by the owner of it before accepting it. +4. User interfaces that show and transfer ownership of contracts. + +## Specification + +Every ERC-173 compliant contract must implement the `ERC173` interface. Contracts should also implement `ERC165` for the ERC-173 interface. + +```solidity + +/// @title ERC-173 Contract Ownership Standard +/// Note: the ERC-165 identifier for this interface is 0x7f5828d0 +interface ERC173 /* is ERC165 */ { + /// @dev This emits when ownership of a contract changes. + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /// @notice Get the address of the owner + /// @return The address of the owner. + function owner() view external returns(address); + + /// @notice Set the address of the new owner of the contract + /// @dev Set _newOwner to address(0) to renounce any ownership. + /// @param _newOwner The address of the new owner of the contract + function transferOwnership(address _newOwner) external; +} + +interface ERC165 { + /// @notice Query if a contract implements an interface + /// @param interfaceID The interface identifier, as specified in ERC-165 + /// @dev Interface identification is specified in ERC-165. + /// @return `true` if the contract implements `interfaceID` and + /// `interfaceID` is not 0xffffffff, `false` otherwise + function supportsInterface(bytes4 interfaceID) external view returns (bool); +} +``` + +The `owner()` function may be implemented as `pure` or `view`. + +The `transferOwnership(address _newOwner)` function may be implemented as `public` or `external`. + +To renounce any ownership of a contract set `_newOwner` to the zero address: `transferOwnership(address(0))`. If this is done then a contract is no longer owned by anybody. + +The OwnershipTransferred event should be emitted when a contract is created. + +## Rationale + +Key factors influencing the standard: +- Keeping the number of functions in the interface to a minimum to prevent contract bloat. +- Backwards compatibility with existing contracts. +- Simplicity +- Gas efficient + +Several ownership schemes were considered. The scheme chosen in this standard was chosen because of its simplicity, low gas cost and backwards compatibility with existing contracts. + +Here are other schemes that were considered: +1. **Associating an Ethereum Name Service (ENS) domain name with a contract.** A contract's `owner()` function could look up the owner address of a particular ENS name and use that as the owning address of the contract. Using this scheme a contract could be transferred by transferring the ownership of the ENS domain name to a different address. Short comings to this approach are that it is not backwards compatible with existing contracts and requires gas to make external calls to ENS related contracts to get the owner address. +2. **Associating an ERC721-based non-fungible token (NFT) with a contract.** Ownership of a contract could be tied to the ownership of an NFT. The benefit of this approach is that the existing ERC721-based infrastructure could be used to sell/buy/auction contracts. Short comings to this approach are additional complexity and infrastructure required. A contract could be associated with a particular NFT but the NFT would not track that it had ownership of a contract unless it was programmed to track contracts. In addition handling ownership of contracts this way is not backwards compatible. + +This standard does not exclude the above ownership schemes or other schemes from also being implemented in the same contract. For example a contract could implement this standard and also implement the other schemes so that ownership could be managed and transferred in multiple ways. This standard does provide a simple ownership scheme that is backwards compatible, is light-weight and simple to implement, and can be widely adopted and depended on. + +This standard can be (and has been) extended by other standards to add additional ownership functionality. + +## Security Considerations + +If the address returned by `owner()` is an externally owned account then its private key must not be lost or compromised. + +## Backwards Compatibility + +Many existing contracts already implement this standard. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/coverage/code-coverage.png b/doc/coverage/code-coverage.png index 0686ade..84075e2 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 37cda05..140fc17 100644 --- a/doc/coverage/coverage/index-sort-b.html +++ b/doc/coverage/coverage/index-sort-b.html @@ -31,27 +31,27 @@ lcov.info Lines: - 163 - 163 - 100.0 % + 207 + 212 + 97.6 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 53 - 53 - 100.0 % + 72 + 76 + 94.7 % Branches: - 31 - 36 - 86.1 % + 37 + 42 + 88.1 % @@ -84,38 +84,38 @@ src/modules -
100.0%
+
95.0%95.0%
- 100.0 % - 79 / 79 - 100.0 % - 25 / 25 - 81.5 % - 22 / 27 + 95.0 % + 95 / 100 + 87.5 % + 28 / 32 + 83.9 % + 26 / 31 - src + src/deployment
100.0%
100.0 % - 42 / 42 + 49 / 49 100.0 % - 13 / 13 + 24 / 24 100.0 % - 4 / 4 + 5 / 5 - src/deployment + src
100.0%
100.0 % - 42 / 42 + 63 / 63 100.0 % - 15 / 15 + 20 / 20 100.0 % - 5 / 5 + 6 / 6 diff --git a/doc/coverage/coverage/index-sort-f.html b/doc/coverage/coverage/index-sort-f.html index 4e0950c..1fedf73 100644 --- a/doc/coverage/coverage/index-sort-f.html +++ b/doc/coverage/coverage/index-sort-f.html @@ -31,27 +31,27 @@ lcov.info Lines: - 163 - 163 - 100.0 % + 207 + 212 + 97.6 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 53 - 53 - 100.0 % + 72 + 76 + 94.7 % Branches: - 31 - 36 - 86.1 % + 37 + 42 + 88.1 % @@ -82,40 +82,40 @@ Branches Sort by branch coverage - src + src/modules -
100.0%
+
95.0%95.0%
- 100.0 % - 42 / 42 - 100.0 % - 13 / 13 - 100.0 % - 4 / 4 + 95.0 % + 95 / 100 + 87.5 % + 28 / 32 + 83.9 % + 26 / 31 - src/deployment + src
100.0%
100.0 % - 42 / 42 + 63 / 63 100.0 % - 15 / 15 + 20 / 20 100.0 % - 5 / 5 + 6 / 6 - src/modules + src/deployment
100.0%
100.0 % - 79 / 79 + 49 / 49 + 100.0 % + 24 / 24 100.0 % - 25 / 25 - 81.5 % - 22 / 27 + 5 / 5 diff --git a/doc/coverage/coverage/index-sort-l.html b/doc/coverage/coverage/index-sort-l.html index 130e77b..de8fcda 100644 --- a/doc/coverage/coverage/index-sort-l.html +++ b/doc/coverage/coverage/index-sort-l.html @@ -31,27 +31,27 @@ lcov.info Lines: - 163 - 163 - 100.0 % + 207 + 212 + 97.6 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 53 - 53 - 100.0 % + 72 + 76 + 94.7 % Branches: - 31 - 36 - 86.1 % + 37 + 42 + 88.1 % @@ -82,16 +82,16 @@ Branches Sort by branch coverage - src + src/modules -
100.0%
+
95.0%95.0%
- 100.0 % - 42 / 42 - 100.0 % - 13 / 13 - 100.0 % - 4 / 4 + 95.0 % + 95 / 100 + 87.5 % + 28 / 32 + 83.9 % + 26 / 31 src/deployment @@ -99,23 +99,23 @@
100.0%
100.0 % - 42 / 42 + 49 / 49 100.0 % - 15 / 15 + 24 / 24 100.0 % 5 / 5 - src/modules + src
100.0%
100.0 % - 79 / 79 + 63 / 63 + 100.0 % + 20 / 20 100.0 % - 25 / 25 - 81.5 % - 22 / 27 + 6 / 6 diff --git a/doc/coverage/coverage/index.html b/doc/coverage/coverage/index.html index 9e873ee..af90d9e 100644 --- a/doc/coverage/coverage/index.html +++ b/doc/coverage/coverage/index.html @@ -31,27 +31,27 @@ lcov.info Lines: - 163 - 163 - 100.0 % + 207 + 212 + 97.6 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 53 - 53 - 100.0 % + 72 + 76 + 94.7 % Branches: - 31 - 36 - 86.1 % + 37 + 42 + 88.1 % @@ -87,11 +87,11 @@
100.0%
100.0 % - 42 / 42 + 63 / 63 100.0 % - 13 / 13 + 20 / 20 100.0 % - 4 / 4 + 6 / 6 src/deployment @@ -99,23 +99,23 @@
100.0%
100.0 % - 42 / 42 + 49 / 49 100.0 % - 15 / 15 + 24 / 24 100.0 % 5 / 5 src/modules -
100.0%
+
95.0%95.0%
- 100.0 % - 79 / 79 - 100.0 % - 25 / 25 - 81.5 % - 22 / 27 + 95.0 % + 95 / 100 + 87.5 % + 28 / 32 + 83.9 % + 26 / 31 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 3814568..cad3862 100644 --- a/doc/coverage/coverage/src/RuleEngineBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/RuleEngineBase.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 42 - 42 + 48 + 48 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 13 - 13 + 14 + 14 100.0 % @@ -69,56 +69,60 @@ Hit count Sort by hit count - RuleEngineBase.created + RuleEngineBase.created 4 - RuleEngineBase.destroyed + RuleEngineBase.destroyed 4 - RuleEngineBase.transferred.0 + RuleEngineBase.transferred.0 5 - RuleEngineBase.detectTransferRestrictionFrom + RuleEngineBase.detectTransferRestrictionFrom 18 - RuleEngineBase.transferred.1 + RuleEngineBase.transferred.1 18 - RuleEngineBase._messageForTransferRestriction + RuleEngineBase._messageForTransferRestriction 19 - RuleEngineBase.messageForTransferRestriction + RuleEngineBase.messageForTransferRestriction 19 - RuleEngineBase.canTransferFrom + RuleEngineBase.canTransferFrom 21 - RuleEngineBase.canTransfer + RuleEngineBase.canTransfer 25 - RuleEngineBase.detectTransferRestriction + RuleEngineBase.detectTransferRestriction 34 - RuleEngineBase._detectTransferRestrictionFrom + RuleEngineBase._detectTransferRestrictionFrom 39 - RuleEngineBase._detectTransferRestriction + RuleEngineBase._supportsRuleEngineBaseInterface + 46 + + + RuleEngineBase._detectTransferRestriction 59 - RuleEngineBase._checkRule - 217 + RuleEngineBase._checkRule + 253
diff --git a/doc/coverage/coverage/src/RuleEngineBase.sol.func.html b/doc/coverage/coverage/src/RuleEngineBase.sol.func.html index 95e4dbb..906ea70 100644 --- a/doc/coverage/coverage/src/RuleEngineBase.sol.func.html +++ b/doc/coverage/coverage/src/RuleEngineBase.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 42 - 42 + 48 + 48 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 13 - 13 + 14 + 14 100.0 % @@ -69,55 +69,59 @@ Hit count Sort by hit count - RuleEngineBase._checkRule - 217 + RuleEngineBase._checkRule + 253 - RuleEngineBase._detectTransferRestriction + RuleEngineBase._detectTransferRestriction 59 - RuleEngineBase._detectTransferRestrictionFrom + RuleEngineBase._detectTransferRestrictionFrom 39 - RuleEngineBase._messageForTransferRestriction + RuleEngineBase._messageForTransferRestriction 19 - RuleEngineBase.canTransfer + RuleEngineBase._supportsRuleEngineBaseInterface + 46 + + + RuleEngineBase.canTransfer 25 - RuleEngineBase.canTransferFrom + RuleEngineBase.canTransferFrom 21 - RuleEngineBase.created + RuleEngineBase.created 4 - RuleEngineBase.destroyed + RuleEngineBase.destroyed 4 - RuleEngineBase.detectTransferRestriction + RuleEngineBase.detectTransferRestriction 34 - RuleEngineBase.detectTransferRestrictionFrom + RuleEngineBase.detectTransferRestrictionFrom 18 - RuleEngineBase.messageForTransferRestriction + RuleEngineBase.messageForTransferRestriction 19 - RuleEngineBase.transferred.0 + RuleEngineBase.transferred.0 5 - RuleEngineBase.transferred.1 + RuleEngineBase.transferred.1 18 diff --git a/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html b/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html index 9a123d7..343a1c7 100644 --- a/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html +++ b/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 42 - 42 + 48 + 48 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 13 - 13 + 14 + 14 100.0 % @@ -74,196 +74,213 @@ 3 : : pragma solidity ^0.8.20; 4 : : 5 : : /* ==== OpenZeppelin === */ - 6 : : import {ERC165Checker} from "OZ/utils/introspection/ERC165Checker.sol"; - 7 : : /* ==== CMTAT === */ - 8 : : import {IRuleEngine, IRuleEngineERC1404} from "CMTAT/interfaces/engine/IRuleEngine.sol"; - 9 : : import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; - 10 : : import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; - 11 : : import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; - 12 : : - 13 : : /* ==== Modules === */ - 14 : : import {ERC3643ComplianceModule, IERC3643Compliance} from "./modules/ERC3643ComplianceModule.sol"; - 15 : : import {VersionModule} from "./modules/VersionModule.sol"; - 16 : : import {RulesManagementModule} from "./modules/RulesManagementModule.sol"; - 17 : : - 18 : : /* ==== Interface and other library === */ - 19 : : import {IRule} from "./interfaces/IRule.sol"; - 20 : : import {RuleEngineInvariantStorage} from "./modules/library/RuleEngineInvariantStorage.sol"; - 21 : : import {RuleInterfaceId} from "./modules/library/RuleInterfaceId.sol"; - 22 : : - 23 : : /** - 24 : : * @title Implementation of a ruleEngine as defined by the CMTAT - 25 : : */ - 26 : : abstract contract RuleEngineBase is - 27 : : VersionModule, - 28 : : RulesManagementModule, - 29 : : ERC3643ComplianceModule, - 30 : : RuleEngineInvariantStorage, - 31 : : IRuleEngineERC1404 - 32 : : { - 33 : : /* ============ State functions ============ */ - 34 : : /* - 35 : : * @inheritdoc IRuleEngine - 36 : : */ - 37 : 5 : function transferred(address spender, address from, address to, uint256 value) - 38 : : public - 39 : : virtual - 40 : : override(IRuleEngine) - 41 : : onlyBoundToken - 42 : : { - 43 : : // Apply on RuleEngine - 44 : 4 : RulesManagementModule._transferred(spender, from, to, value); - 45 : : } - 46 : : - 47 : : /** - 48 : : * @inheritdoc IERC3643IComplianceContract - 49 : : */ - 50 : 18 : function transferred(address from, address to, uint256 value) - 51 : : public - 52 : : virtual - 53 : : override(IERC3643IComplianceContract) - 54 : : onlyBoundToken - 55 : : { - 56 : 16 : _transferred(from, to, value); - 57 : : } - 58 : : - 59 : : /// @inheritdoc IERC3643Compliance - 60 : 4 : function created(address to, uint256 value) public virtual override(IERC3643Compliance) onlyBoundToken { - 61 : 2 : _transferred(address(0), to, value); + 6 : : import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; + 7 : : /* ==== CMTAT interface IDs === */ + 8 : : import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; + 9 : : import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; + 10 : : /* ==== CMTAT === */ + 11 : : import {IRuleEngine, IRuleEngineERC1404} from "CMTAT/interfaces/engine/IRuleEngine.sol"; + 12 : : import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; + 13 : : import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; + 14 : : import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; + 15 : : + 16 : : /* ==== Modules === */ + 17 : : import {ERC3643ComplianceModule, IERC3643Compliance} from "./modules/ERC3643ComplianceModule.sol"; + 18 : : import {VersionModule} from "./modules/VersionModule.sol"; + 19 : : import {RulesManagementModule} from "./modules/RulesManagementModule.sol"; + 20 : : + 21 : : /* ==== Interface and other library === */ + 22 : : import {IRule} from "./interfaces/IRule.sol"; + 23 : : import {ComplianceInterfaceId} from "./modules/library/ComplianceInterfaceId.sol"; + 24 : : import {ERC1404InterfaceId} from "./modules/library/ERC1404InterfaceId.sol"; + 25 : : import {RuleEngineInvariantStorage} from "./modules/library/RuleEngineInvariantStorage.sol"; + 26 : : import {RuleInterfaceId} from "./modules/library/RuleInterfaceId.sol"; + 27 : : + 28 : : /** + 29 : : * @title Implementation of a ruleEngine as defined by the CMTAT + 30 : : */ + 31 : : abstract contract RuleEngineBase is + 32 : : VersionModule, + 33 : : RulesManagementModule, + 34 : : ERC3643ComplianceModule, + 35 : : RuleEngineInvariantStorage, + 36 : : IRuleEngineERC1404 + 37 : : { + 38 : : /* ============ State functions ============ */ + 39 : : /* + 40 : : * @inheritdoc IRuleEngine + 41 : : */ + 42 : 5 : function transferred(address spender, address from, address to, uint256 value) + 43 : : public + 44 : : virtual + 45 : : override(IRuleEngine) + 46 : : onlyBoundToken + 47 : : { + 48 : : // Apply on RuleEngine + 49 : 4 : RulesManagementModule._transferred(spender, from, to, value); + 50 : : } + 51 : : + 52 : : /** + 53 : : * @inheritdoc IERC3643IComplianceContract + 54 : : */ + 55 : 18 : function transferred(address from, address to, uint256 value) + 56 : : public + 57 : : virtual + 58 : : override(IERC3643IComplianceContract) + 59 : : onlyBoundToken + 60 : : { + 61 : 16 : _transferred(from, to, value); 62 : : } 63 : : 64 : : /// @inheritdoc IERC3643Compliance - 65 : 4 : function destroyed(address from, uint256 value) public virtual override(IERC3643Compliance) onlyBoundToken { - 66 : 2 : _transferred(from, address(0), value); + 65 : 4 : function created(address to, uint256 value) public virtual override(IERC3643Compliance) onlyBoundToken { + 66 : 2 : _transferred(address(0), to, value); 67 : : } 68 : : - 69 : : /* ============ View functions ============ */ - 70 : : /** - 71 : : * @notice Go through all the rule to know if a restriction exists on the transfer - 72 : : * @param from the origin address - 73 : : * @param to the destination address - 74 : : * @param value to transfer - 75 : : * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK (0) if the transfer is valid - 76 : : * - 77 : : */ - 78 : 34 : function detectTransferRestriction(address from, address to, uint256 value) - 79 : : public - 80 : : view - 81 : : virtual - 82 : : override(IERC1404) - 83 : : returns (uint8) - 84 : : { - 85 : 59 : return _detectTransferRestriction(from, to, value); - 86 : : } - 87 : : - 88 : : /** - 89 : : * @inheritdoc IERC1404Extend - 90 : : */ - 91 : 18 : function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 92 : : public - 93 : : view - 94 : : virtual - 95 : : override(IERC1404Extend) - 96 : : returns (uint8) - 97 : : { - 98 : 39 : return _detectTransferRestrictionFrom(spender, from, to, value); - 99 : : } - 100 : : - 101 : : /** - 102 : : * @inheritdoc IERC1404 - 103 : : */ - 104 : 19 : function messageForTransferRestriction(uint8 restrictionCode) - 105 : : public - 106 : : view - 107 : : virtual - 108 : : override(IERC1404) - 109 : : returns (string memory) - 110 : : { - 111 : 19 : return _messageForTransferRestriction(restrictionCode); - 112 : : } - 113 : : - 114 : : /** - 115 : : * @inheritdoc IERC3643ComplianceRead - 116 : : */ - 117 : 25 : function canTransfer(address from, address to, uint256 value) - 118 : : public - 119 : : view - 120 : : virtual - 121 : : override(IERC3643ComplianceRead) - 122 : : returns (bool) - 123 : : { - 124 : 25 : return detectTransferRestriction(from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); - 125 : : } - 126 : : - 127 : : /** - 128 : : * @inheritdoc IERC7551Compliance - 129 : : */ - 130 : 21 : function canTransferFrom(address spender, address from, address to, uint256 value) - 131 : : public - 132 : : view - 133 : : virtual - 134 : : override(IERC7551Compliance) - 135 : : returns (bool) - 136 : : { - 137 : 21 : return detectTransferRestrictionFrom(spender, from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); - 138 : : } - 139 : : - 140 : : /*////////////////////////////////////////////////////////////// - 141 : : INTERNAL/PRIVATE FUNCTIONS - 142 : : //////////////////////////////////////////////////////////////*/ - 143 : 59 : function _detectTransferRestriction(address from, address to, uint256 value) internal view virtual returns (uint8) { - 144 : 59 : uint256 rulesLength = rulesCount(); - 145 : 59 : for (uint256 i = 0; i < rulesLength; ++i) { - 146 : 59 : uint8 restriction = IRule(rule(i)).detectTransferRestriction(from, to, value); - 147 [ + ]: 59 : if (restriction > 0) { - 148 : 43 : return restriction; - 149 : : } - 150 : : } - 151 : 16 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); - 152 : : } - 153 : : - 154 : 39 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 155 : : internal - 156 : : view - 157 : : virtual - 158 : : returns (uint8) - 159 : : { - 160 : 39 : uint256 rulesLength = rulesCount(); - 161 : 39 : for (uint256 i = 0; i < rulesLength; ++i) { - 162 : 39 : uint8 restriction = IRule(rule(i)).detectTransferRestrictionFrom(spender, from, to, value); - 163 [ + ]: 39 : if (restriction > 0) { - 164 : 29 : return restriction; - 165 : : } - 166 : : } - 167 : 10 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); - 168 : : } - 169 : : - 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 : : } + 69 : : /// @inheritdoc IERC3643Compliance + 70 : 4 : function destroyed(address from, uint256 value) public virtual override(IERC3643Compliance) onlyBoundToken { + 71 : 2 : _transferred(from, address(0), value); + 72 : : } + 73 : : + 74 : : /* ============ View functions ============ */ + 75 : : /** + 76 : : * @notice Go through all the rule to know if a restriction exists on the transfer + 77 : : * @param from the origin address + 78 : : * @param to the destination address + 79 : : * @param value to transfer + 80 : : * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK (0) if the transfer is valid + 81 : : * + 82 : : */ + 83 : 34 : function detectTransferRestriction(address from, address to, uint256 value) + 84 : : public + 85 : : view + 86 : : virtual + 87 : : override(IERC1404) + 88 : : returns (uint8) + 89 : : { + 90 : 59 : return _detectTransferRestriction(from, to, value); + 91 : : } + 92 : : + 93 : : /** + 94 : : * @inheritdoc IERC1404Extend + 95 : : */ + 96 : 18 : function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 97 : : public + 98 : : view + 99 : : virtual + 100 : : override(IERC1404Extend) + 101 : : returns (uint8) + 102 : : { + 103 : 39 : return _detectTransferRestrictionFrom(spender, from, to, value); + 104 : : } + 105 : : + 106 : : /** + 107 : : * @inheritdoc IERC1404 + 108 : : */ + 109 : 19 : function messageForTransferRestriction(uint8 restrictionCode) + 110 : : public + 111 : : view + 112 : : virtual + 113 : : override(IERC1404) + 114 : : returns (string memory) + 115 : : { + 116 : 19 : return _messageForTransferRestriction(restrictionCode); + 117 : : } + 118 : : + 119 : : /** + 120 : : * @inheritdoc IERC3643ComplianceRead + 121 : : */ + 122 : 25 : function canTransfer(address from, address to, uint256 value) + 123 : : public + 124 : : view + 125 : : virtual + 126 : : override(IERC3643ComplianceRead) + 127 : : returns (bool) + 128 : : { + 129 : 25 : return detectTransferRestriction(from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 130 : : } + 131 : : + 132 : : /** + 133 : : * @inheritdoc IERC7551Compliance + 134 : : */ + 135 : 21 : function canTransferFrom(address spender, address from, address to, uint256 value) + 136 : : public + 137 : : view + 138 : : virtual + 139 : : override(IERC7551Compliance) + 140 : : returns (bool) + 141 : : { + 142 : 21 : return detectTransferRestrictionFrom(spender, from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 143 : : } + 144 : : + 145 : : /*////////////////////////////////////////////////////////////// + 146 : : INTERNAL/PRIVATE FUNCTIONS + 147 : : //////////////////////////////////////////////////////////////*/ + 148 : 59 : function _detectTransferRestriction(address from, address to, uint256 value) internal view virtual returns (uint8) { + 149 : 59 : uint256 rulesLength = rulesCount(); + 150 : 59 : for (uint256 i = 0; i < rulesLength; ++i) { + 151 : 59 : uint8 restriction = IRule(rule(i)).detectTransferRestriction(from, to, value); + 152 [ + ]: 59 : if (restriction > 0) { + 153 : 43 : return restriction; + 154 : : } + 155 : : } + 156 : 16 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 157 : : } + 158 : : + 159 : 39 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 160 : : internal + 161 : : view + 162 : : virtual + 163 : : returns (uint8) + 164 : : { + 165 : 39 : uint256 rulesLength = rulesCount(); + 166 : 39 : for (uint256 i = 0; i < rulesLength; ++i) { + 167 : 39 : uint8 restriction = IRule(rule(i)).detectTransferRestrictionFrom(spender, from, to, value); + 168 [ + ]: 39 : if (restriction > 0) { + 169 : 29 : return restriction; + 170 : : } + 171 : : } + 172 : 10 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 173 : : } + 174 : : + 175 : : /** + 176 : : * @dev This function returns the message from the first rule claiming the code. + 177 : : * Rule designers should keep restriction codes unique across rules. + 178 : : * If a code is shared intentionally, all rules using that code should return + 179 : : * the same message to avoid ambiguous operator feedback. + 180 : : */ + 181 : 19 : function _messageForTransferRestriction(uint8 restrictionCode) internal view virtual returns (string memory) { + 182 : 19 : uint256 rulesLength = rulesCount(); + 183 : 19 : for (uint256 i = 0; i < rulesLength; ++i) { + 184 [ + ]: 16 : if (IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)) { + 185 : 14 : return IRule(rule(i)).messageForTransferRestriction(restrictionCode); + 186 : : } + 187 : : } + 188 : 5 : return "Unknown restriction code"; + 189 : : } + 190 : : + 191 : : /** + 192 : : * @dev Override to add ERC-165 interface check for the full IRule hierarchy. + 193 : : */ + 194 : 253 : function _checkRule(address rule_) internal view virtual override { + 195 : 253 : RulesManagementModule._checkRule(rule_); + 196 [ + ]: 244 : if (!ERC165Checker.supportsInterface(rule_, RuleInterfaceId.IRULE_INTERFACE_ID)) { + 197 : 6 : revert RuleEngine_RuleInvalidInterface(); + 198 : : } + 199 : : } + 200 : : + 201 : : /** + 202 : : * @dev Shared ERC-165 checks common to all RuleEngine deployment variants. + 203 : : * Concrete deployments can extend this with access-control-specific interfaces. + 204 : : */ + 205 : 46 : function _supportsRuleEngineBaseInterface(bytes4 interfaceId) internal pure returns (bool) { + 206 : 46 : return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + 207 : 41 : || interfaceId == ERC1404InterfaceId.IERC1404_INTERFACE_ID + 208 : 31 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + 209 : 26 : || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID + 210 : 21 : || interfaceId == ComplianceInterfaceId.IERC7551_COMPLIANCE_INTERFACE_ID; + 211 : : } + 212 : : } diff --git a/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func-sort-c.html b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func-sort-c.html new file mode 100644 index 0000000..2d808cf --- /dev/null +++ b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func-sort-c.html @@ -0,0 +1,105 @@ + + + + + + + LCOV - lcov.info - src/RuleEngineOwnableShared.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src - RuleEngineOwnableShared.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1515100.0 %
Date:2026-05-05 13:03:55Functions:66100.0 %
Branches:22100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleEngineOwnableShared._msgData2
RuleEngineOwnableShared._checkOwnershipTransferTarget10
RuleEngineOwnableShared.supportsInterface29
RuleEngineOwnableShared.constructor128
RuleEngineOwnableShared._msgSender181
RuleEngineOwnableShared._contextSuffixLength183
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func.html b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func.html new file mode 100644 index 0000000..2f76325 --- /dev/null +++ b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func.html @@ -0,0 +1,105 @@ + + + + + + + LCOV - lcov.info - src/RuleEngineOwnableShared.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src - RuleEngineOwnableShared.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1515100.0 %
Date:2026-05-05 13:03:55Functions:66100.0 %
Branches:22100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleEngineOwnableShared._checkOwnershipTransferTarget10
RuleEngineOwnableShared._contextSuffixLength183
RuleEngineOwnableShared._msgData2
RuleEngineOwnableShared._msgSender181
RuleEngineOwnableShared.constructor128
RuleEngineOwnableShared.supportsInterface29
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.gcov.html b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.gcov.html new file mode 100644 index 0000000..96d5846 --- /dev/null +++ b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.gcov.html @@ -0,0 +1,152 @@ + + + + + + + LCOV - lcov.info - src/RuleEngineOwnableShared.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src - RuleEngineOwnableShared.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1515100.0 %
Date:2026-05-05 13:03:55Functions:66100.0 %
Branches:22100.0 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.20;
+       4                 :            : 
+       5                 :            : /* ==== OpenZeppelin === */
+       6                 :            : import {Context} from "@openzeppelin/contracts/utils/Context.sol";
+       7                 :            : import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
+       8                 :            : import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+       9                 :            : /* ==== Modules === */
+      10                 :            : import {ERC2771ModuleStandalone, ERC2771Context} from "./modules/ERC2771ModuleStandalone.sol";
+      11                 :            : /* ==== Base contract === */
+      12                 :            : import {RuleEngineBase} from "./RuleEngineBase.sol";
+      13                 :            : import {OwnableInterfaceId} from "./modules/library/OwnableInterfaceId.sol";
+      14                 :            : import {IRule} from "./interfaces/IRule.sol";
+      15                 :            : 
+      16                 :            : /**
+      17                 :            :  * @title Shared Ownable deployment logic for RuleEngine variants
+      18                 :            :  * @dev Kept abstract to let child contracts choose the ownership mechanism
+      19                 :            :  * (`Ownable` or `Ownable2Step`) while reusing constructor, ERC-165 and ERC-2771 code.
+      20                 :            :  */
+      21                 :            : abstract contract RuleEngineOwnableShared is ERC2771ModuleStandalone, RuleEngineBase, ERC165 {
+      22                 :        128 :     constructor(address forwarderIrrevocable, address tokenContract) ERC2771ModuleStandalone(forwarderIrrevocable) {
+      23            [ + ]:        128 :         if (tokenContract != address(0)) {
+      24                 :          1 :             _bindToken(tokenContract);
+      25                 :            :         }
+      26                 :            :     }
+      27                 :            : 
+      28                 :            :     /* ============ ERC-165 ============ */
+      29                 :         29 :     function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
+      30                 :         29 :         return _supportsRuleEngineBaseInterface(interfaceId) || interfaceId == OwnableInterfaceId.IERC173_INTERFACE_ID
+      31                 :          5 :             || ERC165.supportsInterface(interfaceId);
+      32                 :            :     }
+      33                 :            : 
+      34                 :            :     /**
+      35                 :            :      * @dev Shared guard for ownership transfer targets in ownable variants.
+      36                 :            :      */
+      37                 :         10 :     function _checkOwnershipTransferTarget(address newOwner) internal view virtual {
+      38            [ + ]:         10 :         if (containsRule(IRule(newOwner))) {
+      39                 :          2 :             revert RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges();
+      40                 :            :         }
+      41                 :            :     }
+      42                 :            : 
+      43                 :            :     /*//////////////////////////////////////////////////////////////
+      44                 :            :                            ERC-2771
+      45                 :            :     //////////////////////////////////////////////////////////////*/
+      46                 :            : 
+      47                 :            :     /**
+      48                 :            :      * @dev This surcharge is not necessary if you do not use the MetaTxModule
+      49                 :            :      */
+      50                 :        181 :     function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) {
+      51                 :        181 :         return ERC2771Context._msgSender();
+      52                 :            :     }
+      53                 :            : 
+      54                 :            :     /**
+      55                 :            :      * @dev This surcharge is not necessary if you do not use the MetaTxModule
+      56                 :            :      */
+      57                 :          2 :     function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) {
+      58                 :          2 :         return ERC2771Context._msgData();
+      59                 :            :     }
+      60                 :            : 
+      61                 :            :     /**
+      62                 :            :      * @dev This surcharge is not necessary if you do not use the MetaTxModule
+      63                 :            :      */
+      64                 :        183 :     function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
+      65                 :        183 :         return ERC2771Context._contextSuffixLength();
+      66                 :            :     }
+      67                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/deployment/RuleEngine.sol.func-sort-c.html b/doc/coverage/coverage/src/deployment/RuleEngine.sol.func-sort-c.html index c376ad7..be7f6ec 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngine.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/deployment/RuleEngine.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 24 - 24 + 25 + 25 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 8 - 8 + 10 + 10 100.0 % @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 5 + 5 100.0 % @@ -69,36 +69,44 @@ Hit count Sort by hit count - RuleEngine._msgData + RuleEngine._msgData 1 - RuleEngine.supportsInterface - 6 + RuleEngine._onlyRulesLimitManager + 5 - RuleEngine._onlyComplianceManager - 15 + RuleEngine.supportsInterface + 17 - RuleEngine.constructor - 127 + RuleEngine._onlyComplianceManager + 18 - RuleEngine.hasRole - 135 + RuleEngine.grantRole + 22 - RuleEngine._onlyRulesManager - 170 + RuleEngine.constructor + 146 - RuleEngine._msgSender - 366 + RuleEngine.hasRole + 154 - RuleEngine._contextSuffixLength - 367 + RuleEngine._onlyRulesManager + 196 + + + RuleEngine._msgSender + 450 + + + RuleEngine._contextSuffixLength + 451
diff --git a/doc/coverage/coverage/src/deployment/RuleEngine.sol.func.html b/doc/coverage/coverage/src/deployment/RuleEngine.sol.func.html index ebb8602..1cb351b 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngine.sol.func.html +++ b/doc/coverage/coverage/src/deployment/RuleEngine.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 24 - 24 + 25 + 25 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 8 - 8 + 10 + 10 100.0 % @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 5 + 5 100.0 % @@ -69,36 +69,44 @@ Hit count Sort by hit count - RuleEngine._contextSuffixLength - 367 + RuleEngine._contextSuffixLength + 451 - RuleEngine._msgData + RuleEngine._msgData 1 - RuleEngine._msgSender - 366 + RuleEngine._msgSender + 450 - RuleEngine._onlyComplianceManager - 15 + RuleEngine._onlyComplianceManager + 18 - RuleEngine._onlyRulesManager - 170 + RuleEngine._onlyRulesLimitManager + 5 - RuleEngine.constructor - 127 + RuleEngine._onlyRulesManager + 196 - RuleEngine.hasRole - 135 + RuleEngine.constructor + 146 - RuleEngine.supportsInterface - 6 + RuleEngine.grantRole + 22 + + + RuleEngine.hasRole + 154 + + + RuleEngine.supportsInterface + 17
diff --git a/doc/coverage/coverage/src/deployment/RuleEngine.sol.gcov.html b/doc/coverage/coverage/src/deployment/RuleEngine.sol.gcov.html index 63031d1..25a82b7 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngine.sol.gcov.html +++ b/doc/coverage/coverage/src/deployment/RuleEngine.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 24 - 24 + 25 + 25 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 8 - 8 + 10 + 10 100.0 % @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 5 + 5 100.0 % @@ -73,88 +73,109 @@ 2 : : 3 : : pragma solidity ^0.8.20; 4 : : - 5 : : /* ==== CMTAT === */ - 6 : : import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; - 7 : : import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; - 8 : : /* ==== OpenZeppelin === */ - 9 : : import {Context} from "OZ/utils/Context.sol"; - 10 : : import {AccessControl} from "OZ/access/AccessControl.sol"; - 11 : : import {IERC165} from "OZ/utils/introspection/ERC165.sol"; + 5 : : /* ==== OpenZeppelin === */ + 6 : : import {Context} from "@openzeppelin/contracts/utils/Context.sol"; + 7 : : import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; + 8 : : import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + 9 : : import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; + 10 : : import {IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + 11 : : import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.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 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 : : } + 16 : : + 17 : : /** + 18 : : * @title Implementation of a ruleEngine as defined by the CMTAT + 19 : : */ + 20 : : contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase, AccessControlEnumerable { + 21 : : using EnumerableSet for EnumerableSet.AddressSet; + 22 : : + 23 : : /** + 24 : : * @param admin Address of the contract (Access Control) + 25 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + 26 : : */ + 27 : 146 : constructor(address admin, address forwarderIrrevocable, address tokenContract) + 28 : : ERC2771ModuleStandalone(forwarderIrrevocable) + 29 : : { + 30 [ + ]: 146 : if (admin == address(0)) { + 31 : 1 : revert RuleEngine_AdminWithAddressZeroNotAllowed(); + 32 : : } + 33 [ + ]: 145 : if (tokenContract != address(0)) { + 34 : 31 : _bindToken(tokenContract); + 35 : : } + 36 : 145 : _grantRole(DEFAULT_ADMIN_ROLE, admin); + 37 : : } + 38 : : + 39 : : /* ============ ACCESS CONTROL ============ */ + 40 : : /** + 41 : : * @notice Grants `role` to `account`. + 42 : : * @dev Prevents granting any role to accounts already configured as rules. + 43 : : */ + 44 : 22 : function grantRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { + 45 [ + ]: 22 : if (_rules.contains(account)) { + 46 : 3 : revert RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges(); + 47 : : } + 48 : 19 : AccessControl.grantRole(role, account); 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(); + 51 : : /** + 52 : : * @notice Returns `true` if `account` has been granted `role`. + 53 : : * @dev The Default Admin has all roles + 54 : : */ + 55 : 154 : function hasRole(bytes32 role, address account) + 56 : : public + 57 : : view + 58 : : virtual + 59 : : override(AccessControl, IAccessControl) + 60 : : returns (bool) + 61 : : { + 62 [ + + ]: 413 : if (AccessControl.hasRole(DEFAULT_ADMIN_ROLE, account)) { + 63 : 219 : return true; + 64 : : } else { + 65 : 194 : return AccessControl.hasRole(role, account); + 66 : : } + 67 : : } + 68 : : + 69 : : /* ============ ERC-165 ============ */ + 70 : 17 : function supportsInterface(bytes4 interfaceId) + 71 : : public + 72 : : view + 73 : : virtual + 74 : : override(AccessControlEnumerable, IERC165) + 75 : : returns (bool) + 76 : : { + 77 : 17 : return _supportsRuleEngineBaseInterface(interfaceId) || AccessControlEnumerable.supportsInterface(interfaceId); 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 : : } + 80 : : /*////////////////////////////////////////////////////////////// + 81 : : ERC-2771 + 82 : : //////////////////////////////////////////////////////////////*/ + 83 : 18 : function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + 84 : 196 : function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + 85 : 5 : function _onlyRulesLimitManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + 86 : : + 87 : : /** + 88 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 89 : : */ + 90 : 450 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { + 91 : 450 : return ERC2771Context._msgSender(); + 92 : : } + 93 : : + 94 : : /** + 95 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 96 : : */ + 97 : 1 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { + 98 : 1 : return ERC2771Context._msgData(); + 99 : : } + 100 : : + 101 : : /** + 102 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 103 : : */ + 104 : 451 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { + 105 : 451 : return ERC2771Context._contextSuffixLength(); + 106 : : } + 107 : : } diff --git a/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func-sort-c.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func-sort-c.html index cac79cd..43de197 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func-sort-c.html @@ -31,13 +31,13 @@ lcov.info Lines: - 18 - 18 + 12 + 12 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: 7 @@ -49,9 +49,9 @@ Branches: - 1 - 1 - 100.0 % + 0 + 0 + - @@ -69,32 +69,32 @@ Hit count Sort by hit count - RuleEngineOwnable._msgData + RuleEngineOwnable._msgData 1 - RuleEngineOwnable.supportsInterface - 9 + RuleEngineOwnable._onlyRulesLimitManager + 2 - RuleEngineOwnable._onlyComplianceManager - 21 + RuleEngineOwnable.transferOwnership + 5 - RuleEngineOwnable._onlyRulesManager - 64 + RuleEngineOwnable._onlyComplianceManager + 22 - RuleEngineOwnable.constructor - 77 + RuleEngineOwnable._onlyRulesManager + 65 - RuleEngineOwnable._msgSender - 108 + RuleEngineOwnable._msgSender + 146 - RuleEngineOwnable._contextSuffixLength - 109 + RuleEngineOwnable._contextSuffixLength + 147
diff --git a/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func.html index 2714e56..778686c 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func.html +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func.html @@ -31,13 +31,13 @@ lcov.info Lines: - 18 - 18 + 12 + 12 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: 7 @@ -49,9 +49,9 @@ Branches: - 1 - 1 - 100.0 % + 0 + 0 + - @@ -69,32 +69,32 @@ Hit count Sort by hit count - RuleEngineOwnable._contextSuffixLength - 109 + RuleEngineOwnable._contextSuffixLength + 147 - RuleEngineOwnable._msgData + RuleEngineOwnable._msgData 1 - RuleEngineOwnable._msgSender - 108 + RuleEngineOwnable._msgSender + 146 - RuleEngineOwnable._onlyComplianceManager - 21 + RuleEngineOwnable._onlyComplianceManager + 22 - RuleEngineOwnable._onlyRulesManager - 64 + RuleEngineOwnable._onlyRulesLimitManager + 2 - RuleEngineOwnable.constructor - 77 + RuleEngineOwnable._onlyRulesManager + 65 - RuleEngineOwnable.supportsInterface - 9 + RuleEngineOwnable.transferOwnership + 5
diff --git a/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.gcov.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.gcov.html index 86ca861..c3c3e37 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.gcov.html +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.gcov.html @@ -31,13 +31,13 @@ lcov.info Lines: - 18 - 18 + 12 + 12 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: 7 @@ -49,9 +49,9 @@ Branches: - 1 - 1 - 100.0 % + 0 + 0 + - @@ -73,87 +73,66 @@ 2 : : 3 : : pragma solidity ^0.8.20; 4 : : - 5 : : /* ==== CMTAT === */ - 6 : : import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; - 7 : : import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; - 8 : : /* ==== OpenZeppelin === */ - 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 : : /* ==== 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 - 20 : : */ - 21 : : contract RuleEngineOwnable is ERC2771ModuleStandalone, RuleEngineBase, Ownable { - 22 : : bytes4 private constant ERC173_INTERFACE_ID = 0x7f5828d0; - 23 : : + 5 : : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + 6 : : import {Context} from "@openzeppelin/contracts/utils/Context.sol"; + 7 : : import {RuleEngineOwnableShared} from "../RuleEngineOwnableShared.sol"; + 8 : : + 9 : : /** + 10 : : * @title Implementation of a ruleEngine with ERC-173 Ownable access control + 11 : : */ + 12 : : contract RuleEngineOwnable is RuleEngineOwnableShared, Ownable { + 13 : : /** + 14 : : * @param owner_ Address of the contract owner (ERC-173) + 15 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + 16 : : * @param tokenContract Address of the token contract to bind (can be zero address) + 17 : : */ + 18 : : constructor(address owner_, address forwarderIrrevocable, address tokenContract) + 19 : : RuleEngineOwnableShared(forwarderIrrevocable, tokenContract) + 20 : : Ownable(owner_) + 21 : : {} + 22 : : + 23 : : /* ============ ACCESS CONTROL ============ */ 24 : : /** - 25 : : * @param owner_ Address of the contract owner (ERC-173) - 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 : 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 [ + ]: 76 : if (tokenContract != address(0)) { - 36 : 1 : _bindToken(tokenContract); - 37 : : } - 38 : : } - 39 : : - 40 : : /* ============ ACCESS CONTROL ============ */ - 41 : : /** - 42 : : * @dev Access control check using Ownable pattern - 43 : : */ - 44 : 64 : function _onlyRulesManager() internal virtual override onlyOwner {} - 45 : : - 46 : : /** - 47 : : * @dev Access control check using Ownable pattern - 48 : : */ - 49 : 21 : function _onlyComplianceManager() internal virtual override onlyOwner {} + 25 : : * @dev Access control check using Ownable pattern + 26 : : */ + 27 : 65 : function _onlyRulesManager() internal virtual override onlyOwner {} + 28 : 2 : function _onlyRulesLimitManager() internal virtual override onlyOwner {} + 29 : : + 30 : : /** + 31 : : * @dev Access control check using Ownable pattern + 32 : : */ + 33 : 22 : function _onlyComplianceManager() internal virtual override onlyOwner {} + 34 : : + 35 : : /** + 36 : : * @notice Transfers ownership of the contract to a new account (`newOwner`). + 37 : : * @dev Reverts when `newOwner` is already configured as a rule. + 38 : : */ + 39 : 5 : function transferOwnership(address newOwner) public virtual override onlyOwner { + 40 : 4 : RuleEngineOwnableShared._checkOwnershipTransferTarget(newOwner); + 41 : 3 : Ownable.transferOwnership(newOwner); + 42 : : } + 43 : : + 44 : : /** + 45 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 46 : : */ + 47 : 146 : function _msgSender() internal view virtual override(RuleEngineOwnableShared, Context) returns (address sender) { + 48 : 146 : return RuleEngineOwnableShared._msgSender(); + 49 : : } 50 : : - 51 : : /* ============ ERC-165 ============ */ - 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 : : } + 51 : : /** + 52 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 53 : : */ + 54 : 1 : function _msgData() internal view virtual override(RuleEngineOwnableShared, Context) returns (bytes calldata) { + 55 : 1 : return RuleEngineOwnableShared._msgData(); + 56 : : } + 57 : : + 58 : : /** + 59 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 60 : : */ + 61 : 147 : function _contextSuffixLength() internal view virtual override(RuleEngineOwnableShared, Context) returns (uint256) { + 62 : 147 : return RuleEngineOwnableShared._contextSuffixLength(); + 63 : : } + 64 : : } diff --git a/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func-sort-c.html new file mode 100644 index 0000000..d43fc3b --- /dev/null +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func-sort-c.html @@ -0,0 +1,109 @@ + + + + + + + LCOV - lcov.info - src/deployment/RuleEngineOwnable2Step.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deployment - RuleEngineOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1212100.0 %
Date:2026-05-05 13:03:55Functions:77100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleEngineOwnable2Step._msgData1
RuleEngineOwnable2Step._onlyRulesLimitManager2
RuleEngineOwnable2Step._onlyComplianceManager3
RuleEngineOwnable2Step._onlyRulesManager5
RuleEngineOwnable2Step.transferOwnership6
RuleEngineOwnable2Step._msgSender35
RuleEngineOwnable2Step._contextSuffixLength36
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func.html new file mode 100644 index 0000000..95204c6 --- /dev/null +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func.html @@ -0,0 +1,109 @@ + + + + + + + LCOV - lcov.info - src/deployment/RuleEngineOwnable2Step.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deployment - RuleEngineOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1212100.0 %
Date:2026-05-05 13:03:55Functions:77100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleEngineOwnable2Step._contextSuffixLength36
RuleEngineOwnable2Step._msgData1
RuleEngineOwnable2Step._msgSender35
RuleEngineOwnable2Step._onlyComplianceManager3
RuleEngineOwnable2Step._onlyRulesLimitManager2
RuleEngineOwnable2Step._onlyRulesManager5
RuleEngineOwnable2Step.transferOwnership6
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.gcov.html new file mode 100644 index 0000000..7d77041 --- /dev/null +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.gcov.html @@ -0,0 +1,151 @@ + + + + + + + LCOV - lcov.info - src/deployment/RuleEngineOwnable2Step.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deployment - RuleEngineOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1212100.0 %
Date:2026-05-05 13:03:55Functions:77100.0 %
Branches:00-
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.20;
+       4                 :            : 
+       5                 :            : /* ==== OpenZeppelin === */
+       6                 :            : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
+       7                 :            : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
+       8                 :            : import {Context} from "@openzeppelin/contracts/utils/Context.sol";
+       9                 :            : import {RuleEngineOwnableShared} from "../RuleEngineOwnableShared.sol";
+      10                 :            : 
+      11                 :            : /**
+      12                 :            :  * @title Implementation of a ruleEngine with ERC-173 Ownable2Step access control
+      13                 :            :  */
+      14                 :            : contract RuleEngineOwnable2Step is RuleEngineOwnableShared, Ownable2Step {
+      15                 :            :     /**
+      16                 :            :      * @param owner_ Address of the contract owner (ERC-173)
+      17                 :            :      * @param forwarderIrrevocable Address of the forwarder, required for the gasless support
+      18                 :            :      * @param tokenContract Address of the token contract to bind (can be zero address)
+      19                 :            :      */
+      20                 :            :     constructor(address owner_, address forwarderIrrevocable, address tokenContract)
+      21                 :            :         RuleEngineOwnableShared(forwarderIrrevocable, tokenContract)
+      22                 :            :         Ownable(owner_)
+      23                 :            :     {}
+      24                 :            : 
+      25                 :            :     /* ============ ACCESS CONTROL ============ */
+      26                 :            :     /**
+      27                 :            :      * @dev Access control check using Ownable pattern
+      28                 :            :      */
+      29                 :          5 :     function _onlyRulesManager() internal virtual override onlyOwner {}
+      30                 :          2 :     function _onlyRulesLimitManager() internal virtual override onlyOwner {}
+      31                 :            : 
+      32                 :            :     /**
+      33                 :            :      * @dev Access control check using Ownable pattern
+      34                 :            :      */
+      35                 :          3 :     function _onlyComplianceManager() internal virtual override onlyOwner {}
+      36                 :            : 
+      37                 :            :     /**
+      38                 :            :      * @notice Starts ownership transfer to `newOwner`.
+      39                 :            :      * @dev Reverts when `newOwner` is already configured as a rule.
+      40                 :            :      */
+      41                 :          6 :     function transferOwnership(address newOwner) public virtual override onlyOwner {
+      42                 :          6 :         RuleEngineOwnableShared._checkOwnershipTransferTarget(newOwner);
+      43                 :          5 :         Ownable2Step.transferOwnership(newOwner);
+      44                 :            :     }
+      45                 :            : 
+      46                 :            :     /**
+      47                 :            :      * @dev This surcharge is not necessary if you do not use the MetaTxModule
+      48                 :            :      */
+      49                 :         35 :     function _msgSender() internal view virtual override(RuleEngineOwnableShared, Context) returns (address sender) {
+      50                 :         35 :         return RuleEngineOwnableShared._msgSender();
+      51                 :            :     }
+      52                 :            : 
+      53                 :            :     /**
+      54                 :            :      * @dev This surcharge is not necessary if you do not use the MetaTxModule
+      55                 :            :      */
+      56                 :          1 :     function _msgData() internal view virtual override(RuleEngineOwnableShared, Context) returns (bytes calldata) {
+      57                 :          1 :         return RuleEngineOwnableShared._msgData();
+      58                 :            :     }
+      59                 :            : 
+      60                 :            :     /**
+      61                 :            :      * @dev This surcharge is not necessary if you do not use the MetaTxModule
+      62                 :            :      */
+      63                 :         36 :     function _contextSuffixLength() internal view virtual override(RuleEngineOwnableShared, Context) returns (uint256) {
+      64                 :         36 :         return RuleEngineOwnableShared._contextSuffixLength();
+      65                 :            :     }
+      66                 :            : }
+
+
+
+ + + + +
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 index f15808a..ed7dd14 100644 --- a/doc/coverage/coverage/src/deployment/index-sort-b.html +++ b/doc/coverage/coverage/src/deployment/index-sort-b.html @@ -31,17 +31,17 @@ lcov.info Lines: - 42 - 42 + 49 + 49 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 15 - 15 + 24 + 24 100.0 % @@ -87,11 +87,23 @@
100.0%
100.0 % - 18 / 18 + 12 / 12 100.0 % 7 / 7 + - + 0 / 0 + + + RuleEngineOwnable2Step.sol + +
100.0%
+ + 100.0 % + 12 / 12 100.0 % - 1 / 1 + 7 / 7 + - + 0 / 0 RuleEngine.sol @@ -99,11 +111,11 @@
100.0%
100.0 % - 24 / 24 + 25 / 25 100.0 % - 8 / 8 + 10 / 10 100.0 % - 4 / 4 + 5 / 5 diff --git a/doc/coverage/coverage/src/deployment/index-sort-f.html b/doc/coverage/coverage/src/deployment/index-sort-f.html index c49a9ca..465762f 100644 --- a/doc/coverage/coverage/src/deployment/index-sort-f.html +++ b/doc/coverage/coverage/src/deployment/index-sort-f.html @@ -31,17 +31,17 @@ lcov.info Lines: - 42 - 42 + 49 + 49 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 15 - 15 + 24 + 24 100.0 % @@ -87,11 +87,23 @@
100.0%
100.0 % - 18 / 18 + 12 / 12 100.0 % 7 / 7 + - + 0 / 0 + + + RuleEngineOwnable2Step.sol + +
100.0%
+ + 100.0 % + 12 / 12 100.0 % - 1 / 1 + 7 / 7 + - + 0 / 0 RuleEngine.sol @@ -99,11 +111,11 @@
100.0%
100.0 % - 24 / 24 + 25 / 25 100.0 % - 8 / 8 + 10 / 10 100.0 % - 4 / 4 + 5 / 5 diff --git a/doc/coverage/coverage/src/deployment/index-sort-l.html b/doc/coverage/coverage/src/deployment/index-sort-l.html index 298c674..761e2e1 100644 --- a/doc/coverage/coverage/src/deployment/index-sort-l.html +++ b/doc/coverage/coverage/src/deployment/index-sort-l.html @@ -31,17 +31,17 @@ lcov.info Lines: - 42 - 42 + 49 + 49 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 15 - 15 + 24 + 24 100.0 % @@ -87,11 +87,23 @@
100.0%
100.0 % - 18 / 18 + 12 / 12 100.0 % 7 / 7 + - + 0 / 0 + + + RuleEngineOwnable2Step.sol + +
100.0%
+ + 100.0 % + 12 / 12 100.0 % - 1 / 1 + 7 / 7 + - + 0 / 0 RuleEngine.sol @@ -99,11 +111,11 @@
100.0%
100.0 % - 24 / 24 + 25 / 25 100.0 % - 8 / 8 + 10 / 10 100.0 % - 4 / 4 + 5 / 5 diff --git a/doc/coverage/coverage/src/deployment/index.html b/doc/coverage/coverage/src/deployment/index.html index c7383b6..02b7147 100644 --- a/doc/coverage/coverage/src/deployment/index.html +++ b/doc/coverage/coverage/src/deployment/index.html @@ -31,17 +31,17 @@ lcov.info Lines: - 42 - 42 + 49 + 49 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 15 - 15 + 24 + 24 100.0 % @@ -87,11 +87,11 @@
100.0%
100.0 % - 24 / 24 + 25 / 25 100.0 % - 8 / 8 + 10 / 10 100.0 % - 4 / 4 + 5 / 5 RuleEngineOwnable.sol @@ -99,11 +99,23 @@
100.0%
100.0 % - 18 / 18 + 12 / 12 100.0 % 7 / 7 + - + 0 / 0 + + + RuleEngineOwnable2Step.sol + +
100.0%
+ + 100.0 % + 12 / 12 100.0 % - 1 / 1 + 7 / 7 + - + 0 / 0 diff --git a/doc/coverage/coverage/src/index-sort-b.html b/doc/coverage/coverage/src/index-sort-b.html index 9424617..bd41fc0 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: - 42 - 42 + 63 + 63 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 13 - 13 + 20 + 20 100.0 % @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 6 + 6 100.0 % @@ -81,15 +81,27 @@ Functions Sort by function coverage Branches Sort by branch coverage + + RuleEngineOwnableShared.sol + +
100.0%
+ + 100.0 % + 15 / 15 + 100.0 % + 6 / 6 + 100.0 % + 2 / 2 + RuleEngineBase.sol
100.0%
100.0 % - 42 / 42 + 48 / 48 100.0 % - 13 / 13 + 14 / 14 100.0 % 4 / 4 diff --git a/doc/coverage/coverage/src/index-sort-f.html b/doc/coverage/coverage/src/index-sort-f.html index b39eba9..6a0b8cd 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: - 42 - 42 + 63 + 63 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 13 - 13 + 20 + 20 100.0 % @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 6 + 6 100.0 % @@ -81,15 +81,27 @@ Functions Sort by function coverage Branches Sort by branch coverage + + RuleEngineOwnableShared.sol + +
100.0%
+ + 100.0 % + 15 / 15 + 100.0 % + 6 / 6 + 100.0 % + 2 / 2 + RuleEngineBase.sol
100.0%
100.0 % - 42 / 42 + 48 / 48 100.0 % - 13 / 13 + 14 / 14 100.0 % 4 / 4 diff --git a/doc/coverage/coverage/src/index-sort-l.html b/doc/coverage/coverage/src/index-sort-l.html index b0d14a3..36d479c 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: - 42 - 42 + 63 + 63 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 13 - 13 + 20 + 20 100.0 % @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 6 + 6 100.0 % @@ -81,15 +81,27 @@ Functions Sort by function coverage Branches Sort by branch coverage + + RuleEngineOwnableShared.sol + +
100.0%
+ + 100.0 % + 15 / 15 + 100.0 % + 6 / 6 + 100.0 % + 2 / 2 + RuleEngineBase.sol
100.0%
100.0 % - 42 / 42 + 48 / 48 100.0 % - 13 / 13 + 14 / 14 100.0 % 4 / 4 diff --git a/doc/coverage/coverage/src/index.html b/doc/coverage/coverage/src/index.html index 26bcfa8..eaa4aa1 100644 --- a/doc/coverage/coverage/src/index.html +++ b/doc/coverage/coverage/src/index.html @@ -31,17 +31,17 @@ lcov.info Lines: - 42 - 42 + 63 + 63 100.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 13 - 13 + 20 + 20 100.0 % @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 6 + 6 100.0 % @@ -87,12 +87,24 @@
100.0%
100.0 % - 42 / 42 + 48 / 48 100.0 % - 13 / 13 + 14 / 14 100.0 % 4 / 4 + + RuleEngineOwnableShared.sol + +
100.0%
+ + 100.0 % + 15 / 15 + 100.0 % + 6 / 6 + 100.0 % + 2 / 2 +
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 1447d4e..61a292e 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 @@ -31,27 +31,27 @@ lcov.info Lines: - 28 - 28 - 100.0 % + 31 + 34 + 91.2 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: 10 - 10 - 100.0 % + 12 + 83.3 % Branches: - 11 - 13 - 84.6 % + 12 + 14 + 85.7 % @@ -69,11 +69,19 @@ Hit count Sort by hit count - ERC3643ComplianceModule.getTokenBounds + ERC3643ComplianceModule._onlyComplianceManager + 0 + + + ERC3643ComplianceModule.onlyComplianceManager + 0 + + + ERC3643ComplianceModule.getTokenBounds 4 - ERC3643ComplianceModule.getTokenBound + ERC3643ComplianceModule.getTokenBound 5 @@ -81,32 +89,32 @@ 5 - ERC3643ComplianceModule._unbindToken - 9 + ERC3643ComplianceModule._unbindToken + 12 - ERC3643ComplianceModule.unbindToken - 10 + ERC3643ComplianceModule.unbindToken + 16 - ERC3643ComplianceModule.isTokenBound - 15 + ERC3643ComplianceModule.isTokenBound + 21 - ERC3643ComplianceModule.bindToken - 26 + ERC3643ComplianceModule._checkBoundToken + 31 - ERC3643ComplianceModule.onlyComplianceManager - 26 + ERC3643ComplianceModule.bindToken + 38 - ERC3643ComplianceModule._checkBoundToken - 31 + ERC3643ComplianceModule._authorizeComplianceBindingChange + 54 - ERC3643ComplianceModule._bindToken - 57 + ERC3643ComplianceModule._bindToken + 66
diff --git a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html index 53bed1c..f5ade38 100644 --- a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html +++ b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html @@ -31,27 +31,27 @@ lcov.info Lines: - 28 - 28 - 100.0 % + 31 + 34 + 91.2 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: 10 - 10 - 100.0 % + 12 + 83.3 % Branches: - 11 - 13 - 84.6 % + 12 + 14 + 85.7 % @@ -69,32 +69,40 @@ Hit count Sort by hit count - ERC3643ComplianceModule._bindToken - 57 + ERC3643ComplianceModule._authorizeComplianceBindingChange + 54 + + + ERC3643ComplianceModule._bindToken + 66 - ERC3643ComplianceModule._checkBoundToken + ERC3643ComplianceModule._checkBoundToken 31 - ERC3643ComplianceModule._unbindToken - 9 + ERC3643ComplianceModule._onlyComplianceManager + 0 + + + ERC3643ComplianceModule._unbindToken + 12 - ERC3643ComplianceModule.bindToken - 26 + ERC3643ComplianceModule.bindToken + 38 - ERC3643ComplianceModule.getTokenBound + ERC3643ComplianceModule.getTokenBound 5 - ERC3643ComplianceModule.getTokenBounds + ERC3643ComplianceModule.getTokenBounds 4 - ERC3643ComplianceModule.isTokenBound - 15 + ERC3643ComplianceModule.isTokenBound + 21 ERC3643ComplianceModule.onlyBoundToken @@ -102,11 +110,11 @@ ERC3643ComplianceModule.onlyComplianceManager - 26 + 0 - ERC3643ComplianceModule.unbindToken - 10 + ERC3643ComplianceModule.unbindToken + 16
diff --git a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html index 0dfe087..232a450 100644 --- a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html +++ b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html @@ -31,27 +31,27 @@ lcov.info Lines: - 28 - 28 - 100.0 % + 31 + 34 + 91.2 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: 10 - 10 - 100.0 % + 12 + 83.3 % Branches: - 11 - 13 - 84.6 % + 12 + 14 + 85.7 % @@ -74,8 +74,8 @@ 3 : : pragma solidity ^0.8.20; 4 : : 5 : : /* ==== OpenZeppelin === */ - 6 : : import {EnumerableSet} from "OZ/utils/structs/EnumerableSet.sol"; - 7 : : import {Context} from "OZ/utils/Context.sol"; + 6 : : import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + 7 : : import {Context} from "@openzeppelin/contracts/utils/Context.sol"; 8 : : /* ==== Interface and other library === */ 9 : : import {IERC3643Compliance} from "../interfaces/IERC3643Compliance.sol"; 10 : : @@ -102,8 +102,8 @@ 31 : : _; 32 : : } 33 : : - 34 : 26 : modifier onlyComplianceManager() { - 35 : 26 : _onlyComplianceManager(); + 34 : 0 : modifier onlyComplianceManager() { + 35 : 0 : _onlyComplianceManager(); 36 : : _; 37 : : } 38 : : @@ -117,70 +117,86 @@ 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 : : /** - 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 : : } + 49 : : * @dev T-REX compatibility: allows token self-binding when caller equals + 50 : : * `token`, because TREX `Token.setCompliance` invokes `compliance.bindToken(address(this))`. + 51 : : */ + 52 : 38 : function bindToken(address token) public virtual override { + 53 : 38 : _authorizeComplianceBindingChange(token); + 54 : 34 : _bindToken(token); + 55 : : } + 56 : : + 57 : : /** + 58 : : * @inheritdoc IERC3643Compliance + 59 : : * @dev Operator warning: unbinding is an administrative operation and does not + 60 : : * erase any state already stored by external rule contracts in a previously + 61 : : * shared ("multi-tenant") setup. + 62 : : * @dev T-REX compatibility: allows token self-unbinding when caller equals + 63 : : * `token`, because TREX token contracts may call `compliance.unbindToken(address(this))`. + 64 : : */ + 65 : 16 : function unbindToken(address token) public virtual override { + 66 : 16 : _authorizeComplianceBindingChange(token); + 67 : 12 : _unbindToken(token); + 68 : : } + 69 : : + 70 : : /// @inheritdoc IERC3643Compliance + 71 : 21 : function isTokenBound(address token) public view virtual override returns (bool) { + 72 : 21 : return _boundTokens.contains(token); + 73 : : } + 74 : : + 75 : : /// @inheritdoc IERC3643Compliance + 76 : 5 : function getTokenBound() public view virtual override returns (address) { + 77 [ + + ]: 5 : if (_boundTokens.length() > 0) { + 78 : : // Note that there are no guarantees on the ordering of values inside the array, + 79 : : // and it may change when more values are added or removed. + 80 : 3 : return _boundTokens.at(0); + 81 : : } else { + 82 : 2 : return address(0); + 83 : : } + 84 : : } + 85 : : + 86 : : /// @inheritdoc IERC3643Compliance + 87 : 4 : function getTokenBounds() public view override returns (address[] memory) { + 88 : 4 : return _boundTokens.values(); + 89 : : } + 90 : : + 91 : : /*////////////////////////////////////////////////////////////// + 92 : : INTERNAL/PRIVATE FUNCTIONS + 93 : : //////////////////////////////////////////////////////////////*/ + 94 : : + 95 : 12 : function _unbindToken(address token) internal { + 96 [ + + ]: 12 : require(_boundTokens.contains(token), RuleEngine_ERC3643Compliance_TokenNotBound()); + 97 : : // Should never revert because we check if the token address is already set before + 98 [ # + ]: 10 : require(_boundTokens.remove(token), RuleEngine_ERC3643Compliance_OperationNotSuccessful()); + 99 : : + 100 : 10 : emit TokenUnbound(token); + 101 : : } + 102 : : + 103 : 66 : function _bindToken(address token) internal { + 104 [ + + ]: 66 : require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); + 105 [ + + ]: 64 : require(!_boundTokens.contains(token), RuleEngine_ERC3643Compliance_TokenAlreadyBound()); + 106 : : // Should never revert because we check if the token address is already set before + 107 [ # + ]: 62 : require(_boundTokens.add(token), RuleEngine_ERC3643Compliance_OperationNotSuccessful()); + 108 : 62 : emit TokenBound(token); 109 : : } 110 : : - 111 : : function _onlyComplianceManager() internal virtual; - 112 : : } + 111 : 31 : function _checkBoundToken() internal view virtual { + 112 [ + ]: 31 : if (!_boundTokens.contains(_msgSender())) { + 113 : 7 : revert RuleEngine_ERC3643Compliance_UnauthorizedCaller(); + 114 : : } + 115 : : } + 116 : : + 117 : : /** + 118 : : * @dev Authorizes bind/unbind operations. + 119 : : * Allows compliance manager, or token self-calls for T-REX compatibility. + 120 : : */ + 121 : 54 : function _authorizeComplianceBindingChange(address token) internal virtual { + 122 [ + ]: 54 : if (_msgSender() != token) { + 123 : 43 : _onlyComplianceManager(); + 124 : : } + 125 : : } + 126 : : + 127 : 0 : function _onlyComplianceManager() internal virtual; + 128 : : } 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 9c2e3a2..e3c9a39 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 @@ -31,27 +31,27 @@ lcov.info Lines: - 49 - 49 - 100.0 % + 62 + 64 + 96.9 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 14 - 14 - 100.0 % + 17 + 19 + 89.5 % Branches: - 11 14 - 78.6 % + 17 + 82.4 % @@ -69,60 +69,80 @@ Hit count Sort by hit count - RulesManagementModule._transferred.1 + RulesManagementModule._onlyRulesLimitManager + 0 + + + RulesManagementModule._onlyRulesManager + 0 + + + RulesManagementModule._transferred.1 4 - RulesManagementModule.rule + RulesManagementModule.rule 5 - RulesManagementModule._removeRule + RulesManagementModule.maxRules + 8 + + + RulesManagementModule.onlyRulesLimitManager + 9 + + + RulesManagementModule.setMaxRules + 9 + + + RulesManagementModule._removeRule 13 - RulesManagementModule.clearRules + RulesManagementModule.rules 15 - RulesManagementModule.onlyRulesManager - 15 + RulesManagementModule.clearRules + 16 - RulesManagementModule.rules - 15 + RulesManagementModule.onlyRulesManager + 16 - RulesManagementModule.removeRule + RulesManagementModule.removeRule 18 - RulesManagementModule._transferred.0 + RulesManagementModule._transferred.0 20 - RulesManagementModule._clearRules - 48 + RulesManagementModule._clearRules + 49 - RulesManagementModule.setRules - 49 + RulesManagementModule.setRules + 51 - RulesManagementModule.containsRule - 71 + RulesManagementModule.containsRule + 79 - RulesManagementModule.addRule - 152 + RulesManagementModule.addRule + 181 - RulesManagementModule.rulesCount - 161 + RulesManagementModule.rulesCount + 183 - RulesManagementModule._checkRule - 217 + RulesManagementModule._checkRule + 253
diff --git a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html index 2a8a1b0..f303599 100644 --- a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html +++ b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html @@ -31,27 +31,27 @@ lcov.info Lines: - 49 - 49 - 100.0 % + 62 + 64 + 96.9 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 14 - 14 - 100.0 % + 17 + 19 + 89.5 % Branches: - 11 14 - 78.6 % + 17 + 82.4 % @@ -69,60 +69,80 @@ Hit count Sort by hit count - RulesManagementModule._checkRule - 217 + RulesManagementModule._checkRule + 253 + + + RulesManagementModule._clearRules + 49 + + + RulesManagementModule._onlyRulesLimitManager + 0 - RulesManagementModule._clearRules - 48 + RulesManagementModule._onlyRulesManager + 0 - RulesManagementModule._removeRule + RulesManagementModule._removeRule 13 - RulesManagementModule._transferred.0 + RulesManagementModule._transferred.0 20 - RulesManagementModule._transferred.1 + RulesManagementModule._transferred.1 4 - RulesManagementModule.addRule - 152 + RulesManagementModule.addRule + 181 - RulesManagementModule.clearRules - 15 + RulesManagementModule.clearRules + 16 - RulesManagementModule.containsRule - 71 + RulesManagementModule.containsRule + 79 + + + RulesManagementModule.maxRules + 8 + + + RulesManagementModule.onlyRulesLimitManager + 9 RulesManagementModule.onlyRulesManager - 15 + 16 - RulesManagementModule.removeRule + RulesManagementModule.removeRule 18 - RulesManagementModule.rule + RulesManagementModule.rule 5 - RulesManagementModule.rules + RulesManagementModule.rules 15 - RulesManagementModule.rulesCount - 161 + RulesManagementModule.rulesCount + 183 - RulesManagementModule.setRules - 49 + RulesManagementModule.setMaxRules + 9 + + + RulesManagementModule.setRules + 51
diff --git a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html index 97c1d96..afffd64 100644 --- a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html +++ b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html @@ -31,27 +31,27 @@ lcov.info Lines: - 49 - 49 - 100.0 % + 62 + 64 + 96.9 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 14 - 14 - 100.0 % + 17 + 19 + 89.5 % Branches: - 11 14 - 78.6 % + 17 + 82.4 % @@ -74,7 +74,7 @@ 3 : : pragma solidity ^0.8.20; 4 : : 5 : : /* ==== OpenZeppelin === */ - 6 : : import {EnumerableSet} from "OZ/utils/structs/EnumerableSet.sol"; + 6 : : import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 7 : : /* ==== Interface and other library === */ 8 : : import {IRulesManagementModule} from "../interfaces/IRulesManagementModule.sol"; 9 : : import {IRule} from "../interfaces/IRule.sol"; @@ -84,191 +84,220 @@ 13 : : * @title RuleEngine - part 14 : : */ 15 : : abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage, IRulesManagementModule { - 16 : 15 : modifier onlyRulesManager() { - 17 : 15 : _onlyRulesManager(); + 16 : 16 : modifier onlyRulesManager() { + 17 : 16 : _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 : 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 ============ */ + 21 : 9 : modifier onlyRulesLimitManager() { + 22 : 9 : _onlyRulesLimitManager(); + 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 : : /// @dev Maximum number of rules allowed in the engine. + 33 : : uint256 internal _maxRules = 10; + 34 : : + 35 : : /*////////////////////////////////////////////////////////////// + 36 : : PUBLIC/EXTERNAL FUNCTIONS + 37 : : //////////////////////////////////////////////////////////////*/ + 38 : : + 39 : : /* ============ State functions ============ */ + 40 : : + 41 : : /** + 42 : : * @inheritdoc IRulesManagementModule + 43 : : * @dev Replaces the entire rule set atomically. + 44 : : * Reverts if `rules_` is empty. Use {clearRules} to remove all rules explicitly. + 45 : : * To transition from one non-empty set to another without an enforcement gap, + 46 : : * call this function directly with the new set. + 47 : : * Security convention: rule contracts should be treated as trusted business logic, + 48 : : * but should not also be granted {RULES_MANAGEMENT_ROLE}. + 49 : : */ + 50 : 51 : function setRules(IRule[] calldata rules_) public virtual override(IRulesManagementModule) onlyRulesManager { + 51 [ + ]: 49 : if (rules_.length == 0) { + 52 : 6 : revert RuleEngine_RulesManagementModule_ArrayIsEmpty(); + 53 : : } + 54 [ + ]: 43 : if (rules_.length > _maxRules) { + 55 : 1 : revert RuleEngine_RulesManagementModule_MaxRulesExceeded(_maxRules); + 56 : : } + 57 [ + ]: 42 : if (_rules.length() > 0) { + 58 : 36 : _clearRules(); + 59 : : } + 60 : 42 : for (uint256 i = 0; i < rules_.length; ++i) { + 61 : 79 : _checkRule(address(rules_[i])); + 62 : : // Should never revert because we check the presence of the rule before + 63 [ # + ]: 74 : require(_rules.add(address(rules_[i])), RuleEngine_RulesManagementModule_OperationNotSuccessful()); + 64 : 74 : emit AddRule(rules_[i]); + 65 : : } + 66 : : } + 67 : : + 68 : : /** + 69 : : * @inheritdoc IRulesManagementModule + 70 : : */ + 71 : 16 : function clearRules() public virtual override(IRulesManagementModule) onlyRulesManager { + 72 : 13 : _clearRules(); + 73 : : } + 74 : : + 75 : : /** + 76 : : * @inheritdoc IRulesManagementModule + 77 : : * @dev Reverts when the configured maximum number of rules is already reached. + 78 : : * Security convention: do not grant {RULES_MANAGEMENT_ROLE} to rule contracts. + 79 : : */ + 80 : 181 : function addRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { + 81 [ + ]: 176 : if (_rules.length() >= _maxRules) { + 82 : 2 : revert RuleEngine_RulesManagementModule_MaxRulesExceeded(_maxRules); + 83 : : } + 84 : 174 : _checkRule(address(rule_)); + 85 [ # + ]: 164 : require(_rules.add(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); + 86 : 164 : emit AddRule(rule_); + 87 : : } 88 : : 89 : : /** 90 : : * @inheritdoc IRulesManagementModule 91 : : */ - 92 : 161 : function rulesCount() public view virtual override(IRulesManagementModule) returns (uint256) { - 93 : 278 : return _rules.length(); + 92 : 8 : function maxRules() public view virtual override(IRulesManagementModule) returns (uint256) { + 93 : 8 : return _maxRules; 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 : : } + 99 : 9 : function setMaxRules(uint256 maxRules_) public virtual override(IRulesManagementModule) onlyRulesLimitManager { + 100 [ + ]: 5 : if (maxRules_ == 0) { + 101 : 1 : revert RuleEngine_RulesManagementModule_MaxRulesZeroNotAllowed(); + 102 : : } + 103 : 4 : _maxRules = maxRules_; + 104 : 4 : emit SetMaxRules(maxRules_); + 105 : : } + 106 : : + 107 : : /** + 108 : : * @inheritdoc IRulesManagementModule + 109 : : */ + 110 : 18 : function removeRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { + 111 [ + + ]: 16 : require(_rules.contains(address(rule_)), RuleEngine_RulesManagementModule_RuleDoNotMatch()); + 112 : 13 : _removeRule(rule_); + 113 : : } + 114 : : + 115 : : /* ============ View functions ============ */ + 116 : : + 117 : : /** + 118 : : * @inheritdoc IRulesManagementModule + 119 : : */ + 120 : 183 : function rulesCount() public view virtual override(IRulesManagementModule) returns (uint256) { + 121 : 300 : return _rules.length(); + 122 : : } + 123 : : + 124 : : /** + 125 : : * @inheritdoc IRulesManagementModule + 126 : : */ + 127 : 79 : function containsRule(IRule rule_) public view virtual override(IRulesManagementModule) returns (bool) { + 128 : 89 : return _rules.contains(address(rule_)); + 129 : : } + 130 : : + 131 : : /** + 132 : : * @inheritdoc IRulesManagementModule + 133 : : */ + 134 : 5 : function rule(uint256 ruleId) public view virtual override(IRulesManagementModule) returns (address) { + 135 [ + + ]: 133 : if (ruleId < _rules.length()) { + 136 : : // Note that there are no guarantees on the ordering of values inside the array, + 137 : : // and it may change when more values are added or removed. + 138 : 131 : return _rules.at(ruleId); + 139 : : } else { + 140 : 2 : return address(0); + 141 : : } + 142 : : } + 143 : : + 144 : : /** + 145 : : * @inheritdoc IRulesManagementModule + 146 : : */ + 147 : 15 : function rules() public view virtual override(IRulesManagementModule) returns (address[] memory) { + 148 : 15 : return _rules.values(); + 149 : : } + 150 : : + 151 : : /*////////////////////////////////////////////////////////////// + 152 : : INTERNAL/PRIVATE FUNCTIONS + 153 : : //////////////////////////////////////////////////////////////*/ + 154 : : /** + 155 : : * @notice Clear all the rules of the array of rules + 156 : : * + 157 : : */ + 158 : 49 : function _clearRules() internal virtual { + 159 : 49 : emit ClearRules(); + 160 : 49 : _rules.clear(); + 161 : : } + 162 : : + 163 : : /** + 164 : : * @notice Remove a rule from the array of rules + 165 : : * Revert if the rule found at the specified index does not match the rule in argument + 166 : : * @param rule_ address of the target rule + 167 : : * + 168 : : * + 169 : : */ + 170 : 13 : function _removeRule(IRule rule_) internal virtual { + 171 : : // Should never revert because we check the presence of the rule before + 172 [ # + ]: 13 : require(_rules.remove(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); + 173 : 13 : emit RemoveRule(rule_); + 174 : : } + 175 : : + 176 : : /** + 177 : : * @dev check if a rule is valid, revert otherwise + 178 : : */ + 179 : 253 : function _checkRule(address rule_) internal view virtual { + 180 [ + ]: 253 : if (rule_ == address(0x0)) { + 181 : 3 : revert RuleEngine_RulesManagementModule_RuleAddressZeroNotAllowed(); + 182 : : } + 183 [ + ]: 250 : if (_rules.contains(rule_)) { + 184 : 6 : revert RuleEngine_RulesManagementModule_RuleAlreadyExists(); + 185 : : } + 186 : : } + 187 : : + 188 : : /* ============ Transferred functions ============ */ + 189 : : + 190 : : /** + 191 : : * @notice Go through all the rule to know if a restriction exists on the transfer + 192 : : * @dev Complexity is O(number of configured rules). Large rule sets can make + 193 : : * transfers too expensive on chains with lower block gas limits. + 194 : : * Security convention: rule contracts are expected to be trusted and must not + 195 : : * hold {RULES_MANAGEMENT_ROLE}. + 196 : : * @param from the origin address + 197 : : * @param to the destination address + 198 : : * @param value to transfer + 199 : : * + 200 : : */ + 201 : 20 : function _transferred(address from, address to, uint256 value) internal virtual { + 202 : 20 : uint256 rulesLength = _rules.length(); + 203 : 20 : for (uint256 i = 0; i < rulesLength; ++i) { + 204 : 14 : IRule(_rules.at(i)).transferred(from, to, value); + 205 : : } + 206 : : } + 207 : : + 208 : : /** + 209 : : * @notice Go through all the rule to know if a restriction exists on the transfer + 210 : : * @dev Complexity is O(number of configured rules). Large rule sets can make + 211 : : * transfers too expensive on chains with lower block gas limits. + 212 : : * Security convention: rule contracts are expected to be trusted and must not + 213 : : * hold {RULES_MANAGEMENT_ROLE}. + 214 : : * @param spender the spender address (transferFrom) + 215 : : * @param from the origin address + 216 : : * @param to the destination address + 217 : : * @param value to transfer + 218 : : * + 219 : : */ + 220 : 4 : function _transferred(address spender, address from, address to, uint256 value) internal virtual { + 221 : 4 : uint256 rulesLength = _rules.length(); + 222 : 4 : for (uint256 i = 0; i < rulesLength; ++i) { + 223 : 4 : IRule(_rules.at(i)).transferred(spender, from, to, value); + 224 : : } + 225 : : } + 226 : : + 227 : 0 : function _onlyRulesManager() internal virtual; + 228 : 0 : function _onlyRulesLimitManager() internal virtual; + 229 : : } 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 b953c88..a850218 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-03-18 19:08:29 + 2026-05-05 13:03:55 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 f4517d4..47875c2 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-03-18 19:08:29 + 2026-05-05 13:03:55 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 cf339dc..348fd4f 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-03-18 19:08:29 + 2026-05-05 13:03:55 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 b6a5a03..fa15856 100644 --- a/doc/coverage/coverage/src/modules/index-sort-b.html +++ b/doc/coverage/coverage/src/modules/index-sort-b.html @@ -31,27 +31,27 @@ lcov.info Lines: - 79 - 79 - 100.0 % + 95 + 100 + 95.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 25 - 25 - 100.0 % + 28 + 32 + 87.5 % Branches: - 22 - 27 - 81.5 % + 26 + 31 + 83.9 % @@ -84,26 +84,26 @@ RulesManagementModule.sol -
100.0%
+
96.9%96.9%
- 100.0 % - 49 / 49 - 100.0 % - 14 / 14 - 78.6 % - 11 / 14 + 96.9 % + 62 / 64 + 89.5 % + 17 / 19 + 82.4 % + 14 / 17 ERC3643ComplianceModule.sol -
100.0%
+
91.2%91.2%
- 100.0 % - 28 / 28 - 100.0 % - 10 / 10 - 84.6 % - 11 / 13 + 91.2 % + 31 / 34 + 83.3 % + 10 / 12 + 85.7 % + 12 / 14 VersionModule.sol diff --git a/doc/coverage/coverage/src/modules/index-sort-f.html b/doc/coverage/coverage/src/modules/index-sort-f.html index 4252b69..a2c384a 100644 --- a/doc/coverage/coverage/src/modules/index-sort-f.html +++ b/doc/coverage/coverage/src/modules/index-sort-f.html @@ -31,27 +31,27 @@ lcov.info Lines: - 79 - 79 - 100.0 % + 95 + 100 + 95.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 25 - 25 - 100.0 % + 28 + 32 + 87.5 % Branches: - 22 - 27 - 81.5 % + 26 + 31 + 83.9 % @@ -82,40 +82,40 @@ Branches Sort by branch coverage - VersionModule.sol + ERC3643ComplianceModule.sol -
100.0%
+
91.2%91.2%
- 100.0 % - 2 / 2 - 100.0 % - 1 / 1 - - - 0 / 0 + 91.2 % + 31 / 34 + 83.3 % + 10 / 12 + 85.7 % + 12 / 14 - ERC3643ComplianceModule.sol + RulesManagementModule.sol -
100.0%
+
96.9%96.9%
- 100.0 % - 28 / 28 - 100.0 % - 10 / 10 - 84.6 % - 11 / 13 + 96.9 % + 62 / 64 + 89.5 % + 17 / 19 + 82.4 % + 14 / 17 - RulesManagementModule.sol + VersionModule.sol
100.0%
100.0 % - 49 / 49 + 2 / 2 100.0 % - 14 / 14 - 78.6 % - 11 / 14 + 1 / 1 + - + 0 / 0 diff --git a/doc/coverage/coverage/src/modules/index-sort-l.html b/doc/coverage/coverage/src/modules/index-sort-l.html index db2c56d..d18eaa9 100644 --- a/doc/coverage/coverage/src/modules/index-sort-l.html +++ b/doc/coverage/coverage/src/modules/index-sort-l.html @@ -31,27 +31,27 @@ lcov.info Lines: - 79 - 79 - 100.0 % + 95 + 100 + 95.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 25 - 25 - 100.0 % + 28 + 32 + 87.5 % Branches: - 22 - 27 - 81.5 % + 26 + 31 + 83.9 % @@ -82,40 +82,40 @@ Branches Sort by branch coverage - VersionModule.sol + ERC3643ComplianceModule.sol -
100.0%
+
91.2%91.2%
- 100.0 % - 2 / 2 - 100.0 % - 1 / 1 - - - 0 / 0 + 91.2 % + 31 / 34 + 83.3 % + 10 / 12 + 85.7 % + 12 / 14 - ERC3643ComplianceModule.sol + RulesManagementModule.sol -
100.0%
+
96.9%96.9%
- 100.0 % - 28 / 28 - 100.0 % - 10 / 10 - 84.6 % - 11 / 13 + 96.9 % + 62 / 64 + 89.5 % + 17 / 19 + 82.4 % + 14 / 17 - RulesManagementModule.sol + VersionModule.sol
100.0%
100.0 % - 49 / 49 + 2 / 2 100.0 % - 14 / 14 - 78.6 % - 11 / 14 + 1 / 1 + - + 0 / 0 diff --git a/doc/coverage/coverage/src/modules/index.html b/doc/coverage/coverage/src/modules/index.html index f103df9..164da3f 100644 --- a/doc/coverage/coverage/src/modules/index.html +++ b/doc/coverage/coverage/src/modules/index.html @@ -31,27 +31,27 @@ lcov.info Lines: - 79 - 79 - 100.0 % + 95 + 100 + 95.0 % Date: - 2026-03-18 19:08:29 + 2026-05-05 13:03:55 Functions: - 25 - 25 - 100.0 % + 28 + 32 + 87.5 % Branches: - 22 - 27 - 81.5 % + 26 + 31 + 83.9 % @@ -84,26 +84,26 @@ ERC3643ComplianceModule.sol -
100.0%
+
91.2%91.2%
- 100.0 % - 28 / 28 - 100.0 % - 10 / 10 - 84.6 % - 11 / 13 + 91.2 % + 31 / 34 + 83.3 % + 10 / 12 + 85.7 % + 12 / 14 RulesManagementModule.sol -
100.0%
+
96.9%96.9%
- 100.0 % - 49 / 49 - 100.0 % - 14 / 14 - 78.6 % - 11 / 14 + 96.9 % + 62 / 64 + 89.5 % + 17 / 19 + 82.4 % + 14 / 17 VersionModule.sol diff --git a/doc/coverage/lcov.info b/doc/coverage/lcov.info index b64b64b..f9c8faf 100644 --- a/doc/coverage/lcov.info +++ b/doc/coverage/lcov.info @@ -1,178 +1,258 @@ TN: SF:src/RuleEngineBase.sol -DA:37,5 -FN:37,RuleEngineBase.transferred.0 +DA:42,5 +FN:42,RuleEngineBase.transferred.0 FNDA:5,RuleEngineBase.transferred.0 -DA:44,4 -DA:50,18 -FN:50,RuleEngineBase.transferred.1 +DA:49,4 +DA:55,18 +FN:55,RuleEngineBase.transferred.1 FNDA:18,RuleEngineBase.transferred.1 -DA:56,16 -DA:60,4 -FN:60,RuleEngineBase.created -FNDA:4,RuleEngineBase.created -DA:61,2 +DA:61,16 DA:65,4 -FN:65,RuleEngineBase.destroyed -FNDA:4,RuleEngineBase.destroyed +FN:65,RuleEngineBase.created +FNDA:4,RuleEngineBase.created DA:66,2 -DA:78,34 -FN:78,RuleEngineBase.detectTransferRestriction +DA:70,4 +FN:70,RuleEngineBase.destroyed +FNDA:4,RuleEngineBase.destroyed +DA:71,2 +DA:83,34 +FN:83,RuleEngineBase.detectTransferRestriction FNDA:34,RuleEngineBase.detectTransferRestriction -DA:85,59 -DA:91,18 -FN:91,RuleEngineBase.detectTransferRestrictionFrom +DA:90,59 +DA:96,18 +FN:96,RuleEngineBase.detectTransferRestrictionFrom FNDA:18,RuleEngineBase.detectTransferRestrictionFrom -DA:98,39 -DA:104,19 -FN:104,RuleEngineBase.messageForTransferRestriction +DA:103,39 +DA:109,19 +FN:109,RuleEngineBase.messageForTransferRestriction FNDA:19,RuleEngineBase.messageForTransferRestriction -DA:111,19 -DA:117,25 -FN:117,RuleEngineBase.canTransfer +DA:116,19 +DA:122,25 +FN:122,RuleEngineBase.canTransfer FNDA:25,RuleEngineBase.canTransfer -DA:124,25 -DA:130,21 -FN:130,RuleEngineBase.canTransferFrom +DA:129,25 +DA:135,21 +FN:135,RuleEngineBase.canTransferFrom FNDA:21,RuleEngineBase.canTransferFrom -DA:137,21 -DA:143,59 -FN:143,RuleEngineBase._detectTransferRestriction +DA:142,21 +DA:148,59 +FN:148,RuleEngineBase._detectTransferRestriction FNDA:59,RuleEngineBase._detectTransferRestriction -DA:144,59 -DA:145,59 -DA:146,59 -DA:147,59 -BRDA:147,0,0,43 -DA:148,43 -DA:151,16 -DA:154,39 -FN:154,RuleEngineBase._detectTransferRestrictionFrom +DA:149,59 +DA:150,59 +DA:151,59 +DA:152,59 +BRDA:152,0,0,43 +DA:153,43 +DA:156,16 +DA:159,39 +FN:159,RuleEngineBase._detectTransferRestrictionFrom FNDA:39,RuleEngineBase._detectTransferRestrictionFrom -DA:160,39 -DA:161,39 -DA:162,39 -DA:163,39 -BRDA:163,1,0,29 -DA:164,29 -DA:167,10 -DA:176,19 -FN:176,RuleEngineBase._messageForTransferRestriction +DA:165,39 +DA:166,39 +DA:167,39 +DA:168,39 +BRDA:168,1,0,29 +DA:169,29 +DA:172,10 +DA:181,19 +FN:181,RuleEngineBase._messageForTransferRestriction FNDA:19,RuleEngineBase._messageForTransferRestriction -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 -LH:42 +DA:182,19 +DA:183,19 +DA:184,16 +BRDA:184,2,0,14 +DA:185,14 +DA:188,5 +DA:194,253 +FN:194,RuleEngineBase._checkRule +FNDA:253,RuleEngineBase._checkRule +DA:195,253 +DA:196,244 +BRDA:196,3,0,6 +DA:197,6 +DA:205,46 +FN:205,RuleEngineBase._supportsRuleEngineBaseInterface +FNDA:46,RuleEngineBase._supportsRuleEngineBaseInterface +DA:206,46 +DA:207,41 +DA:208,31 +DA:209,26 +DA:210,21 +FNF:14 +FNH:14 +LF:48 +LH:48 BRF:4 BRH:4 end_of_record TN: -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 +SF:src/RuleEngineOwnableShared.sol +DA:22,128 +FN:22,RuleEngineOwnableShared.constructor +FNDA:128,RuleEngineOwnableShared.constructor +DA:23,128 +BRDA:23,0,0,1 +DA:24,1 +DA:29,29 +FN:29,RuleEngineOwnableShared.supportsInterface +FNDA:29,RuleEngineOwnableShared.supportsInterface +DA:30,29 +DA:31,5 +DA:37,10 +FN:37,RuleEngineOwnableShared._checkOwnershipTransferTarget +FNDA:10,RuleEngineOwnableShared._checkOwnershipTransferTarget +DA:38,10 +BRDA:38,1,0,2 +DA:39,2 +DA:50,181 +FN:50,RuleEngineOwnableShared._msgSender +FNDA:181,RuleEngineOwnableShared._msgSender +DA:51,181 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 +FN:57,RuleEngineOwnableShared._msgData +FNDA:2,RuleEngineOwnableShared._msgData +DA:58,2 +DA:64,183 +FN:64,RuleEngineOwnableShared._contextSuffixLength +FNDA:183,RuleEngineOwnableShared._contextSuffixLength +DA:65,183 +FNF:6 +FNH:6 +LF:15 +LH:15 +BRF:2 +BRH:2 +end_of_record +TN: +SF:src/deployment/RuleEngine.sol +DA:27,146 +FN:27,RuleEngine.constructor +FNDA:146,RuleEngine.constructor +DA:30,146 +BRDA:30,0,0,1 +DA:31,1 +DA:33,145 +BRDA:33,1,0,31 +DA:34,31 +DA:36,145 +DA:44,22 +FN:44,RuleEngine.grantRole +FNDA:22,RuleEngine.grantRole +DA:45,22 +BRDA:45,2,0,3 +DA:46,3 +DA:48,19 +DA:55,154 +FN:55,RuleEngine.hasRole +FNDA:154,RuleEngine.hasRole +DA:62,413 +BRDA:62,3,0,219 +BRDA:62,3,1,194 +DA:63,219 +DA:65,194 +DA:70,17 +FN:70,RuleEngine.supportsInterface +FNDA:17,RuleEngine.supportsInterface +DA:77,17 +DA:83,18 +FN:83,RuleEngine._onlyComplianceManager +FNDA:18,RuleEngine._onlyComplianceManager +DA:84,196 +FN:84,RuleEngine._onlyRulesManager +FNDA:196,RuleEngine._onlyRulesManager +DA:85,5 +FN:85,RuleEngine._onlyRulesLimitManager +FNDA:5,RuleEngine._onlyRulesLimitManager +DA:90,450 +FN:90,RuleEngine._msgSender +FNDA:450,RuleEngine._msgSender +DA:91,450 +DA:97,1 +FN:97,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 +DA:98,1 +DA:104,451 +FN:104,RuleEngine._contextSuffixLength +FNDA:451,RuleEngine._contextSuffixLength +DA:105,451 +FNF:10 +FNH:10 +LF:25 +LH:25 +BRF:5 +BRH:5 end_of_record TN: SF:src/deployment/RuleEngineOwnable.sol -DA:29,77 -FN:29,RuleEngineOwnable.constructor -FNDA:77,RuleEngineOwnable.constructor -DA:35,76 -BRDA:35,0,0,1 -DA:36,1 -DA:44,64 -FN:44,RuleEngineOwnable._onlyRulesManager -FNDA:64,RuleEngineOwnable._onlyRulesManager -DA:49,21 -FN:49,RuleEngineOwnable._onlyComplianceManager -FNDA:21,RuleEngineOwnable._onlyComplianceManager -DA:52,9 -FN:52,RuleEngineOwnable.supportsInterface -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:69,108 -DA:75,1 -FN:75,RuleEngineOwnable._msgData +DA:27,65 +FN:27,RuleEngineOwnable._onlyRulesManager +FNDA:65,RuleEngineOwnable._onlyRulesManager +DA:28,2 +FN:28,RuleEngineOwnable._onlyRulesLimitManager +FNDA:2,RuleEngineOwnable._onlyRulesLimitManager +DA:33,22 +FN:33,RuleEngineOwnable._onlyComplianceManager +FNDA:22,RuleEngineOwnable._onlyComplianceManager +DA:39,5 +FN:39,RuleEngineOwnable.transferOwnership +FNDA:5,RuleEngineOwnable.transferOwnership +DA:40,4 +DA:41,3 +DA:47,146 +FN:47,RuleEngineOwnable._msgSender +FNDA:146,RuleEngineOwnable._msgSender +DA:48,146 +DA:54,1 +FN:54,RuleEngineOwnable._msgData FNDA:1,RuleEngineOwnable._msgData -DA:76,1 -DA:82,109 -FN:82,RuleEngineOwnable._contextSuffixLength -FNDA:109,RuleEngineOwnable._contextSuffixLength -DA:83,109 +DA:55,1 +DA:61,147 +FN:61,RuleEngineOwnable._contextSuffixLength +FNDA:147,RuleEngineOwnable._contextSuffixLength +DA:62,147 +FNF:7 +FNH:7 +LF:12 +LH:12 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/deployment/RuleEngineOwnable2Step.sol +DA:29,5 +FN:29,RuleEngineOwnable2Step._onlyRulesManager +FNDA:5,RuleEngineOwnable2Step._onlyRulesManager +DA:30,2 +FN:30,RuleEngineOwnable2Step._onlyRulesLimitManager +FNDA:2,RuleEngineOwnable2Step._onlyRulesLimitManager +DA:35,3 +FN:35,RuleEngineOwnable2Step._onlyComplianceManager +FNDA:3,RuleEngineOwnable2Step._onlyComplianceManager +DA:41,6 +FN:41,RuleEngineOwnable2Step.transferOwnership +FNDA:6,RuleEngineOwnable2Step.transferOwnership +DA:42,6 +DA:43,5 +DA:49,35 +FN:49,RuleEngineOwnable2Step._msgSender +FNDA:35,RuleEngineOwnable2Step._msgSender +DA:50,35 +DA:56,1 +FN:56,RuleEngineOwnable2Step._msgData +FNDA:1,RuleEngineOwnable2Step._msgData +DA:57,1 +DA:63,36 +FN:63,RuleEngineOwnable2Step._contextSuffixLength +FNDA:36,RuleEngineOwnable2Step._contextSuffixLength +DA:64,36 FNF:7 FNH:7 -LF:18 -LH:18 -BRF:1 -BRH:1 +LF:12 +LH:12 +BRF:0 +BRH:0 end_of_record TN: SF:src/modules/ERC3643ComplianceModule.sol @@ -180,169 +260,208 @@ DA:29,5 FN:29,ERC3643ComplianceModule.onlyBoundToken FNDA:5,ERC3643ComplianceModule.onlyBoundToken DA:30,5 -DA:34,26 +DA:34,0 FN:34,ERC3643ComplianceModule.onlyComplianceManager -FNDA:26,ERC3643ComplianceModule.onlyComplianceManager -DA:35,26 -DA:50,26 -FN:50,ERC3643ComplianceModule.bindToken -FNDA:26,ERC3643ComplianceModule.bindToken -DA:51,25 -DA:60,10 -FN:60,ERC3643ComplianceModule.unbindToken -FNDA:10,ERC3643ComplianceModule.unbindToken -DA:61,9 -DA:65,15 -FN:65,ERC3643ComplianceModule.isTokenBound -FNDA:15,ERC3643ComplianceModule.isTokenBound -DA:66,15 -DA:70,5 -FN:70,ERC3643ComplianceModule.getTokenBound +FNDA:0,ERC3643ComplianceModule.onlyComplianceManager +DA:35,0 +DA:52,38 +FN:52,ERC3643ComplianceModule.bindToken +FNDA:38,ERC3643ComplianceModule.bindToken +DA:53,38 +DA:54,34 +DA:65,16 +FN:65,ERC3643ComplianceModule.unbindToken +FNDA:16,ERC3643ComplianceModule.unbindToken +DA:66,16 +DA:67,12 +DA:71,21 +FN:71,ERC3643ComplianceModule.isTokenBound +FNDA:21,ERC3643ComplianceModule.isTokenBound +DA:72,21 +DA:76,5 +FN:76,ERC3643ComplianceModule.getTokenBound FNDA:5,ERC3643ComplianceModule.getTokenBound -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 +DA:77,5 +BRDA:77,0,0,3 +BRDA:77,0,1,2 +DA:80,3 +DA:82,2 +DA:87,4 +FN:87,ERC3643ComplianceModule.getTokenBounds FNDA:4,ERC3643ComplianceModule.getTokenBounds -DA:82,4 -DA:89,9 -FN:89,ERC3643ComplianceModule._unbindToken -FNDA:9,ERC3643ComplianceModule._unbindToken -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 +DA:88,4 +DA:95,12 +FN:95,ERC3643ComplianceModule._unbindToken +FNDA:12,ERC3643ComplianceModule._unbindToken +DA:96,12 +BRDA:96,1,0,2 +BRDA:96,1,1,10 +DA:98,10 +BRDA:98,2,0,- +BRDA:98,2,1,10 +DA:100,10 +DA:103,66 +FN:103,ERC3643ComplianceModule._bindToken +FNDA:66,ERC3643ComplianceModule._bindToken +DA:104,66 +BRDA:104,3,0,2 +BRDA:104,3,1,64 +DA:105,64 +BRDA:105,4,0,2 +BRDA:105,4,1,62 +DA:107,62 +BRDA:107,5,0,- +BRDA:107,5,1,62 +DA:108,62 +DA:111,31 +FN:111,ERC3643ComplianceModule._checkBoundToken FNDA:31,ERC3643ComplianceModule._checkBoundToken -DA:106,31 -BRDA:106,6,0,7 -DA:107,7 -FNF:10 +DA:112,31 +BRDA:112,6,0,7 +DA:113,7 +DA:121,54 +FN:121,ERC3643ComplianceModule._authorizeComplianceBindingChange +FNDA:54,ERC3643ComplianceModule._authorizeComplianceBindingChange +DA:122,54 +BRDA:122,7,0,43 +DA:123,43 +DA:127,0 +FN:127,ERC3643ComplianceModule._onlyComplianceManager +FNDA:0,ERC3643ComplianceModule._onlyComplianceManager +FNF:12 FNH:10 -LF:28 -LH:28 -BRF:13 -BRH:11 +LF:34 +LH:31 +BRF:14 +BRH:12 end_of_record TN: SF:src/modules/RulesManagementModule.sol -DA:16,15 +DA:16,16 FN:16,RulesManagementModule.onlyRulesManager -FNDA:15,RulesManagementModule.onlyRulesManager -DA:17,15 -DA:45,49 -FN:45,RulesManagementModule.setRules -FNDA:49,RulesManagementModule.setRules -DA:46,47 -BRDA:46,0,0,6 -DA:47,6 -DA:49,41 -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: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:16,RulesManagementModule.onlyRulesManager +DA:17,16 +DA:21,9 +FN:21,RulesManagementModule.onlyRulesLimitManager +FNDA:9,RulesManagementModule.onlyRulesLimitManager +DA:22,9 +DA:50,51 +FN:50,RulesManagementModule.setRules +FNDA:51,RulesManagementModule.setRules +DA:51,49 +BRDA:51,0,0,6 +DA:52,6 +DA:54,43 +BRDA:54,1,0,1 +DA:55,1 +DA:57,42 +BRDA:57,2,0,36 +DA:58,36 +DA:60,42 +DA:61,79 +DA:63,74 +BRDA:63,3,0,- +BRDA:63,3,1,74 +DA:64,74 +DA:71,16 +FN:71,RulesManagementModule.clearRules +FNDA:16,RulesManagementModule.clearRules +DA:72,13 +DA:80,181 +FN:80,RulesManagementModule.addRule +FNDA:181,RulesManagementModule.addRule +DA:81,176 +BRDA:81,4,0,2 +DA:82,2 +DA:84,174 +DA:85,164 +BRDA:85,5,0,- +BRDA:85,5,1,164 +DA:86,164 +DA:92,8 +FN:92,RulesManagementModule.maxRules +FNDA:8,RulesManagementModule.maxRules +DA:93,8 +DA:99,9 +FN:99,RulesManagementModule.setMaxRules +FNDA:9,RulesManagementModule.setMaxRules +DA:100,5 +BRDA:100,6,0,1 +DA:101,1 +DA:103,4 +DA:104,4 +DA:110,18 +FN:110,RulesManagementModule.removeRule FNDA:18,RulesManagementModule.removeRule -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:93,278 -DA:99,71 -FN:99,RulesManagementModule.containsRule -FNDA:71,RulesManagementModule.containsRule -DA:100,71 -DA:106,5 -FN:106,RulesManagementModule.rule +DA:111,16 +BRDA:111,7,0,3 +BRDA:111,7,1,13 +DA:112,13 +DA:120,183 +FN:120,RulesManagementModule.rulesCount +FNDA:183,RulesManagementModule.rulesCount +DA:121,300 +DA:127,79 +FN:127,RulesManagementModule.containsRule +FNDA:79,RulesManagementModule.containsRule +DA:128,89 +DA:134,5 +FN:134,RulesManagementModule.rule FNDA:5,RulesManagementModule.rule -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 +DA:135,133 +BRDA:135,8,0,131 +BRDA:135,8,1,2 +DA:138,131 +DA:140,2 +DA:147,15 +FN:147,RulesManagementModule.rules FNDA:15,RulesManagementModule.rules -DA:120,15 -DA:130,48 -FN:130,RulesManagementModule._clearRules -FNDA:48,RulesManagementModule._clearRules -DA:131,48 -DA:132,48 -DA:142,13 -FN:142,RulesManagementModule._removeRule +DA:148,15 +DA:158,49 +FN:158,RulesManagementModule._clearRules +FNDA:49,RulesManagementModule._clearRules +DA:159,49 +DA:160,49 +DA:170,13 +FN:170,RulesManagementModule._removeRule FNDA:13,RulesManagementModule._removeRule -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 +DA:172,13 +BRDA:172,9,0,- +BRDA:172,9,1,13 +DA:173,13 +DA:179,253 +FN:179,RulesManagementModule._checkRule +FNDA:253,RulesManagementModule._checkRule +DA:180,253 +BRDA:180,10,0,3 +DA:181,3 +DA:183,250 +BRDA:183,11,0,6 +DA:184,6 +DA:201,20 +FN:201,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 +DA:202,20 +DA:203,20 +DA:204,14 +DA:220,4 +FN:220,RulesManagementModule._transferred.1 FNDA:4,RulesManagementModule._transferred.1 -DA:193,4 -DA:194,4 -DA:195,4 -FNF:14 -FNH:14 -LF:49 -LH:49 -BRF:14 -BRH:11 +DA:221,4 +DA:222,4 +DA:223,4 +DA:227,0 +FN:227,RulesManagementModule._onlyRulesManager +FNDA:0,RulesManagementModule._onlyRulesManager +DA:228,0 +FN:228,RulesManagementModule._onlyRulesLimitManager +FNDA:0,RulesManagementModule._onlyRulesLimitManager +FNF:19 +FNH:17 +LF:64 +LH:62 +BRF:17 +BRH:14 end_of_record TN: SF:src/modules/VersionModule.sol diff --git a/doc/security/audits/tools/aderyn-report-feedback.md b/doc/security/audits/tools/aderyn-report-feedback.md index 7211d8d..1d3e41a 100644 --- a/doc/security/audits/tools/aderyn-report-feedback.md +++ b/doc/security/audits/tools/aderyn-report-feedback.md @@ -1,160 +1,49 @@ # 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/`). - ---- +**Tool:** [Aderyn](https://github.com/Cyfrin/aderyn) +**Report file:** `doc/security/audits/tools/aderyn-report.md` +**Assessment date:** 2026-05-05 ## 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 +| ID | Finding | Tool Severity | Assessment | Decision | +|----|---------|---------------|------------|----------| +| L-1 | Centralization Risk | Low | Valid observation of privileged governance model | Accepted by design | +| L-2 | Unspecific Solidity Pragma | Low | Intentional library-style pragma with pinned compiler in tooling | Accepted by design | +| L-3 | PUSH0 Opcode | Low | Not applicable for configured target EVM | Accepted by design | +| L-4 | Modifier Invoked Only Once | Low | Style heuristic; modifier supports hook pattern consistency | Accepted by design | +| L-5 | Empty Block | Low | Expected for hook overrides where modifier carries logic | Accepted by design | +| L-6 | Loop Contains `require`/`revert` | Low | Required for atomicity in `setRules` | Accepted by design | +| L-7 | Costly operations inside loop | Low | Storage writes are intrinsic to rule registration | Accepted by design | +| L-8 | Unchecked Return | Low | `_grantRole` return not security-relevant in constructor flow | Accepted by design | -1 instance: `_grantRole(DEFAULT_ADMIN_ROLE, admin)` in `RuleEngine` constructor (line 35) — the `bool` return value is ignored. +No High findings were reported. -### Assessment +## Detailed triage -**Accepted — return value is irrelevant in this context. No action required.** +### L-1 Centralization Risk +This contract family is explicitly administrative compliance infrastructure. `RuleEngine` (RBAC), `RuleEngineOwnable`, and `RuleEngineOwnable2Step` intentionally expose privileged operations for rule and token binding governance. This is a product requirement, not a vulnerability. -`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`. +### L-2 Unspecific Solidity Pragma +`^0.8.20` is intentional for integrator compatibility. Build tooling remains pinned (Solc 0.8.34). This is a common and acceptable pattern for reusable contract packages. -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. +### L-3 PUSH0 Opcode +The project target is modern EVM (`prague`). `PUSH0` compatibility concerns only pre-Shanghai targets; those are outside the supported deployment matrix. -This pattern (ignoring the return of `_grantRole` in a constructor) is standard across all OpenZeppelin-based contracts. +### L-4 Modifier Invoked Only Once +`onlyRulesLimitManager` being used once is not a risk. It preserves the same extensible access-control abstraction used across modules and variants. ---- +### L-5 Empty Block +Empty bodies in `_onlyComplianceManager`, `_onlyRulesManager`, and `_onlyRulesLimitManager` are intentional. Authorization executes in the modifier, and the empty body enforces a clean virtual-hook architecture. -## Overall conclusion +### L-6 Loop Contains `require`/`revert` +`setRules` must be all-or-nothing. Reverting on invalid element prevents partial updates and inconsistent policy state. -All 7 Aderyn findings are **accepted**: +### L-7 Costly operations inside loop +`EnumerableSet.add` in a loop is unavoidable for persistent rule set updates. Cost scales with rule count and is an operator-controlled admin path. -- **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. +### L-8 Unchecked Return +Ignoring `_grantRole` return in constructor is acceptable; no exploitable path is introduced, and constructor semantics do not depend on that boolean. -No code changes are required for v3.0.0-rc2 based on this report. +## Conclusion +All Aderyn findings are either design tradeoffs or static-analysis style heuristics. No code changes are required based on this report. diff --git a/doc/security/audits/tools/aderyn-report.md b/doc/security/audits/tools/aderyn-report.md index d408f9d..9619842 100644 --- a/doc/security/audits/tools/aderyn-report.md +++ b/doc/security/audits/tools/aderyn-report.md @@ -11,10 +11,11 @@ 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: 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) + - [L-4: Modifier Invoked Only Once](#l-4-modifier-invoked-only-once) + - [L-5: Empty Block](#l-5-empty-block) + - [L-6: Loop Contains `require`/`revert`](#l-6-loop-contains-requirerevert) + - [L-7: Costly operations inside loop](#l-7-costly-operations-inside-loop) + - [L-8: Unchecked Return](#l-8-unchecked-return) # Summary @@ -23,29 +24,33 @@ This report was generated by [Aderyn](https://github.com/Cyfrin/aderyn), a stati | Key | Value | | --- | --- | -| .sol Files | 14 | -| Total nSLOC | 425 | +| .sol Files | 18 | +| Total nSLOC | 542 | ## Files Details | Filepath | nSLOC | | --- | --- | -| src/RuleEngineBase.sol | 127 | -| src/deployment/RuleEngine.sol | 47 | -| src/deployment/RuleEngineOwnable.sol | 39 | +| src/RuleEngineBase.sol | 138 | +| src/RuleEngineOwnableShared.sol | 33 | +| src/deployment/RuleEngine.sol | 63 | +| src/deployment/RuleEngineOwnable.sol | 26 | +| src/deployment/RuleEngineOwnable2Step.sol | 27 | | src/interfaces/IERC3643Compliance.sol | 13 | | src/interfaces/IRule.sol | 5 | -| src/interfaces/IRulesManagementModule.sol | 12 | +| src/interfaces/IRulesManagementModule.sol | 14 | | src/modules/ERC2771ModuleStandalone.sol | 6 | -| src/modules/ERC3643ComplianceModule.sol | 58 | -| src/modules/RulesManagementModule.sol | 83 | +| src/modules/ERC3643ComplianceModule.sol | 65 | +| src/modules/RulesManagementModule.sol | 105 | | src/modules/VersionModule.sol | 8 | | src/modules/library/ComplianceInterfaceId.sol | 5 | +| src/modules/library/ERC1404InterfaceId.sol | 4 | +| src/modules/library/OwnableInterfaceId.sol | 4 | | src/modules/library/RuleEngineInvariantStorage.sol | 5 | | src/modules/library/RuleInterfaceId.sol | 4 | -| src/modules/library/RulesManagementModuleInvariantStorage.sol | 13 | -| **Total** | **425** | +| src/modules/library/RulesManagementModuleInvariantStorage.sol | 17 | +| **Total** | **542** | ## Issue Summary @@ -53,7 +58,7 @@ This report was generated by [Aderyn](https://github.com/Cyfrin/aderyn), a stati | Category | No. of Issues | | --- | --- | | High | 0 | -| Low | 7 | +| Low | 8 | # Low Issues @@ -62,45 +67,93 @@ This report was generated by [Aderyn](https://github.com/Cyfrin/aderyn), a stati Contracts have owners with privileged rights to perform admin tasks and need to be trusted to not perform malicious updates or drain funds. -
6 Found Instances +
14 Found Instances -- Found in src/deployment/RuleEngine.sol [Line: 21](src/deployment/RuleEngine.sol#L21) +- Found in src/deployment/RuleEngine.sol [Line: 20](src/deployment/RuleEngine.sol#L20) ```solidity - contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase, AccessControl { + contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase, AccessControlEnumerable { ``` -- Found in src/deployment/RuleEngine.sol [Line: 63](src/deployment/RuleEngine.sol#L63) +- Found in src/deployment/RuleEngine.sol [Line: 83](src/deployment/RuleEngine.sol#L83) ```solidity function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} ``` -- Found in src/deployment/RuleEngine.sol [Line: 64](src/deployment/RuleEngine.sol#L64) +- Found in src/deployment/RuleEngine.sol [Line: 84](src/deployment/RuleEngine.sol#L84) ```solidity function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} ``` -- Found in src/deployment/RuleEngineOwnable.sol [Line: 21](src/deployment/RuleEngineOwnable.sol#L21) +- Found in src/deployment/RuleEngine.sol [Line: 85](src/deployment/RuleEngine.sol#L85) ```solidity - contract RuleEngineOwnable is ERC2771ModuleStandalone, RuleEngineBase, Ownable { + function _onlyRulesLimitManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} ``` -- Found in src/deployment/RuleEngineOwnable.sol [Line: 44](src/deployment/RuleEngineOwnable.sol#L44) +- Found in src/deployment/RuleEngineOwnable.sol [Line: 12](src/deployment/RuleEngineOwnable.sol#L12) + + ```solidity + contract RuleEngineOwnable is RuleEngineOwnableShared, Ownable { + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 27](src/deployment/RuleEngineOwnable.sol#L27) ```solidity function _onlyRulesManager() internal virtual override onlyOwner {} ``` -- Found in src/deployment/RuleEngineOwnable.sol [Line: 49](src/deployment/RuleEngineOwnable.sol#L49) +- Found in src/deployment/RuleEngineOwnable.sol [Line: 28](src/deployment/RuleEngineOwnable.sol#L28) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 33](src/deployment/RuleEngineOwnable.sol#L33) ```solidity function _onlyComplianceManager() internal virtual override onlyOwner {} ``` +- Found in src/deployment/RuleEngineOwnable.sol [Line: 39](src/deployment/RuleEngineOwnable.sol#L39) + + ```solidity + function transferOwnership(address newOwner) public virtual override onlyOwner { + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 14](src/deployment/RuleEngineOwnable2Step.sol#L14) + + ```solidity + contract RuleEngineOwnable2Step is RuleEngineOwnableShared, Ownable2Step { + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 29](src/deployment/RuleEngineOwnable2Step.sol#L29) + + ```solidity + function _onlyRulesManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 30](src/deployment/RuleEngineOwnable2Step.sol#L30) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 35](src/deployment/RuleEngineOwnable2Step.sol#L35) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 41](src/deployment/RuleEngineOwnable2Step.sol#L41) + + ```solidity + function transferOwnership(address newOwner) public virtual override onlyOwner { + ``` +
@@ -109,7 +162,7 @@ Contracts have owners with privileged rights to perform admin tasks and need to 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;` -
12 Found Instances +
14 Found Instances - Found in src/RuleEngineBase.sol [Line: 3](src/RuleEngineBase.sol#L3) @@ -118,6 +171,12 @@ Consider using a specific version of Solidity in your contracts instead of a wid pragma solidity ^0.8.20; ``` +- Found in src/RuleEngineOwnableShared.sol [Line: 3](src/RuleEngineOwnableShared.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + - Found in src/deployment/RuleEngine.sol [Line: 3](src/deployment/RuleEngine.sol#L3) ```solidity @@ -130,6 +189,12 @@ Consider using a specific version of Solidity in your contracts instead of a wid pragma solidity ^0.8.20; ``` +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 3](src/deployment/RuleEngineOwnable2Step.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + - Found in src/interfaces/IERC3643Compliance.sol [Line: 3](src/interfaces/IERC3643Compliance.sol#L3) ```solidity @@ -192,7 +257,7 @@ Consider using a specific version of Solidity in your contracts instead of a wid 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. -
14 Found Instances +
18 Found Instances - Found in src/RuleEngineBase.sol [Line: 3](src/RuleEngineBase.sol#L3) @@ -201,6 +266,12 @@ Solc compiler version 0.8.20 switches the default target EVM version to Shanghai pragma solidity ^0.8.20; ``` +- Found in src/RuleEngineOwnableShared.sol [Line: 3](src/RuleEngineOwnableShared.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + - Found in src/deployment/RuleEngine.sol [Line: 3](src/deployment/RuleEngine.sol#L3) ```solidity @@ -213,6 +284,12 @@ Solc compiler version 0.8.20 switches the default target EVM version to Shanghai pragma solidity ^0.8.20; ``` +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 3](src/deployment/RuleEngineOwnable2Step.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + - Found in src/interfaces/IERC3643Compliance.sol [Line: 3](src/interfaces/IERC3643Compliance.sol#L3) ```solidity @@ -261,6 +338,18 @@ Solc compiler version 0.8.20 switches the default target EVM version to Shanghai pragma solidity ^0.8.20; ``` +- Found in src/modules/library/ERC1404InterfaceId.sol [Line: 3](src/modules/library/ERC1404InterfaceId.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/library/OwnableInterfaceId.sol [Line: 3](src/modules/library/OwnableInterfaceId.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + - Found in src/modules/library/RuleEngineInvariantStorage.sol [Line: 3](src/modules/library/RuleEngineInvariantStorage.sol#L3) ```solidity @@ -283,32 +372,79 @@ Solc compiler version 0.8.20 switches the default target EVM version to Shanghai -## L-4: Empty Block +## L-4: Modifier Invoked Only Once + +Consider removing the modifier or inlining the logic into the calling function. + +
1 Found Instances + + +- Found in src/modules/RulesManagementModule.sol [Line: 21](src/modules/RulesManagementModule.sol#L21) + + ```solidity + modifier onlyRulesLimitManager() { + ``` + +
+ + + +## L-5: Empty Block Consider removing empty blocks. -
4 Found Instances +
9 Found Instances -- Found in src/deployment/RuleEngine.sol [Line: 63](src/deployment/RuleEngine.sol#L63) +- Found in src/deployment/RuleEngine.sol [Line: 83](src/deployment/RuleEngine.sol#L83) ```solidity function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} ``` -- Found in src/deployment/RuleEngine.sol [Line: 64](src/deployment/RuleEngine.sol#L64) +- Found in src/deployment/RuleEngine.sol [Line: 84](src/deployment/RuleEngine.sol#L84) ```solidity function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} ``` -- Found in src/deployment/RuleEngineOwnable.sol [Line: 44](src/deployment/RuleEngineOwnable.sol#L44) +- Found in src/deployment/RuleEngine.sol [Line: 85](src/deployment/RuleEngine.sol#L85) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 27](src/deployment/RuleEngineOwnable.sol#L27) ```solidity function _onlyRulesManager() internal virtual override onlyOwner {} ``` -- Found in src/deployment/RuleEngineOwnable.sol [Line: 49](src/deployment/RuleEngineOwnable.sol#L49) +- Found in src/deployment/RuleEngineOwnable.sol [Line: 28](src/deployment/RuleEngineOwnable.sol#L28) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 33](src/deployment/RuleEngineOwnable.sol#L33) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 29](src/deployment/RuleEngineOwnable2Step.sol#L29) + + ```solidity + function _onlyRulesManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 30](src/deployment/RuleEngineOwnable2Step.sol#L30) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 35](src/deployment/RuleEngineOwnable2Step.sol#L35) ```solidity function _onlyComplianceManager() internal virtual override onlyOwner {} @@ -318,14 +454,14 @@ Consider removing empty blocks. -## L-5: Loop Contains `require`/`revert` +## L-6: 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: 52](src/modules/RulesManagementModule.sol#L52) +- Found in src/modules/RulesManagementModule.sol [Line: 60](src/modules/RulesManagementModule.sol#L60) ```solidity for (uint256 i = 0; i < rules_.length; ++i) { @@ -335,14 +471,14 @@ Avoid `require` / `revert` statements in a loop because a single bad item can ca -## L-6: Costly operations inside loop +## L-7: 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) +- Found in src/modules/RulesManagementModule.sol [Line: 60](src/modules/RulesManagementModule.sol#L60) ```solidity for (uint256 i = 0; i < rules_.length; ++i) { @@ -352,14 +488,14 @@ Invoking `SSTORE` operations in loops may waste gas. Use a local variable to hol -## L-7: Unchecked Return +## L-8: 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) +- Found in src/deployment/RuleEngine.sol [Line: 36](src/deployment/RuleEngine.sol#L36) ```solidity _grantRole(DEFAULT_ADMIN_ROLE, admin); diff --git a/doc/security/audits/tools/nethermind-audit-agent/v3.0.0-rc1/audit_agent_report_1_v3.0.0-rc1-feedback.md b/doc/security/audits/tools/nethermind-audit-agent/v3.0.0-rc1/audit_agent_report_1_v3.0.0-rc1-feedback.md index 0bec46a..5ad6bdd 100644 --- a/doc/security/audits/tools/nethermind-audit-agent/v3.0.0-rc1/audit_agent_report_1_v3.0.0-rc1-feedback.md +++ b/doc/security/audits/tools/nethermind-audit-agent/v3.0.0-rc1/audit_agent_report_1_v3.0.0-rc1-feedback.md @@ -88,7 +88,7 @@ Code check confirmed: **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` +**Remediation commits:** `1caf4ea` (rc2 docs warning), `v3.0.0-rc3` hard-cap implementation ### Finding summary @@ -96,27 +96,37 @@ Code check confirmed: ### 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. +**Updated in rc3: fully addressed on-chain.** The prior rc2 docs-only mitigation was insufficient for liveness guarantees. `v3.0.0-rc3` introduces an on-chain configurable cap with a safe default. ### 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. +- **`src/modules/library/RulesManagementModuleInvariantStorage.sol`** + - Add `DEFAULT_MAX_RULES = 10`. + - Add `RuleEngine_RulesManagementModule_MaxRulesExceeded(uint256)`. + - Add `RuleEngine_RulesManagementModule_MaxRulesZeroNotAllowed()`. + - Add `SetMaxRules(uint256)` event. +- **`src/interfaces/IRulesManagementModule.sol`** + - Add `maxRules()` and `setMaxRules(uint256)`. +- **`src/modules/RulesManagementModule.sol`** + - Add `_maxRules` state initialized to default cap (`10`). + - Enforce cap in `addRule` and `setRules`. + - Add governance setter `setMaxRules(uint256)` with zero-value guard. +- **Access control overrides** + - `RuleEngine`: cap setter restricted to `DEFAULT_ADMIN_ROLE`. + - `RuleEngineOwnable` / `RuleEngineOwnable2Step`: cap setter restricted to owner. +- **`README.md`** + - Replace outdated “no on-chain cap” warning with current rc3 behavior. ### 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. ✓ +Code and tests confirm: +- cap default is `10`; +- `addRule` and `setRules` revert above cap; +- cap update emits `SetMaxRules`; +- cap setter is admin/owner restricted by variant; +- zero cap is rejected. -**Status: Implemented (warnings only — no hard cap by design).** +**Status: Fixed in `v3.0.0-rc3` (runtime mitigation implemented).** --- @@ -157,7 +167,7 @@ Code check confirmed: **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` +**Remediation commits:** `33ff6fa` (rc2 docs warning), `v3.0.0-rc3` on-chain privilege separation ### Finding summary @@ -165,26 +175,32 @@ Code check confirmed: ### 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. +**Updated in rc3: partially mitigated on-chain.** We keep the trust-model framing, but implemented direct runtime guardrails to prevent key privilege-coupling paths between rule accounts and governance/control-plane identities. ### 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. +- **`src/deployment/RuleEngine.sol`** + - Override `grantRole`: revert if `account` is currently in `_rules` (applies to any role grant). +- **`src/deployment/RuleEngineOwnable.sol`** + - Override `transferOwnership`: revert if `newOwner` is currently in rules set. +- **`src/deployment/RuleEngineOwnable2Step.sol`** + - Override `transferOwnership`: revert if `newOwner` is currently in rules set. +- **`src/RuleEngineOwnableShared.sol`** + - Add shared `_checkOwnershipTransferTarget` guard to reduce duplication across ownable variants. +- **`src/modules/library/RulesManagementModuleInvariantStorage.sol`** + - Add `RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges()` error. +- Existing NatSpec/README warnings retained as operational guidance. -No reentrancy guard was added (would add gas overhead to every transfer; not warranted given the trust model). +No reentrancy guard was added; transfer-path fan-out remains architecture by design. ### 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. ✓ +Code and tests confirm: +- role grants to rule accounts revert in RBAC variant; +- ownership transfer to rule accounts reverts in both ownable variants; +- NatSpec/README warnings remain present. -**Status: Implemented (warnings only — no reentrancy guard by design).** +**Status: Implemented in `v3.0.0-rc3` (on-chain guardrails + documentation).** --- diff --git a/doc/security/audits/tools/slither-report-feedback.md b/doc/security/audits/tools/slither-report-feedback.md index 6c2f217..f10c7f5 100644 --- a/doc/security/audits/tools/slither-report-feedback.md +++ b/doc/security/audits/tools/slither-report-feedback.md @@ -1,80 +1,32 @@ # 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. - ---- +**Tool:** [Slither](https://github.com/crytic/slither) +**Report file:** `doc/security/audits/tools/slither-report.md` +**Assessment date:** 2026-05-05 ## 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. +| IDs | Detector | Tool Impact | Assessment | Decision | +|-----|----------|-------------|------------|----------| +| 0-9 | `calls-loop` | Low | Inherent to pluggable rule-engine dispatch architecture | Accepted by design | +| 10 | `dead-code` (`RuleEngineOwnable2Step._msgData`) | Informational | False positive in context; required override in ERC2771/context inheritance chain | Accepted / no action | +| 11 | `naming-convention` (CMTAT dependency enum) | Informational | External dependency style issue, not project code risk | Ignored | +| 12-13 | `unindexed-event-address` | Informational | Valid optimization note, but ABI-breaking to change now | Deferred | -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. +## Detailed triage -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. +### IDs 0-9: `calls-loop` +The RuleEngine intentionally iterates `_rules` and performs external rule calls for transfer checks and transfer hooks. This is core product behavior. +Risk is controlled through trusted-rule governance and documented operational limits on rule count. No direct security defect is introduced by this detector output. -**Decision: accepted as-is.** If the interface is ever revised for another reason, the `indexed` keyword should be added at the same time. +### ID 10: `dead-code` on `RuleEngineOwnable2Step._msgData` +This override is part of required multiple-inheritance context resolution (`RuleEngineOwnableShared` + `Context`) for ERC2771-compatible message handling. Static analysis can mark it as unused even though it is part of the contract’s context surface and override chain. Keep as-is. ---- +### ID 11: `naming-convention` in `lib/CMTAT/.../draft-IERC1404.sol` +Finding targets vendored dependency naming style. This is not a vulnerability and should not be patched locally unless forking upstream conventions intentionally. -## Overall conclusion +### IDs 12-13: `unindexed-event-address` +`TokenBound(address)` / `TokenUnbound(address)` could add `indexed` for cheaper filtering, but doing so changes event signature and breaks existing consumers. Given low event frequency and backward-compatibility requirements, defer to a planned major interface revision. -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. +## Conclusion +No actionable security fixes are required from this Slither run. Findings are architectural by-design, dependency style-only, or compatibility tradeoffs. diff --git a/doc/security/audits/tools/slither-report.md b/doc/security/audits/tools/slither-report.md index 75c678b..842bbfa 100644 --- a/doc/security/audits/tools/slither-report.md +++ b/doc/security/audits/tools/slither-report.md @@ -1,102 +1,122 @@ **THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results. Summary - [calls-loop](#calls-loop) (10 results) (Low) + - [dead-code](#dead-code) (1 results) (Informational) + - [naming-convention](#naming-convention) (1 results) (Informational) - [unindexed-event-address](#unindexed-event-address) (2 results) (Informational) ## calls-loop Impact: Low Confidence: Medium - [ ] ID-0 -[RuleEngineBase._messageForTransferRestriction(uint8)](src/RuleEngineBase.sol#L176-L184) has external calls inside a loop: [IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)](src/RuleEngineBase.sol#L179) +[RulesManagementModule._transferred(address,address,address,uint256)](src/modules/RulesManagementModule.sol#L220-L225) has external calls inside a loop: [IRule(_rules.at(i)).transferred(spender,from,to,value)](src/modules/RulesManagementModule.sol#L223) Calls stack containing the loop: - RuleEngineBase.messageForTransferRestriction(uint8) + RuleEngineBase.transferred(address,address,address,uint256) -src/RuleEngineBase.sol#L176-L184 +src/modules/RulesManagementModule.sol#L220-L225 - [ ] ID-1 -[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) +[RulesManagementModule._transferred(address,address,uint256)](src/modules/RulesManagementModule.sol#L201-L206) has external calls inside a loop: [IRule(_rules.at(i)).transferred(from,to,value)](src/modules/RulesManagementModule.sol#L204) Calls stack containing the loop: - RuleEngineBase.detectTransferRestrictionFrom(address,address,address,uint256) + RuleEngineBase.destroyed(address,uint256) -src/RuleEngineBase.sol#L154-L168 +src/modules/RulesManagementModule.sol#L201-L206 - [ ] ID-2 -[RuleEngineBase._messageForTransferRestriction(uint8)](src/RuleEngineBase.sol#L176-L184) has external calls inside a loop: [IRule(rule(i)).messageForTransferRestriction(restrictionCode)](src/RuleEngineBase.sol#L180) +[RulesManagementModule._transferred(address,address,uint256)](src/modules/RulesManagementModule.sol#L201-L206) has external calls inside a loop: [IRule(_rules.at(i)).transferred(from,to,value)](src/modules/RulesManagementModule.sol#L204) Calls stack containing the loop: - RuleEngineBase.messageForTransferRestriction(uint8) + RuleEngineBase.transferred(address,address,uint256) -src/RuleEngineBase.sol#L176-L184 +src/modules/RulesManagementModule.sol#L201-L206 - [ ] ID-3 -[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) +[RuleEngineBase._messageForTransferRestriction(uint8)](src/RuleEngineBase.sol#L181-L189) has external calls inside a loop: [IRule(rule(i)).messageForTransferRestriction(restrictionCode)](src/RuleEngineBase.sol#L185) Calls stack containing the loop: - RuleEngineBase.transferred(address,address,address,uint256) + RuleEngineBase.messageForTransferRestriction(uint8) -src/modules/RulesManagementModule.sol#L192-L197 +src/RuleEngineBase.sol#L181-L189 - [ ] 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) +[RulesManagementModule._transferred(address,address,uint256)](src/modules/RulesManagementModule.sol#L201-L206) has external calls inside a loop: [IRule(_rules.at(i)).transferred(from,to,value)](src/modules/RulesManagementModule.sol#L204) Calls stack containing the loop: - RuleEngineBase.canTransferFrom(address,address,address,uint256) - RuleEngineBase.detectTransferRestrictionFrom(address,address,address,uint256) + RuleEngineBase.created(address,uint256) -src/RuleEngineBase.sol#L154-L168 +src/modules/RulesManagementModule.sol#L201-L206 - [ ] 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) +[RuleEngineBase._detectTransferRestrictionFrom(address,address,address,uint256)](src/RuleEngineBase.sol#L159-L173) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestrictionFrom(spender,from,to,value)](src/RuleEngineBase.sol#L167) Calls stack containing the loop: - RuleEngineBase.detectTransferRestriction(address,address,uint256) + RuleEngineBase.detectTransferRestrictionFrom(address,address,address,uint256) -src/RuleEngineBase.sol#L143-L152 +src/RuleEngineBase.sol#L159-L173 - [ ] 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) +[RuleEngineBase._messageForTransferRestriction(uint8)](src/RuleEngineBase.sol#L181-L189) has external calls inside a loop: [IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)](src/RuleEngineBase.sol#L184) Calls stack containing the loop: - RuleEngineBase.created(address,uint256) + RuleEngineBase.messageForTransferRestriction(uint8) -src/modules/RulesManagementModule.sol#L173-L178 +src/RuleEngineBase.sol#L181-L189 - [ ] 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) +[RuleEngineBase._detectTransferRestriction(address,address,uint256)](src/RuleEngineBase.sol#L148-L157) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestriction(from,to,value)](src/RuleEngineBase.sol#L151) Calls stack containing the loop: - RuleEngineBase.canTransfer(address,address,uint256) RuleEngineBase.detectTransferRestriction(address,address,uint256) -src/RuleEngineBase.sol#L143-L152 +src/RuleEngineBase.sol#L148-L157 - [ ] 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) +[RuleEngineBase._detectTransferRestriction(address,address,uint256)](src/RuleEngineBase.sol#L148-L157) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestriction(from,to,value)](src/RuleEngineBase.sol#L151) Calls stack containing the loop: - RuleEngineBase.transferred(address,address,uint256) + RuleEngineBase.canTransfer(address,address,uint256) + RuleEngineBase.detectTransferRestriction(address,address,uint256) -src/modules/RulesManagementModule.sol#L173-L178 +src/RuleEngineBase.sol#L148-L157 - [ ] 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) +[RuleEngineBase._detectTransferRestrictionFrom(address,address,address,uint256)](src/RuleEngineBase.sol#L159-L173) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestrictionFrom(spender,from,to,value)](src/RuleEngineBase.sol#L167) Calls stack containing the loop: - RuleEngineBase.destroyed(address,uint256) + RuleEngineBase.canTransferFrom(address,address,address,uint256) + RuleEngineBase.detectTransferRestrictionFrom(address,address,address,uint256) + +src/RuleEngineBase.sol#L159-L173 + + +## dead-code +Impact: Informational +Confidence: Medium + - [ ] ID-10 +[RuleEngineOwnable2Step._msgData()](src/deployment/RuleEngineOwnable2Step.sol#L56-L58) is never used and should be removed + +src/deployment/RuleEngineOwnable2Step.sol#L56-L58 -src/modules/RulesManagementModule.sol#L173-L178 + +## naming-convention +Impact: Informational +Confidence: High + - [ ] ID-11 +Enum [IERC1404Extend.REJECTED_CODE_BASE](lib/CMTAT/contracts/interfaces/tokenization/draft-IERC1404.sol#L49-L57) is not in CapWords + +lib/CMTAT/contracts/interfaces/tokenization/draft-IERC1404.sol#L49-L57 ## unindexed-event-address Impact: Informational Confidence: High - - [ ] ID-10 + - [ ] ID-12 Event [IERC3643Compliance.TokenBound(address)](src/interfaces/IERC3643Compliance.sol#L14) has address parameters but no indexed parameters src/interfaces/IERC3643Compliance.sol#L14 - - [ ] ID-11 + - [ ] ID-13 Event [IERC3643Compliance.TokenUnbound(address)](src/interfaces/IERC3643Compliance.sol#L20) has address parameters but no indexed parameters src/interfaces/IERC3643Compliance.sol#L20 diff --git a/doc/security/audits/tools/v3.0.0-rc2/aderyn-report-feedback.md b/doc/security/audits/tools/v3.0.0-rc2/aderyn-report-feedback.md new file mode 100644 index 0000000..7211d8d --- /dev/null +++ b/doc/security/audits/tools/v3.0.0-rc2/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/v3.0.0-rc2/aderyn-report.md b/doc/security/audits/tools/v3.0.0-rc2/aderyn-report.md new file mode 100644 index 0000000..d408f9d --- /dev/null +++ b/doc/security/audits/tools/v3.0.0-rc2/aderyn-report.md @@ -0,0 +1,371 @@ +# Aderyn Analysis Report + +This report was generated by [Aderyn](https://github.com/Cyfrin/aderyn), a static analysis tool built by [Cyfrin](https://cyfrin.io), a blockchain security company. This report is not a substitute for manual audit or security review. It should not be relied upon for any purpose other than to assist in the identification of potential security vulnerabilities. +# Table of Contents + +- [Summary](#summary) + - [Files Summary](#files-summary) + - [Files Details](#files-details) + - [Issue Summary](#issue-summary) +- [Low Issues](#low-issues) + - [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: 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 + +## Files Summary + +| Key | Value | +| --- | --- | +| .sol Files | 14 | +| Total nSLOC | 425 | + + +## Files Details + +| Filepath | nSLOC | +| --- | --- | +| 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 | 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 + +| Category | No. of Issues | +| --- | --- | +| High | 0 | +| Low | 7 | + + +# Low Issues + +## L-1: Centralization Risk + +Contracts have owners with privileged rights to perform admin tasks and need to be trusted to not perform malicious updates or drain funds. + +
6 Found Instances + + +- Found in src/deployment/RuleEngine.sol [Line: 21](src/deployment/RuleEngine.sol#L21) + + ```solidity + contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase, AccessControl { + ``` + +- 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: 21](src/deployment/RuleEngineOwnable.sol#L21) + + ```solidity + contract RuleEngineOwnable is ERC2771ModuleStandalone, RuleEngineBase, Ownable { + ``` + +- 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-2: Unspecific Solidity Pragma + +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;` + +
12 Found Instances + + +- Found in src/RuleEngineBase.sol [Line: 3](src/RuleEngineBase.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- 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; + ``` + +- Found in src/interfaces/IERC3643Compliance.sol [Line: 3](src/interfaces/IERC3643Compliance.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/interfaces/IRule.sol [Line: 3](src/interfaces/IRule.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/interfaces/IRulesManagementModule.sol [Line: 3](src/interfaces/IRulesManagementModule.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/ERC2771ModuleStandalone.sol [Line: 3](src/modules/ERC2771ModuleStandalone.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/ERC3643ComplianceModule.sol [Line: 3](src/modules/ERC3643ComplianceModule.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/RulesManagementModule.sol [Line: 3](src/modules/RulesManagementModule.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/VersionModule.sol [Line: 3](src/modules/VersionModule.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/RulesManagementModuleInvariantStorage.sol [Line: 3](src/modules/library/RulesManagementModuleInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +
+ + + +## L-3: PUSH0 Opcode + +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. + +
14 Found Instances + + +- Found in src/RuleEngineBase.sol [Line: 3](src/RuleEngineBase.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- 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; + ``` + +- Found in src/interfaces/IERC3643Compliance.sol [Line: 3](src/interfaces/IERC3643Compliance.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/interfaces/IRule.sol [Line: 3](src/interfaces/IRule.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/interfaces/IRulesManagementModule.sol [Line: 3](src/interfaces/IRulesManagementModule.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/ERC2771ModuleStandalone.sol [Line: 3](src/modules/ERC2771ModuleStandalone.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/ERC3643ComplianceModule.sol [Line: 3](src/modules/ERC3643ComplianceModule.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/RulesManagementModule.sol [Line: 3](src/modules/RulesManagementModule.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/VersionModule.sol [Line: 3](src/modules/VersionModule.sol#L3) + + ```solidity + 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 + pragma solidity ^0.8.20; + ``` + +
+ + + +## L-4: Empty Block + +Consider removing empty blocks. + +
4 Found Instances + + +- 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: 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) { + ``` + +
+ + + +## 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/v3.0.0-rc2/slither-report-feedback.md b/doc/security/audits/tools/v3.0.0-rc2/slither-report-feedback.md new file mode 100644 index 0000000..6c2f217 --- /dev/null +++ b/doc/security/audits/tools/v3.0.0-rc2/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/v3.0.0-rc2/slither-report.md b/doc/security/audits/tools/v3.0.0-rc2/slither-report.md new file mode 100644 index 0000000..75c678b --- /dev/null +++ b/doc/security/audits/tools/v3.0.0-rc2/slither-report.md @@ -0,0 +1,104 @@ +**THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results. +Summary + - [calls-loop](#calls-loop) (10 results) (Low) + - [unindexed-event-address](#unindexed-event-address) (2 results) (Informational) +## calls-loop +Impact: Low +Confidence: Medium + - [ ] ID-0 +[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#L176-L184 + + + - [ ] ID-1 +[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#L154-L168 + + + - [ ] ID-2 +[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#L176-L184 + + + - [ ] ID-3 +[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 + + + - [ ] 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) + +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: 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 + + diff --git a/doc/specification/README.pdf b/doc/specification/README.pdf deleted file mode 100644 index 7ea2e38..0000000 Binary files a/doc/specification/README.pdf and /dev/null differ diff --git a/doc/specification/RuleEngine_specificationv3.0.0-rc0.odg b/doc/specification/RuleEngine_specificationv3.0.0-rc0.odg index c164da2..b6f6e3f 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/cover_page.pdf b/doc/specification/cover_page.pdf deleted file mode 100644 index 45822f4..0000000 Binary files a/doc/specification/cover_page.pdf and /dev/null differ diff --git a/doc/specification/previous_specification/RuleEngine_specificationv3.0.0-rc2.pdf b/doc/specification/previous_specification/RuleEngine_specificationv3.0.0-rc2.pdf new file mode 100644 index 0000000..1a69e38 Binary files /dev/null and b/doc/specification/previous_specification/RuleEngine_specificationv3.0.0-rc2.pdf differ diff --git a/src/RuleEngineBase.sol b/src/RuleEngineBase.sol index 5d99d1c..4e86787 100644 --- a/src/RuleEngineBase.sol +++ b/src/RuleEngineBase.sol @@ -21,6 +21,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 {ERC1404InterfaceId} from "./modules/library/ERC1404InterfaceId.sol"; import {RuleEngineInvariantStorage} from "./modules/library/RuleEngineInvariantStorage.sol"; import {RuleInterfaceId} from "./modules/library/RuleInterfaceId.sol"; @@ -191,7 +192,7 @@ abstract contract RuleEngineBase is * @dev Override to add ERC-165 interface check for the full IRule hierarchy. */ function _checkRule(address rule_) internal view virtual override { - super._checkRule(rule_); + RulesManagementModule._checkRule(rule_); if (!ERC165Checker.supportsInterface(rule_, RuleInterfaceId.IRULE_INTERFACE_ID)) { revert RuleEngine_RuleInvalidInterface(); } @@ -203,6 +204,7 @@ abstract contract RuleEngineBase is */ function _supportsRuleEngineBaseInterface(bytes4 interfaceId) internal pure returns (bool) { return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + || interfaceId == ERC1404InterfaceId.IERC1404_INTERFACE_ID || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID || interfaceId == ComplianceInterfaceId.IERC7551_COMPLIANCE_INTERFACE_ID; diff --git a/src/RuleEngineOwnableShared.sol b/src/RuleEngineOwnableShared.sol index 4193758..e4d2ab5 100644 --- a/src/RuleEngineOwnableShared.sol +++ b/src/RuleEngineOwnableShared.sol @@ -4,20 +4,21 @@ pragma solidity ^0.8.20; /* ==== OpenZeppelin === */ import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; /* ==== Modules === */ import {ERC2771ModuleStandalone, ERC2771Context} from "./modules/ERC2771ModuleStandalone.sol"; /* ==== Base contract === */ import {RuleEngineBase} from "./RuleEngineBase.sol"; +import {OwnableInterfaceId} from "./modules/library/OwnableInterfaceId.sol"; +import {IRule} from "./interfaces/IRule.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; - +abstract contract RuleEngineOwnableShared is ERC2771ModuleStandalone, RuleEngineBase, ERC165 { constructor(address forwarderIrrevocable, address tokenContract) ERC2771ModuleStandalone(forwarderIrrevocable) { if (tokenContract != address(0)) { _bindToken(tokenContract); @@ -25,10 +26,18 @@ abstract contract RuleEngineOwnableShared is ERC2771ModuleStandalone, RuleEngine } /* ============ ERC-165 ============ */ - function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { - return _supportsRuleEngineBaseInterface(interfaceId) - || interfaceId == ERC173_INTERFACE_ID - || interfaceId == type(IERC165).interfaceId; + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return _supportsRuleEngineBaseInterface(interfaceId) || interfaceId == OwnableInterfaceId.IERC173_INTERFACE_ID + || ERC165.supportsInterface(interfaceId); + } + + /** + * @dev Shared guard for ownership transfer targets in ownable variants. + */ + function _checkOwnershipTransferTarget(address newOwner) internal view virtual { + if (containsRule(IRule(newOwner))) { + revert RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges(); + } } /*////////////////////////////////////////////////////////////// diff --git a/src/deployment/RuleEngine.sol b/src/deployment/RuleEngine.sol index e9a501b..44f8c54 100644 --- a/src/deployment/RuleEngine.sol +++ b/src/deployment/RuleEngine.sol @@ -8,6 +8,7 @@ import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol" import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; /* ==== Modules === */ import {ERC2771ModuleStandalone, ERC2771Context} from "../modules/ERC2771ModuleStandalone.sol"; /* ==== Base contract === */ @@ -17,6 +18,8 @@ import {RuleEngineBase} from "../RuleEngineBase.sol"; * @title Implementation of a ruleEngine as defined by the CMTAT */ contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase, AccessControlEnumerable { + using EnumerableSet for EnumerableSet.AddressSet; + /** * @param admin Address of the contract (Access Control) * @param forwarderIrrevocable Address of the forwarder, required for the gasless support @@ -34,6 +37,17 @@ contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase, AccessControlEnu } /* ============ ACCESS CONTROL ============ */ + /** + * @notice Grants `role` to `account`. + * @dev Prevents granting any role to accounts already configured as rules. + */ + function grantRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { + if (_rules.contains(account)) { + revert RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges(); + } + AccessControl.grantRole(role, account); + } + /** * @notice Returns `true` if `account` has been granted `role`. * @dev The Default Admin has all roles @@ -68,6 +82,7 @@ contract RuleEngine is ERC2771ModuleStandalone, RuleEngineBase, AccessControlEnu //////////////////////////////////////////////////////////////*/ function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + function _onlyRulesLimitManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} /** * @dev This surcharge is not necessary if you do not use the MetaTxModule diff --git a/src/deployment/RuleEngineOwnable.sol b/src/deployment/RuleEngineOwnable.sol index 7753874..391c066 100644 --- a/src/deployment/RuleEngineOwnable.sol +++ b/src/deployment/RuleEngineOwnable.sol @@ -25,12 +25,22 @@ contract RuleEngineOwnable is RuleEngineOwnableShared, Ownable { * @dev Access control check using Ownable pattern */ function _onlyRulesManager() internal virtual override onlyOwner {} + function _onlyRulesLimitManager() internal virtual override onlyOwner {} /** * @dev Access control check using Ownable pattern */ function _onlyComplianceManager() internal virtual override onlyOwner {} + /** + * @notice Transfers ownership of the contract to a new account (`newOwner`). + * @dev Reverts when `newOwner` is already configured as a rule. + */ + function transferOwnership(address newOwner) public virtual override onlyOwner { + RuleEngineOwnableShared._checkOwnershipTransferTarget(newOwner); + Ownable.transferOwnership(newOwner); + } + /** * @dev This surcharge is not necessary if you do not use the MetaTxModule */ @@ -51,5 +61,4 @@ contract RuleEngineOwnable is RuleEngineOwnableShared, Ownable { 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 index 2b7240a..39c4e15 100644 --- a/src/deployment/RuleEngineOwnable2Step.sol +++ b/src/deployment/RuleEngineOwnable2Step.sol @@ -7,6 +7,7 @@ import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; import {RuleEngineOwnableShared} from "../RuleEngineOwnableShared.sol"; +import {Ownable2StepInterfaceId} from "../modules/library/Ownable2StepInterfaceId.sol"; /** * @title Implementation of a ruleEngine with ERC-173 Ownable2Step access control @@ -27,12 +28,28 @@ contract RuleEngineOwnable2Step is RuleEngineOwnableShared, Ownable2Step { * @dev Access control check using Ownable pattern */ function _onlyRulesManager() internal virtual override onlyOwner {} + function _onlyRulesLimitManager() internal virtual override onlyOwner {} /** * @dev Access control check using Ownable pattern */ function _onlyComplianceManager() internal virtual override onlyOwner {} + /** + * @notice Starts ownership transfer to `newOwner`. + * @dev Reverts when `newOwner` is already configured as a rule. + */ + function transferOwnership(address newOwner) public virtual override onlyOwner { + RuleEngineOwnableShared._checkOwnershipTransferTarget(newOwner); + Ownable2Step.transferOwnership(newOwner); + } + + /* ============ ERC-165 ============ */ + function supportsInterface(bytes4 interfaceId) public view virtual override(RuleEngineOwnableShared) returns (bool) { + return interfaceId == Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID + || RuleEngineOwnableShared.supportsInterface(interfaceId); + } + /** * @dev This surcharge is not necessary if you do not use the MetaTxModule */ diff --git a/src/interfaces/IRulesManagementModule.sol b/src/interfaces/IRulesManagementModule.sol index 81dfced..17c1242 100644 --- a/src/interfaces/IRulesManagementModule.sol +++ b/src/interfaces/IRulesManagementModule.sol @@ -6,6 +6,18 @@ pragma solidity ^0.8.20; import {IRule} from "./IRule.sol"; interface IRulesManagementModule { + /** + * @notice Returns the maximum number of rules allowed in the engine. + */ + function maxRules() external view returns (uint256); + + /** + * @notice Updates the maximum number of rules allowed in the engine. + * @dev Access control is implementation specific (admin/owner). + * @param maxRules_ New maximum number of rules. + */ + function setMaxRules(uint256 maxRules_) external; + /** * @notice Defines the rules for the rule engine. * @dev Sets the list of rule contract addresses for s. diff --git a/src/mocks/IERC1404Subset.sol b/src/mocks/IERC1404Subset.sol new file mode 100644 index 0000000..990f8aa --- /dev/null +++ b/src/mocks/IERC1404Subset.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/** + * @dev Test-only subset matching IERC1404 for interfaceId checks. + */ +interface IERC1404Subset { + function detectTransferRestriction(address from, address to, uint256 value) external view returns (uint8); + function messageForTransferRestriction(uint8 restrictionCode) external view returns (string memory); +} diff --git a/src/mocks/IERC173Subset.sol b/src/mocks/IERC173Subset.sol new file mode 100644 index 0000000..aca9d79 --- /dev/null +++ b/src/mocks/IERC173Subset.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/** + * @dev Test-only subset matching ERC-173 for interfaceId checks. + */ +interface IERC173Subset { + function owner() external view returns (address); + function transferOwnership(address newOwner) external; +} diff --git a/src/mocks/IOwnable2StepSubset.sol b/src/mocks/IOwnable2StepSubset.sol new file mode 100644 index 0000000..fe5b99f --- /dev/null +++ b/src/mocks/IOwnable2StepSubset.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/** + * @dev Test-only subset for Ownable2Step-specific ERC-165 checks. + */ +interface IOwnable2StepSubset { + function pendingOwner() external view returns (address); + function acceptOwnership() external; +} diff --git a/src/mocks/RuleEngineExposed.sol b/src/mocks/RuleEngineExposed.sol index 948dd0f..80d5da9 100644 --- a/src/mocks/RuleEngineExposed.sol +++ b/src/mocks/RuleEngineExposed.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; import {RuleEngine} from "../deployment/RuleEngine.sol"; import {RuleEngineOwnable} from "../deployment/RuleEngineOwnable.sol"; +import {RuleEngineOwnable2Step} from "../deployment/RuleEngineOwnable2Step.sol"; /** * @title RuleEngineExposed @@ -27,3 +28,15 @@ contract RuleEngineOwnableExposed is RuleEngineOwnable { return _msgData(); } } + +/** + * @title RuleEngineOwnable2StepExposed + * @dev Exposes internal functions for testing coverage + */ +contract RuleEngineOwnable2StepExposed is RuleEngineOwnable2Step { + constructor(address owner_, address forwarder, address token) RuleEngineOwnable2Step(owner_, forwarder, token) {} + + function exposedMsgData() external view returns (bytes memory) { + return _msgData(); + } +} diff --git a/src/mocks/rules/operation/RuleConditionalTransferLight.sol b/src/mocks/rules/operation/RuleConditionalTransferLight.sol index dc864fa..0d9beb1 100644 --- a/src/mocks/rules/operation/RuleConditionalTransferLight.sol +++ b/src/mocks/rules/operation/RuleConditionalTransferLight.sol @@ -33,7 +33,7 @@ contract RuleConditionalTransferLight is AccessControl, RuleConditionalTransferL function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, IERC165) returns (bool) { return interfaceId == RULE_ENGINE_INTERFACE_ID || interfaceId == ERC1404EXTEND_INTERFACE_ID - || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || super.supportsInterface(interfaceId); + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || AccessControl.supportsInterface(interfaceId); } /** diff --git a/src/mocks/rules/operation/RuleOperationRevert.sol b/src/mocks/rules/operation/RuleOperationRevert.sol index a8c3cbb..1bc7fc7 100644 --- a/src/mocks/rules/operation/RuleOperationRevert.sol +++ b/src/mocks/rules/operation/RuleOperationRevert.sol @@ -23,7 +23,7 @@ contract RuleOperationRevert is AccessControl, IRule, RuleCommonInvariantStorage function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, IERC165) returns (bool) { return interfaceId == RULE_ENGINE_INTERFACE_ID || interfaceId == ERC1404EXTEND_INTERFACE_ID - || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || super.supportsInterface(interfaceId); + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || AccessControl.supportsInterface(interfaceId); } /** diff --git a/src/mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol b/src/mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol index fb3eb3a..b6cf6dd 100644 --- a/src/mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol +++ b/src/mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; -// forge-lint: disable-next-line(unaliased-plain-import) -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../../validation/abstract/RuleCommonInvariantStorage.sol"; diff --git a/src/mocks/rules/validation/RuleWhitelist.sol b/src/mocks/rules/validation/RuleWhitelist.sol index f728072..21aa75d 100644 --- a/src/mocks/rules/validation/RuleWhitelist.sol +++ b/src/mocks/rules/validation/RuleWhitelist.sol @@ -21,7 +21,7 @@ contract RuleWhitelist is RuleAddressList, RuleWhitelistCommon { function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, IERC165) returns (bool) { return interfaceId == RULE_ENGINE_INTERFACE_ID || interfaceId == ERC1404EXTEND_INTERFACE_ID - || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || super.supportsInterface(interfaceId); + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || AccessControl.supportsInterface(interfaceId); } /** diff --git a/src/modules/ERC3643ComplianceModule.sol b/src/modules/ERC3643ComplianceModule.sol index a7e2185..8a48bcc 100644 --- a/src/modules/ERC3643ComplianceModule.sol +++ b/src/modules/ERC3643ComplianceModule.sol @@ -46,8 +46,11 @@ abstract contract ERC3643ComplianceModule is Context, 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. + * @dev T-REX compatibility: allows token self-binding when caller equals + * `token`, because TREX `Token.setCompliance` invokes `compliance.bindToken(address(this))`. */ - function bindToken(address token) public virtual override onlyComplianceManager { + function bindToken(address token) public virtual override { + _authorizeComplianceBindingChange(token); _bindToken(token); } @@ -56,8 +59,11 @@ abstract contract ERC3643ComplianceModule is Context, 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. + * @dev T-REX compatibility: allows token self-unbinding when caller equals + * `token`, because TREX token contracts may call `compliance.unbindToken(address(this))`. */ - function unbindToken(address token) public virtual override onlyComplianceManager { + function unbindToken(address token) public virtual override { + _authorizeComplianceBindingChange(token); _unbindToken(token); } @@ -102,11 +108,21 @@ abstract contract ERC3643ComplianceModule is Context, IERC3643Compliance { emit TokenBound(token); } - function _checkBoundToken() internal view virtual{ + function _checkBoundToken() internal view virtual { if (!_boundTokens.contains(_msgSender())) { revert RuleEngine_ERC3643Compliance_UnauthorizedCaller(); } } + /** + * @dev Authorizes bind/unbind operations. + * Allows compliance manager, or token self-calls for T-REX compatibility. + */ + function _authorizeComplianceBindingChange(address token) internal virtual { + if (_msgSender() != token) { + _onlyComplianceManager(); + } + } + function _onlyComplianceManager() internal virtual; } diff --git a/src/modules/RulesManagementModule.sol b/src/modules/RulesManagementModule.sol index 72ed6fa..4f56938 100644 --- a/src/modules/RulesManagementModule.sol +++ b/src/modules/RulesManagementModule.sol @@ -18,12 +18,19 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage _; } + modifier onlyRulesLimitManager() { + _onlyRulesLimitManager(); + _; + } + /* ==== Type declaration === */ using EnumerableSet for EnumerableSet.AddressSet; /* ==== State Variables === */ /// @dev Array of rules EnumerableSet.AddressSet internal _rules; + /// @dev Maximum number of rules allowed in the engine. + uint256 internal _maxRules = 10; /*////////////////////////////////////////////////////////////// PUBLIC/EXTERNAL FUNCTIONS @@ -37,8 +44,6 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage * 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}. */ @@ -46,6 +51,9 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage if (rules_.length == 0) { revert RuleEngine_RulesManagementModule_ArrayIsEmpty(); } + if (rules_.length > _maxRules) { + revert RuleEngine_RulesManagementModule_MaxRulesExceeded(_maxRules); + } if (_rules.length() > 0) { _clearRules(); } @@ -66,16 +74,36 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage /** * @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. + * @dev Reverts when the configured maximum number of rules is already reached. * Security convention: do not grant {RULES_MANAGEMENT_ROLE} to rule contracts. */ function addRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { + if (_rules.length() >= _maxRules) { + revert RuleEngine_RulesManagementModule_MaxRulesExceeded(_maxRules); + } _checkRule(address(rule_)); require(_rules.add(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); emit AddRule(rule_); } + /** + * @inheritdoc IRulesManagementModule + */ + function maxRules() public view virtual override(IRulesManagementModule) returns (uint256) { + return _maxRules; + } + + /** + * @inheritdoc IRulesManagementModule + */ + function setMaxRules(uint256 maxRules_) public virtual override(IRulesManagementModule) onlyRulesLimitManager { + if (maxRules_ == 0) { + revert RuleEngine_RulesManagementModule_MaxRulesZeroNotAllowed(); + } + _maxRules = maxRules_; + emit SetMaxRules(maxRules_); + } + /** * @inheritdoc IRulesManagementModule */ @@ -197,4 +225,5 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage } function _onlyRulesManager() internal virtual; + function _onlyRulesLimitManager() internal virtual; } diff --git a/src/modules/library/ERC1404InterfaceId.sol b/src/modules/library/ERC1404InterfaceId.sol new file mode 100644 index 0000000..b4bd60e --- /dev/null +++ b/src/modules/library/ERC1404InterfaceId.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/** + * @title ERC1404InterfaceId + * @dev ERC-165 interface IDs for ERC-1404 interfaces. + */ +library ERC1404InterfaceId { + bytes4 public constant IERC1404_INTERFACE_ID = 0xab84a5c8; +} diff --git a/src/modules/library/Ownable2StepInterfaceId.sol b/src/modules/library/Ownable2StepInterfaceId.sol new file mode 100644 index 0000000..a04a4c7 --- /dev/null +++ b/src/modules/library/Ownable2StepInterfaceId.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/** + * @title Ownable2StepInterfaceId + * @dev ERC-165 interface ID for Ownable2Step-specific functions only. + */ +library Ownable2StepInterfaceId { + // bytes4(keccak256("acceptOwnership()")) ^ bytes4(keccak256("pendingOwner()")) + bytes4 public constant IOWNABLE2STEP_INTERFACE_ID = 0x9ab669ef; +} diff --git a/src/modules/library/OwnableInterfaceId.sol b/src/modules/library/OwnableInterfaceId.sol new file mode 100644 index 0000000..af9bb7d --- /dev/null +++ b/src/modules/library/OwnableInterfaceId.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/** + * @title OwnableInterfaceId + * @dev ERC-165 interface IDs used by ownable RuleEngine variants. + */ +library OwnableInterfaceId { + bytes4 public constant IERC173_INTERFACE_ID = 0x7f5828d0; +} diff --git a/src/modules/library/RulesManagementModuleInvariantStorage.sol b/src/modules/library/RulesManagementModuleInvariantStorage.sol index 6358bf0..377a031 100644 --- a/src/modules/library/RulesManagementModuleInvariantStorage.sol +++ b/src/modules/library/RulesManagementModuleInvariantStorage.sol @@ -5,12 +5,17 @@ pragma solidity ^0.8.20; import {IRule} from "../../interfaces/IRule.sol"; abstract contract RulesManagementModuleInvariantStorage { + //uint256 public constant DEFAULT_MAX_RULES = 10; + /* ==== Errors === */ error RuleEngine_RulesManagementModule_RuleAddressZeroNotAllowed(); error RuleEngine_RulesManagementModule_RuleAlreadyExists(); error RuleEngine_RulesManagementModule_RuleDoNotMatch(); error RuleEngine_RulesManagementModule_ArrayIsEmpty(); error RuleEngine_RulesManagementModule_OperationNotSuccessful(); + error RuleEngine_RulesManagementModule_MaxRulesExceeded(uint256 maxRules); + error RuleEngine_RulesManagementModule_MaxRulesZeroNotAllowed(); + error RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges(); /* ============ Events ============ */ /** @@ -30,6 +35,12 @@ abstract contract RulesManagementModuleInvariantStorage { */ event ClearRules(); + /** + * @notice Emitted when the maximum allowed number of rules is updated. + * @param maxRules The new rule cap. + */ + event SetMaxRules(uint256 maxRules); + /* ==== Constant === */ /// @notice Role to manage the ruleEngine // Will not be present in the final bytecode if not used diff --git a/test/HelperContractOwnable2Step.sol b/test/HelperContractOwnable2Step.sol index 0ed3205..ace2ff8 100644 --- a/test/HelperContractOwnable2Step.sol +++ b/test/HelperContractOwnable2Step.sol @@ -17,5 +17,5 @@ abstract contract HelperContractOwnable2Step { RuleEngineOwnable2Step public ruleEngineMock; RuleConditionalTransferLight public ruleConditionalTransferLight; - string internal constant ERC2771ForwarderDomain = "ERC2771ForwarderDomain"; + string internal constant ERC2771_FORWARDER_DOMAIN = "ERC2771ForwarderDomain"; } diff --git a/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol b/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol index 22ec64a..2521e97 100644 --- a/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol +++ b/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../../HelperContract.sol"; - /** * @title Tests on the Access Control */ @@ -95,4 +94,40 @@ contract RuleEngineTest is Test, HelperContract { vm.expectRevert(ERC3643ComplianceModule.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); ruleEngineMock.transferred(address(0), ADDRESS1, ADDRESS2, 10); } + + function testCannotAttackerSetMaxRules() public { + vm.prank(ATTACKER); + vm.expectRevert(abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, bytes32(0))); + ruleEngineMock.setMaxRules(12); + } + + function testRulesManagerCannotSetMaxRulesWithoutAdminRole() public { + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.grantRole(RULES_MANAGEMENT_ROLE, WHITELIST_OPERATOR_ADDRESS); + + vm.prank(WHITELIST_OPERATOR_ADDRESS); + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, WHITELIST_OPERATOR_ADDRESS, bytes32(0)) + ); + ruleEngineMock.setMaxRules(12); + } + + function testCannotGrantRulesManagementRoleToRuleAccount() public { + vm.expectRevert(RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges.selector); + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.grantRole(RULES_MANAGEMENT_ROLE, address(ruleWhitelist)); + } + + function testCannotGrantDefaultAdminRoleToRuleAccount() public { + vm.expectRevert(RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges.selector); + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.grantRole(bytes32(0), address(ruleWhitelist)); + } + + function testCannotGrantArbitraryRoleToRuleAccount() public { + assertTrue(ruleEngineMock.containsRule(ruleWhitelist)); + vm.expectRevert(RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges.selector); + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.grantRole(keccak256("ARBITRARY_ROLE"), address(ruleWhitelist)); + } } diff --git a/test/RuleEngine/ERC3643Compliance.t.sol b/test/RuleEngine/ERC3643Compliance.t.sol index 7f22385..b936a43 100644 --- a/test/RuleEngine/ERC3643Compliance.t.sol +++ b/test/RuleEngine/ERC3643Compliance.t.sol @@ -162,6 +162,38 @@ contract RuleEngineTest is Test, HelperContract { ruleEngine.bindToken(address(0x1)); } + function testTokenCanBindItself() public { + vm.prank(address(token1)); + ruleEngine.bindToken(address(token1)); + + assertTrue(ruleEngine.isTokenBound(address(token1))); + } + + function testBoundTokenCanUnbindItself() public { + vm.prank(address(token1)); + ruleEngine.bindToken(address(token1)); + + vm.prank(address(token1)); + ruleEngine.unbindToken(address(token1)); + + assertFalse(ruleEngine.isTokenBound(address(token1))); + } + + function testTokenCannotBindAnotherToken() public { + vm.expectRevert(); + vm.prank(address(token1)); + ruleEngine.bindToken(address(token2)); + } + + function testTokenCannotUnbindAnotherToken() public { + vm.prank(operator); + ruleEngine.bindToken(address(token2)); + + vm.expectRevert(); + vm.prank(address(token1)); + ruleEngine.unbindToken(address(token2)); + } + function testCannotCreatedIfNotBound() public { vm.expectRevert(ERC3643ComplianceModule.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); ruleEngine.created(user1, 100); diff --git a/test/RuleEngine/RuleEngineCoverage.t.sol b/test/RuleEngine/RuleEngineCoverage.t.sol index 5f9e9e2..3328f5f 100644 --- a/test/RuleEngine/RuleEngineCoverage.t.sol +++ b/test/RuleEngine/RuleEngineCoverage.t.sol @@ -8,7 +8,9 @@ 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 {IERC1404Subset} from "src/mocks/IERC1404Subset.sol"; import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol"; +import {ERC1404InterfaceId} from "src/modules/library/ERC1404InterfaceId.sol"; /** * @title Coverage tests for RuleEngine (supportsInterface, _msgData, ERC-165 rule check) @@ -39,6 +41,11 @@ contract RuleEngineCoverageTest is Test, HelperContract { assertTrue(ruleEngineMock.supportsInterface(ERC1404_EXTEND_ID)); } + function testSupportsERC1404Interface() public view { + assertTrue(ruleEngineMock.supportsInterface(ERC1404InterfaceId.IERC1404_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(IERC1404Subset).interfaceId)); + } + function testSupportsERC3643ComplianceInterface() public view { assertTrue(ruleEngineMock.supportsInterface(type(ICompliance).interfaceId)); } diff --git a/test/RuleEngine/RuleEngineDeployment.t.sol b/test/RuleEngine/RuleEngineDeployment.t.sol index 841adc0..14ebbe0 100644 --- a/test/RuleEngine/RuleEngineDeployment.t.sol +++ b/test/RuleEngine/RuleEngineDeployment.t.sol @@ -12,6 +12,8 @@ import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.s import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; import {ICompliance} from "src/mocks/ICompliance.sol"; import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol"; +import {IERC1404Subset} from "src/mocks/IERC1404Subset.sol"; +import {ERC1404InterfaceId} from "src/modules/library/ERC1404InterfaceId.sol"; /** * @title General functions of the RuleEngine @@ -62,6 +64,8 @@ contract RuleEngineTest is Test, HelperContract { assertTrue(ruleEngineMock.supportsInterface(type(IAccessControl).interfaceId)); assertTrue(ruleEngineMock.supportsInterface(type(IAccessControlEnumerable).interfaceId)); assertTrue(ruleEngineMock.supportsInterface(RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(ERC1404InterfaceId.IERC1404_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(IERC1404Subset).interfaceId)); assertTrue(ruleEngineMock.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID)); assertTrue(ruleEngineMock.supportsInterface(type(ICompliance).interfaceId)); assertTrue(ruleEngineMock.supportsInterface(type(IERC7551ComplianceSubset).interfaceId)); diff --git a/test/RuleEngine/RulesManagementModuleTest/CMTATIntegrationBase.sol b/test/RuleEngine/RulesManagementModuleTest/CMTATIntegrationBase.sol index a9c0ae8..072da8c 100644 --- a/test/RuleEngine/RulesManagementModuleTest/CMTATIntegrationBase.sol +++ b/test/RuleEngine/RulesManagementModuleTest/CMTATIntegrationBase.sol @@ -6,8 +6,6 @@ import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../../HelperContract.sol"; - - /** * @title Base integration test for RuleEngine with CMTAT */ diff --git a/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperation.t.sol b/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperation.t.sol index 2d61617..72f76bf 100644 --- a/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperation.t.sol +++ b/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperation.t.sol @@ -5,8 +5,6 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../../HelperContract.sol"; - - /** * @title General functions of the RuleEngine */ @@ -218,6 +216,98 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { assertEq(resUint256, 2); } + function testDefaultMaxRulesIsTen() public view { + assertEq(ruleEngineMock.maxRules(), 10); + } + + function testCannotAddRuleAboveMaxRules() public { + for (uint256 i = 0; i < 9; ++i) { + vm.prank(WHITELIST_OPERATOR_ADDRESS); + RuleConditionalTransferLight rule = + new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.addRule(rule); + } + + vm.prank(WHITELIST_OPERATOR_ADDRESS); + RuleConditionalTransferLight extraRule = + new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + vm.expectRevert( + abi.encodeWithSelector( + RuleEngine_RulesManagementModule_MaxRulesExceeded.selector, ruleEngineMock.maxRules() + ) + ); + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.addRule(extraRule); + } + + function testCannotSetRulesAboveMaxRules() public { + IRule[] memory manyRules = new IRule[](11); + for (uint256 i = 0; i < 11; ++i) { + vm.prank(WHITELIST_OPERATOR_ADDRESS); + RuleConditionalTransferLight rule = + new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + manyRules[i] = IRule(rule); + } + + vm.expectRevert( + abi.encodeWithSelector( + RuleEngine_RulesManagementModule_MaxRulesExceeded.selector, ruleEngineMock.maxRules() + ) + ); + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.setRules(manyRules); + } + + function testCanSetRulesAtMaxRulesBoundary() public { + IRule[] memory atMaxRules = new IRule[](10); + for (uint256 i = 0; i < 10; ++i) { + vm.prank(WHITELIST_OPERATOR_ADDRESS); + RuleConditionalTransferLight rule = + new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + atMaxRules[i] = IRule(rule); + } + + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.setRules(atMaxRules); + assertEq(ruleEngineMock.rulesCount(), 10); + } + + function testAdminCanUpdateMaxRules() public { + vm.expectEmit(false, false, false, true); + emit RulesManagementModuleInvariantStorage.SetMaxRules(15); + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.setMaxRules(15); + assertEq(ruleEngineMock.maxRules(), 15); + } + + function testCannotSetMaxRulesToZero() public { + vm.expectRevert(RuleEngine_RulesManagementModule_MaxRulesZeroNotAllowed.selector); + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.setMaxRules(0); + } + + function testLoweredMaxRulesDoesNotRemoveExistingRulesButBlocksNewAdds() public { + vm.prank(WHITELIST_OPERATOR_ADDRESS); + RuleConditionalTransferLight secondRule = + new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.addRule(secondRule); + assertEq(ruleEngineMock.rulesCount(), 2); + + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.setMaxRules(1); + assertEq(ruleEngineMock.maxRules(), 1); + assertEq(ruleEngineMock.rulesCount(), 2); + + vm.prank(WHITELIST_OPERATOR_ADDRESS); + RuleConditionalTransferLight thirdRule = + new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + vm.expectRevert(abi.encodeWithSelector(RuleEngine_RulesManagementModule_MaxRulesExceeded.selector, uint256(1))); + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.addRule(thirdRule); + } + function testCannotAddRuleZeroAddress() public { // Act vm.expectRevert(RuleEngine_RulesManagementModule_RuleAddressZeroNotAllowed.selector); diff --git a/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperationRevertBase.sol b/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperationRevertBase.sol index 8aa6d5e..d63de8a 100644 --- a/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperationRevertBase.sol +++ b/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperationRevertBase.sol @@ -7,7 +7,6 @@ import "../../HelperContract.sol"; import {RuleOperationRevert} from "src/mocks/rules/operation/RuleOperationRevert.sol"; - /** * @title Base test for RuleEngine operation revert with CMTAT */ diff --git a/test/RuleEngine/RulesManagementModuleTest/RuleEngineRestriction.t.sol b/test/RuleEngine/RulesManagementModuleTest/RuleEngineRestriction.t.sol index 67d9f44..2405f4b 100644 --- a/test/RuleEngine/RulesManagementModuleTest/RuleEngineRestriction.t.sol +++ b/test/RuleEngine/RulesManagementModuleTest/RuleEngineRestriction.t.sol @@ -5,8 +5,6 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../../HelperContract.sol"; - - //ADmin, forwarder irrect /RuleEngine /** * @title General functions of the RuleEngine diff --git a/test/RuleEngine/ruleEngineValidation/RuleEngineRestriction.sol b/test/RuleEngine/ruleEngineValidation/RuleEngineRestriction.sol index 324f603..31d0e54 100644 --- a/test/RuleEngine/ruleEngineValidation/RuleEngineRestriction.sol +++ b/test/RuleEngine/ruleEngineValidation/RuleEngineRestriction.sol @@ -5,8 +5,6 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../../HelperContract.sol"; - - /** * @title tests concerning the restrictions and for the transfers */ diff --git a/test/RuleEngine/ruleEngineValidation/RuleEngineValidation.sol b/test/RuleEngine/ruleEngineValidation/RuleEngineValidation.sol index 6f2f9f6..38328d7 100644 --- a/test/RuleEngine/ruleEngineValidation/RuleEngineValidation.sol +++ b/test/RuleEngine/ruleEngineValidation/RuleEngineValidation.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../../HelperContract.sol"; - /** * @title General functions of the RuleEngine */ diff --git a/test/RuleEngineOwnable/AccessControl/RuleEngineOwnableAccessControl.t.sol b/test/RuleEngineOwnable/AccessControl/RuleEngineOwnableAccessControl.t.sol index 5d0b96c..fd4685b 100644 --- a/test/RuleEngineOwnable/AccessControl/RuleEngineOwnableAccessControl.t.sol +++ b/test/RuleEngineOwnable/AccessControl/RuleEngineOwnableAccessControl.t.sol @@ -77,6 +77,14 @@ contract RuleEngineOwnableAccessControlTest is Test, HelperContractOwnable { assertEq(ruleEngineMock.rulesCount(), 0); } + function testOwnerCanSetMaxRules() public { + vm.expectEmit(false, false, false, true); + emit RulesManagementModuleInvariantStorage.SetMaxRules(12); + vm.prank(OWNER_ADDRESS); + ruleEngineMock.setMaxRules(12); + assertEq(ruleEngineMock.maxRules(), 12); + } + function testOwnerCanBindToken() public { // Act vm.prank(OWNER_ADDRESS); @@ -143,6 +151,12 @@ contract RuleEngineOwnableAccessControlTest is Test, HelperContractOwnable { ruleEngineMock.clearRules(); } + function testNonOwnerCannotSetMaxRules() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, ATTACKER)); + vm.prank(ATTACKER); + ruleEngineMock.setMaxRules(12); + } + function testNonOwnerCannotBindToken() public { // Act & Assert vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, ATTACKER)); @@ -205,6 +219,15 @@ contract RuleEngineOwnableAccessControlTest is Test, HelperContractOwnable { ruleEngineMock.transferOwnership(ATTACKER); } + function testOwnerCannotTransferOwnershipToRuleAddress() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.addRule(ruleConditionalTransferLight); + + vm.expectRevert(RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges.selector); + vm.prank(OWNER_ADDRESS); + ruleEngineMock.transferOwnership(address(ruleConditionalTransferLight)); + } + /*////////////////////////////////////////////////////////////// RENOUNCE OWNERSHIP //////////////////////////////////////////////////////////////*/ diff --git a/test/RuleEngineOwnable/ERC3643Compliance.t.sol b/test/RuleEngineOwnable/ERC3643Compliance.t.sol index 9856d36..0545a49 100644 --- a/test/RuleEngineOwnable/ERC3643Compliance.t.sol +++ b/test/RuleEngineOwnable/ERC3643Compliance.t.sol @@ -155,6 +155,38 @@ contract RuleEngineOwnableERC3643Test is Test, HelperContractOwnable { ruleEngineMock.bindToken(address(0x1)); } + function testTokenCanBindItself() public { + vm.prank(address(token1)); + ruleEngineMock.bindToken(address(token1)); + + assertTrue(ruleEngineMock.isTokenBound(address(token1))); + } + + function testBoundTokenCanUnbindItself() public { + vm.prank(address(token1)); + ruleEngineMock.bindToken(address(token1)); + + vm.prank(address(token1)); + ruleEngineMock.unbindToken(address(token1)); + + assertFalse(ruleEngineMock.isTokenBound(address(token1))); + } + + function testTokenCannotBindAnotherToken() public { + vm.expectRevert(); + vm.prank(address(token1)); + ruleEngineMock.bindToken(address(token2)); + } + + function testTokenCannotUnbindAnotherToken() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.bindToken(address(token2)); + + vm.expectRevert(); + vm.prank(address(token1)); + ruleEngineMock.unbindToken(address(token2)); + } + function testCannotCreatedIfNotBound() public { vm.expectRevert(ERC3643ComplianceModule.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); ruleEngineMock.created(user1, 100); diff --git a/test/RuleEngineOwnable/RuleEngineOwnableCoverage.t.sol b/test/RuleEngineOwnable/RuleEngineOwnableCoverage.t.sol index 52ef58d..cf5d616 100644 --- a/test/RuleEngineOwnable/RuleEngineOwnableCoverage.t.sol +++ b/test/RuleEngineOwnable/RuleEngineOwnableCoverage.t.sol @@ -9,7 +9,11 @@ import {RuleEngineOwnableExposed} from "src/mocks/RuleEngineExposed.sol"; import {RuleInvalidMock} from "src/mocks/RuleInvalidMock.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; import {ICompliance} from "src/mocks/ICompliance.sol"; +import {IERC173Subset} from "src/mocks/IERC173Subset.sol"; +import {IERC1404Subset} from "src/mocks/IERC1404Subset.sol"; import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol"; +import {ERC1404InterfaceId} from "src/modules/library/ERC1404InterfaceId.sol"; +import {OwnableInterfaceId} from "src/modules/library/OwnableInterfaceId.sol"; /** * @title Coverage tests for RuleEngineOwnable (_msgData, ERC-165 rule check) @@ -20,7 +24,6 @@ contract RuleEngineOwnableCoverageTest is Test, HelperContractOwnable { // Known interface IDs bytes4 constant RULE_ENGINE_ID = 0x20c49ce7; bytes4 constant ERC1404_EXTEND_ID = 0x78a8de7d; - bytes4 constant ERC173_ID = 0x7f5828d0; bytes4 constant ERC165_ID = 0x01ffc9a7; bytes4 constant INVALID_ID = 0xffffffff; @@ -41,8 +44,14 @@ contract RuleEngineOwnableCoverageTest is Test, HelperContractOwnable { assertTrue(ruleEngineMock.supportsInterface(ERC1404_EXTEND_ID)); } + function testSupportsERC1404Interface() public view { + assertTrue(ruleEngineMock.supportsInterface(ERC1404InterfaceId.IERC1404_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(IERC1404Subset).interfaceId)); + } + function testSupportsERC173Interface() public view { - assertTrue(ruleEngineMock.supportsInterface(ERC173_ID)); + assertTrue(ruleEngineMock.supportsInterface(OwnableInterfaceId.IERC173_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(IERC173Subset).interfaceId)); } function testSupportsERC3643ComplianceInterface() public view { diff --git a/test/RuleEngineOwnable/RuleEngineOwnableDeployment.t.sol b/test/RuleEngineOwnable/RuleEngineOwnableDeployment.t.sol index 939b19a..3ab94c6 100644 --- a/test/RuleEngineOwnable/RuleEngineOwnableDeployment.t.sol +++ b/test/RuleEngineOwnable/RuleEngineOwnableDeployment.t.sol @@ -9,7 +9,11 @@ import {IERC165} from "@openzeppelin/contracts/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 {IERC173Subset} from "src/mocks/IERC173Subset.sol"; +import {IERC1404Subset} from "src/mocks/IERC1404Subset.sol"; import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol"; +import {ERC1404InterfaceId} from "src/modules/library/ERC1404InterfaceId.sol"; +import {OwnableInterfaceId} from "src/modules/library/OwnableInterfaceId.sol"; /** * @title Deployment tests for RuleEngineOwnable @@ -66,7 +70,10 @@ contract RuleEngineOwnableDeploymentTest is Test, HelperContractOwnable { // Act & Assert assertTrue(ruleEngineMock.supportsInterface(type(IERC165).interfaceId)); - assertTrue(ruleEngineMock.supportsInterface(0x7f5828d0)); // ERC-173 + assertTrue(ruleEngineMock.supportsInterface(ERC1404InterfaceId.IERC1404_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(IERC1404Subset).interfaceId)); + assertTrue(ruleEngineMock.supportsInterface(OwnableInterfaceId.IERC173_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(IERC173Subset).interfaceId)); assertTrue(ruleEngineMock.supportsInterface(RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID)); assertTrue(ruleEngineMock.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID)); assertTrue(ruleEngineMock.supportsInterface(type(ICompliance).interfaceId)); diff --git a/test/RuleEngineOwnable/RulesManagementModuleTest/RuleEngineOwnableOperation.t.sol b/test/RuleEngineOwnable/RulesManagementModuleTest/RuleEngineOwnableOperation.t.sol index 6e4bf2f..530c5f6 100644 --- a/test/RuleEngineOwnable/RulesManagementModuleTest/RuleEngineOwnableOperation.t.sol +++ b/test/RuleEngineOwnable/RulesManagementModuleTest/RuleEngineOwnableOperation.t.sol @@ -5,8 +5,6 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../../HelperContractOwnable.sol"; - - /** * @title Rules management operations tests for RuleEngineOwnable */ diff --git a/test/RuleEngineOwnable2Step/RuleEngineOwnable2Step.t.sol b/test/RuleEngineOwnable2Step/RuleEngineOwnable2Step.t.sol index 2e096c2..6a5916f 100644 --- a/test/RuleEngineOwnable2Step/RuleEngineOwnable2Step.t.sol +++ b/test/RuleEngineOwnable2Step/RuleEngineOwnable2Step.t.sol @@ -8,7 +8,16 @@ import {IERC165} from "@openzeppelin/contracts/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 {IERC173Subset} from "src/mocks/IERC173Subset.sol"; +import {IOwnable2StepSubset} from "src/mocks/IOwnable2StepSubset.sol"; +import {IERC1404Subset} from "src/mocks/IERC1404Subset.sol"; import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol"; +import {ERC1404InterfaceId} from "src/modules/library/ERC1404InterfaceId.sol"; +import {OwnableInterfaceId} from "src/modules/library/OwnableInterfaceId.sol"; +import {Ownable2StepInterfaceId} from "src/modules/library/Ownable2StepInterfaceId.sol"; +import {ERC3643ComplianceModule} from "src/modules/ERC3643ComplianceModule.sol"; +import {RulesManagementModuleInvariantStorage} from "src/modules/library/RulesManagementModuleInvariantStorage.sol"; +import {RuleEngineOwnable2StepExposed} from "src/mocks/RuleEngineExposed.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../HelperContractOwnable2Step.sol"; @@ -16,10 +25,13 @@ import "../HelperContractOwnable2Step.sol"; * @title Deployment and ownership tests for RuleEngineOwnable2Step */ contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { - bytes4 constant ERC173_ID = 0x7f5828d0; + address internal constant TOKEN_1 = address(0x1111); + address internal constant TOKEN_2 = address(0x2222); + RuleEngineOwnable2StepExposed internal ruleEngineOwnable2StepExposed; function setUp() public { ruleEngineMock = new RuleEngineOwnable2Step(OWNER_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); + ruleEngineOwnable2StepExposed = new RuleEngineOwnable2StepExposed(OWNER_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); ruleConditionalTransferLight = new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); } @@ -35,7 +47,7 @@ contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { function testTrustedForwarderSetAtDeployment() public { MinimalForwarderMock forwarder = new MinimalForwarderMock(); - forwarder.initialize(ERC2771ForwarderDomain); + forwarder.initialize(ERC2771_FORWARDER_DOMAIN); RuleEngineOwnable2Step ruleEngineWithForwarder = new RuleEngineOwnable2Step(OWNER_ADDRESS, address(forwarder), ZERO_ADDRESS); @@ -44,7 +56,12 @@ contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { function testSupportsOwnableAndComplianceInterfaces() public view { assertTrue(ruleEngineMock.supportsInterface(type(IERC165).interfaceId)); - assertTrue(ruleEngineMock.supportsInterface(ERC173_ID)); + assertTrue(ruleEngineMock.supportsInterface(ERC1404InterfaceId.IERC1404_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(IERC1404Subset).interfaceId)); + assertTrue(ruleEngineMock.supportsInterface(OwnableInterfaceId.IERC173_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(IERC173Subset).interfaceId)); + assertTrue(ruleEngineMock.supportsInterface(Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID)); + assertTrue(ruleEngineMock.supportsInterface(type(IOwnable2StepSubset).interfaceId)); assertTrue(ruleEngineMock.supportsInterface(RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID)); assertTrue(ruleEngineMock.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID)); assertTrue(ruleEngineMock.supportsInterface(type(ICompliance).interfaceId)); @@ -57,12 +74,30 @@ contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { assertEq(ruleEngineMock.rulesCount(), 1); } + function testDefaultMaxRulesIsTen() public view { + assertEq(ruleEngineMock.maxRules(), 10); + } + + function testOwnerCanSetMaxRules() public { + vm.expectEmit(false, false, false, true); + emit RulesManagementModuleInvariantStorage.SetMaxRules(12); + vm.prank(OWNER_ADDRESS); + ruleEngineMock.setMaxRules(12); + assertEq(ruleEngineMock.maxRules(), 12); + } + function testNonOwnerCannotAddRule() public { vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, ATTACKER)); vm.prank(ATTACKER); ruleEngineMock.addRule(ruleConditionalTransferLight); } + function testNonOwnerCannotSetMaxRules() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, ATTACKER)); + vm.prank(ATTACKER); + ruleEngineMock.setMaxRules(12); + } + function testTransferOwnershipSetsPendingOwner() public { vm.prank(OWNER_ADDRESS); ruleEngineMock.transferOwnership(NEW_OWNER_ADDRESS); @@ -91,6 +126,18 @@ contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { ruleEngineMock.acceptOwnership(); } + function testOwnerCannotTransferOwnershipToRuleAddress() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.addRule(ruleConditionalTransferLight); + + vm.expectRevert( + RulesManagementModuleInvariantStorage.RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges + .selector + ); + vm.prank(OWNER_ADDRESS); + ruleEngineMock.transferOwnership(address(ruleConditionalTransferLight)); + } + function testOwnerKeepsRightsUntilAcceptOwnership() public { vm.prank(OWNER_ADDRESS); ruleEngineMock.transferOwnership(NEW_OWNER_ADDRESS); @@ -112,4 +159,43 @@ contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { vm.prank(OWNER_ADDRESS); ruleEngineMock.clearRules(); } + + function testTokenCanBindItself() public { + vm.prank(TOKEN_1); + ruleEngineMock.bindToken(TOKEN_1); + + assertTrue(ruleEngineMock.isTokenBound(TOKEN_1)); + } + + function testBoundTokenCanUnbindItself() public { + vm.prank(TOKEN_1); + ruleEngineMock.bindToken(TOKEN_1); + + vm.prank(TOKEN_1); + ruleEngineMock.unbindToken(TOKEN_1); + + assertFalse(ruleEngineMock.isTokenBound(TOKEN_1)); + } + + function testTokenCannotBindAnotherToken() public { + vm.expectRevert(); + vm.prank(TOKEN_1); + ruleEngineMock.bindToken(TOKEN_2); + } + + function testTokenCannotUnbindAnotherToken() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.bindToken(TOKEN_2); + + vm.expectRevert(); + vm.prank(TOKEN_1); + ruleEngineMock.unbindToken(TOKEN_2); + } + + function testMsgDataReturnsCalldata() public view { + bytes memory data = ruleEngineOwnable2StepExposed.exposedMsgData(); + assertEq(data.length, 4); + // forge-lint: disable-next-line(unsafe-typecast) + assertEq(bytes4(data), ruleEngineOwnable2StepExposed.exposedMsgData.selector); + } } diff --git a/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControl.t.sol b/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControl.t.sol index ddde5ad..1e5ba9c 100644 --- a/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControl.t.sol +++ b/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControl.t.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../../HelperContract.sol"; - /** * @title Tests on the Access Control */ diff --git a/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControlOZ.t.sol b/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControlOZ.t.sol index 314e912..771224e 100644 --- a/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControlOZ.t.sol +++ b/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControlOZ.t.sol @@ -6,7 +6,6 @@ import {Test} from "forge-std/Test.sol"; import "../../HelperContract.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; - /** * @title Tests on the provided functions by OpenZeppelin */ diff --git a/test/RuleWhitelist/CMTATIntegrationBase.sol b/test/RuleWhitelist/CMTATIntegrationBase.sol index fa02913..3ad50cb 100644 --- a/test/RuleWhitelist/CMTATIntegrationBase.sol +++ b/test/RuleWhitelist/CMTATIntegrationBase.sol @@ -6,7 +6,6 @@ import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../HelperContract.sol"; - /** * @title Base integration test with the CMTAT (whitelist) */ diff --git a/test/RuleWhitelist/RuleWhitelistAdd.t.sol b/test/RuleWhitelist/RuleWhitelistAdd.t.sol index 0c080b7..baebe68 100644 --- a/test/RuleWhitelist/RuleWhitelistAdd.t.sol +++ b/test/RuleWhitelist/RuleWhitelistAdd.t.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../HelperContract.sol"; - /** * @title Tests the functions to add addresses to the whitelist */ diff --git a/test/RuleWhitelist/RuleWhitelistRemove.t.sol b/test/RuleWhitelist/RuleWhitelistRemove.t.sol index 228bbef..4ff9b2c 100644 --- a/test/RuleWhitelist/RuleWhitelistRemove.t.sol +++ b/test/RuleWhitelist/RuleWhitelistRemove.t.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../HelperContract.sol"; - /** * @title Tests the functions to remove addresses from the whitelist */