diff --git a/.gitignore b/.gitignore index f9c867e..f8d680e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ lib/ out/ docOut/ cache/ +#coverage scratch output (the published report lives in doc/coverage/) +lcov.info +/coverage/ .~lock.test.odt# nethereum-gen.settings #hardhat diff --git a/.gitmodules b/.gitmodules index c4ae152..3d4fedd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "lib/CMTATv3.0.0"] path = lib/CMTATv3.0.0 url = https://github.com/CMTA/CMTAT +[submodule "ERC-3643"] + path = lib/ERC-3643 + url = https://github.com/ERC-3643/ERC-3643 diff --git a/AGENTS.md b/AGENTS.md index 89370db..6e92152 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,14 +2,14 @@ This file helps AI agents (Cursor, Claude Code, etc.) understand and work with this codebase. -AGENTS.md and CLAUDE.md files must always be identical +AGENTS.md and CLAUDE.md files must always be identical — always update both together. ## Project Summary **RuleEngine** is a Solidity smart contract system that enforces transfer restrictions for [CMTAT](https://github.com/CMTA/CMTAT) and [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) tokens. It acts as an external controller that calls pluggable rule contracts on each token transfer, mint, or burn. - **Version:** 3.0.0 (defined in `src/modules/VersionModule.sol`) -- **Solidity:** ^0.8.20 (compiled with 0.8.34) +- **Solidity:** ^0.8.20 (compiled with 0.8.36) - **EVM target:** Prague - **License:** MPL-2.0 @@ -21,13 +21,27 @@ forge test # Run all tests forge test -vvv # Verbose test output forge test --match-contract --match-test # Run specific test forge coverage # Code coverage -forge coverage --no-match-coverage "(script|mocks|test)" --report lcov # Production coverage +forge coverage --no-match-coverage "(mocks|test)" --report lcov # Production coverage (src/ + script/) forge fmt # Format code ``` Dependencies are git submodules. Initialize with `forge install`, update with `forge update`. CMTAT submodule also needs `cd lib/CMTAT && npm install` for its OpenZeppelin deps. +## Agent Workflow + +- **Never create git commits.** Provide commit messages only when they are requested. +- **Always run the full test suite (`forge test`) after any code modification** — including lint-driven or mechanical refactors — before reporting completion. +- **Always update the documentation** to reflect the latest change. There are two READMEs: `README.md` at the root is the short overview (project, architecture, main files, quick start); `doc/README.md` is the full reference (interfaces, Ethereum API, deployment, UML, audits). Update whichever the change affects — often both. +- After each implemented feature or fix, provide a **one-line GitHub commit message** covering all changes since the last commit. + +### When implementing a new rule or feature + +1. Create or update the technical documentation in `doc/technical` +2. Update `README.md` (root overview) and `doc/README.md` (full reference) as applicable +3. Create or update tests, targeting **100% code coverage** — check with `forge coverage --report summary` +4. Update `CHANGELOG.md` + ## Import Remappings | Alias | Path | @@ -132,31 +146,42 @@ function _checkRule(address rule_) internal view virtual override { ### Rule Execution Flow ``` -Token operation → RuleEngine.transferred(spender, from, to, value) ← CMTAT v3.3.0+ primary path +CMTAT only: RuleEngine.transferred(spender, from, to, value) ← transferFrom, mint, burn (spender = _msgSender()) ├── onlyBoundToken modifier (caller must be bound) └── for each rule in _rules: rule.transferred(spender, from, to, value) // reverts if disallowed - RuleEngine.transferred(from, to, value) ← 3-arg fallback (spender == address(0)) +CMTAT + ERC-3643: RuleEngine.transferred(from, to, value) ← standard transfer (spender == address(0)) ├── onlyBoundToken modifier └── for each rule in _rules: rule.transferred(from, to, value) - RuleEngine.created(to, value) ← ERC-3643 mint entry point +ERC-3643 only: RuleEngine.created(to, value) ← ERC-3643 mint entry point ├── onlyBoundToken modifier └── calls _transferred(address(0), to, value) - RuleEngine.destroyed(from, value) ← ERC-3643 burn entry point +ERC-3643 only: RuleEngine.destroyed(from, value) ← ERC-3643 burn entry point ├── onlyBoundToken modifier └── calls _transferred(from, address(0), value) ``` -Since CMTAT v3.3.0, mint (`from == address(0)`) and burn (`to == address(0)`) also go through the 4-argument overload with the operator as `spender`. Rules that check `spender` must skip or adapt that check for mint/burn to avoid blocking those operations unintentionally. +**CMTAT and ERC-3643 use disjoint entry points.** The 4-argument `transferred` is declared by CMTAT's `IRuleEngine` (`lib/CMTAT/contracts/interfaces/engine/IRuleEngine.sol`), so an ERC-3643 token never reaches it. Conversely `created` / `destroyed` are declared by `IERC3643Compliance` and CMTAT never calls them — CMTAT routes mint and burn through the 4-argument `transferred` instead. Only the 3-argument `transferred` is shared by both. + +CMTAT selects the overload in `ValidationModuleRuleEngine._callRuleEngineTransferred`, branching on `spender != address(0)`. A standard `transfer` has no spender (`CMTATBaseCommon.transfer` passes `address(0)` internally), so the `else` branch calls the **3-argument** `transferred(from, to, value)` — the zero address is a branch condition only and is never forwarded to the engine. `transferFrom`, `mint` and `burn` carry `_msgSender()` as spender and take the **4-argument** overload. Neither is a fallback: which one is called depends purely on the operation. + +Since CMTAT v3.3.0, mint (`from == address(0)`) and burn (`to == address(0)`) therefore also reach the 4-argument overload with the operator as `spender`. Rules that check `spender` must skip or adapt that check for mint/burn to avoid blocking those operations unintentionally. `created` and `destroyed` use the 3-argument `_transferred` path (no spender), consistent with the ERC-3643 spec which does not carry a spender for mint/burn. View path: `detectTransferRestriction()` iterates rules, returns first non-zero code. +**The 3-argument view path fails open for spender-dependent rules.** `detectTransferRestriction` and +`canTransfer` carry no `spender`, so a rule keyed by spender (e.g. a per-minter mint allowance) cannot +evaluate the operation and must answer "no restriction". The engine aggregates that answer, so these two views +can report a mint as allowed that `transferred(spender, ...)` will revert. Use the 4-argument +`detectTransferRestrictionFrom` / `canTransferFrom` to pre-check an operation that has an operator. See +`H-1` in `doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md`. + ### Storage: EnumerableSet Both rules and bound tokens use `EnumerableSet.AddressSet`: @@ -255,8 +280,11 @@ 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 +- All `internal` functions must be marked `virtual`, so inheriting contracts can override them. +- Use `require(condition, CustomError(...))` for custom errors; avoid direct `revert CustomError(...)`. - In `src/`, avoid `super` calls and prefer explicit parent-contract calls (e.g., `AccessControl.grantRole(...)`) for readability and deterministic inheritance behavior. - Section headers: `/* ============ SECTION ============ */` +- **No emoji in code comments or NatSpec.** Use a plain word marker instead: `WARNING:`, `NOTE:`, `IMPORTANT:`. Emoji render inconsistently across editors, terminals, `forge doc` output and diffs; they are not searchable (`grep WARNING` finds the marker, `grep ⚠️` depends on the shell); and they encode as multi-byte sequences that can be silently mangled by tooling. This applies to `src/`, `test/` and `script/`. Markdown documentation may use emoji freely — the restriction is Solidity comments only. - Run `forge fmt` before committing ## Common Tasks diff --git a/CHANGELOG.md b/CHANGELOG.md index 35ffbb1..315d157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,8 +47,56 @@ forge lint +### v3.0.0-rc5 + +### Changed + +- `ERC3643ComplianceModule._bindToken` / `_unbindToken`: rely on the `EnumerableSet` mutation return value instead of a preceding `contains()` lookup, keeping the `TokenAlreadyBound` / `TokenNotBound` diagnostics (269 gas measured). +- `_bindToken`, `_unbindToken` and `RuleEngineBase._supportsRuleEngineBaseInterface` are now `virtual`, along with the remaining non-`virtual` internals in the mock rules, per the project convention. +- `RuleAddressList.addressIsListedBatch`: `memory` parameter changed to `calldata` (587 gas measured for 10 addresses). +- Deployment now emits `SetMaxRules` with the initial cap, so the event log alone is sufficient to reconstruct `maxRules`. +- `RulesManagementModule`: the rule-cap write and its event moved into a new `internal virtual _setMaxRules(uint256)`, called by `setMaxRules` and by the deployable contracts' constructors. `_maxRules` is now written from a single place, so the invariant "every change to the cap emits `SetMaxRules`" holds structurally rather than by convention, and the non-zero check guards every path including construction. +- `RulesManagementModule`: rule insertion moved into a new `internal virtual _addRule(IRule)`, called by `addRule` and by the `setRules` loop. `AddRule` is now emitted from a single site. The `maxRules` cap is deliberately checked by the callers, since `addRule` checks per insertion while `setRules` checks the whole batch up front. + +### Added + +- Add `ERC3643TokenMock`: a minimal ERC-3643 (T-REX) style token whose compliance interaction mirrors `Token.sol` from the reference implementation, used to test the RuleEngine through the ERC-3643 entry points (`setCompliance` self-binding, `transferred`, `created`, `destroyed`). +- Add `ERC3643TokenIntegration.t.sol` (11 tests), including a regression guard for the H-1 mint pre-check fail-open and one pinning the requirement that `address(0)` be whitelisted for an ERC-3643 token to mint. + +- **Renamed the reference rules in `src/mocks/rules/` with a `Mock` suffix**, so a reader cannot mistake them for the production rules of the same name maintained in [CMTA/Rules](https://github.com/CMTA/Rules): `RuleWhitelist` -> `RuleWhitelistMock`, `RuleConditionalTransferLight` -> `RuleConditionalTransferLightMock`, `RuleMintAllowance` -> `RuleMintAllowanceMock`, `RuleOperationRevert` -> `RuleOperationRevertMock`. Files renamed to match. The abstract bases and invariant-storage contracts they build on are unchanged, as they are not themselves rules. + +### Removed + +- `RuleEngine_ERC3643Compliance_OperationNotSuccessful`: unreachable after the bind/unbind simplification and referenced nowhere else. + +### Documentation + +- Document that the ERC-1404 3-argument `canTransfer` / `detectTransferRestriction` path fails open for spender-dependent rules, and that `canTransferFrom` / `detectTransferRestrictionFrom` must be used to pre-check an operation that has an operator. +- Add the code-quality review in [doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md](./doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md). +- Add integration guides in `doc/technical`: [RuleEngine-with-CMTAT.md](./doc/technical/RuleEngine-with-CMTAT.md) and [RuleEngine-with-ERC3643.md](./doc/technical/RuleEngine-with-ERC3643.md), covering entry points, configuration, warnings, limitations and test coverage for each token standard. +- Add the script review in [doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS_SCRIPT.md](./doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS_SCRIPT.md). +- Add the v3.0.0-rc5 Slither and Aderyn reports with their assessment feedback, each prefixed with a summary table of findings and dispositions. +- Add [doc/security/audits/AUDIT_OVERVIEW.md](./doc/security/audits/AUDIT_OVERVIEW.md) indexing every analysis performed, the static-analysis results per tool, and the substantive findings that were fixed. + +### Fixed + +- `RuleEngineScript.s.sol`: the CMTAT token is now bound to the engine (passed to the constructor). Previously the script produced a deployment in which every transfer, mint and burn reverted with `RuleEngine_ERC3643Compliance_UnauthorizedCaller`, because the token was never bound. +- `RuleEngineScript.s.sol`: `setRuleEngine` is now called through the typed interface instead of a low-level `.call` guarded by a bare `require(success)`. The previous form returned success when `CMTAT_ADDRESS` held no code, silently producing an unconfigured deployment, and discarded the revert reason on failure. +- `RuleEngineScript.s.sol`: the demo whitelist is now seeded with the deployer and `address(0)`, so the resulting deployment can transfer, mint and burn as-is. +- `test/script/RuleEngineScript.t.sol`: asserts the resulting deployment works (engine set, token bound, rule configured, a real mint) instead of only that `run()` does not revert. +- `doc/script/script_surya_*.sh`: fixed the shebang (`#/bin/bash` -> `#!/bin/bash`), the undefined `$dir` loop variable, `mkdir` without `-p` in the report script, and the output-directory guard in the inheritance script; added `set -euo pipefail` and null-delimited `find` iteration to all three. The loop iterates `find .` rather than an absolute path on purpose: `surya mdreport` embeds the path it is given, so an absolute one would write machine-specific paths into the committed reports under `doc/schema/surya/surya_report`. +- `package.json`: the `surya:*` and `uml:*` scripts now write beneath `docOut/` (gitignored) instead of the repository root. +- `doc/script/convert_links_for_pdf.sh`: the default input is now `doc/README.md` (the full documentation) rather than the short root README. + +### Dependencies + +- Update CMTAT submodule to [v3.3.0-rc3](https://github.com/CMTA/CMTAT/releases/tag/v3.3.0-rc3). +- Update OpenZeppelin Contracts and OpenZeppelin Contracts Upgradeable submodules to [v5.7.0](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.7.0). + ### v3.0.0-rc4 - 2026-05-22 +Commit: `66fcf2aafebd1f9d9de8a81dec92b88da071c9b3` + ### Added - Add `RuleMintAllowance` mock rule: admin-controlled per-minter mint allowance with `setMintAllowance(address, uint256)`, enforcement in `transferred(spender, from, to, value)` when `from == address(0)`, and `CODE_MINTER_INSUFFICIENT_ALLOWANCE` (code 81). diff --git a/CLAUDE.md b/CLAUDE.md index 89370db..6e92152 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,14 +2,14 @@ This file helps AI agents (Cursor, Claude Code, etc.) understand and work with this codebase. -AGENTS.md and CLAUDE.md files must always be identical +AGENTS.md and CLAUDE.md files must always be identical — always update both together. ## Project Summary **RuleEngine** is a Solidity smart contract system that enforces transfer restrictions for [CMTAT](https://github.com/CMTA/CMTAT) and [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) tokens. It acts as an external controller that calls pluggable rule contracts on each token transfer, mint, or burn. - **Version:** 3.0.0 (defined in `src/modules/VersionModule.sol`) -- **Solidity:** ^0.8.20 (compiled with 0.8.34) +- **Solidity:** ^0.8.20 (compiled with 0.8.36) - **EVM target:** Prague - **License:** MPL-2.0 @@ -21,13 +21,27 @@ forge test # Run all tests forge test -vvv # Verbose test output forge test --match-contract --match-test # Run specific test forge coverage # Code coverage -forge coverage --no-match-coverage "(script|mocks|test)" --report lcov # Production coverage +forge coverage --no-match-coverage "(mocks|test)" --report lcov # Production coverage (src/ + script/) forge fmt # Format code ``` Dependencies are git submodules. Initialize with `forge install`, update with `forge update`. CMTAT submodule also needs `cd lib/CMTAT && npm install` for its OpenZeppelin deps. +## Agent Workflow + +- **Never create git commits.** Provide commit messages only when they are requested. +- **Always run the full test suite (`forge test`) after any code modification** — including lint-driven or mechanical refactors — before reporting completion. +- **Always update the documentation** to reflect the latest change. There are two READMEs: `README.md` at the root is the short overview (project, architecture, main files, quick start); `doc/README.md` is the full reference (interfaces, Ethereum API, deployment, UML, audits). Update whichever the change affects — often both. +- After each implemented feature or fix, provide a **one-line GitHub commit message** covering all changes since the last commit. + +### When implementing a new rule or feature + +1. Create or update the technical documentation in `doc/technical` +2. Update `README.md` (root overview) and `doc/README.md` (full reference) as applicable +3. Create or update tests, targeting **100% code coverage** — check with `forge coverage --report summary` +4. Update `CHANGELOG.md` + ## Import Remappings | Alias | Path | @@ -132,31 +146,42 @@ function _checkRule(address rule_) internal view virtual override { ### Rule Execution Flow ``` -Token operation → RuleEngine.transferred(spender, from, to, value) ← CMTAT v3.3.0+ primary path +CMTAT only: RuleEngine.transferred(spender, from, to, value) ← transferFrom, mint, burn (spender = _msgSender()) ├── onlyBoundToken modifier (caller must be bound) └── for each rule in _rules: rule.transferred(spender, from, to, value) // reverts if disallowed - RuleEngine.transferred(from, to, value) ← 3-arg fallback (spender == address(0)) +CMTAT + ERC-3643: RuleEngine.transferred(from, to, value) ← standard transfer (spender == address(0)) ├── onlyBoundToken modifier └── for each rule in _rules: rule.transferred(from, to, value) - RuleEngine.created(to, value) ← ERC-3643 mint entry point +ERC-3643 only: RuleEngine.created(to, value) ← ERC-3643 mint entry point ├── onlyBoundToken modifier └── calls _transferred(address(0), to, value) - RuleEngine.destroyed(from, value) ← ERC-3643 burn entry point +ERC-3643 only: RuleEngine.destroyed(from, value) ← ERC-3643 burn entry point ├── onlyBoundToken modifier └── calls _transferred(from, address(0), value) ``` -Since CMTAT v3.3.0, mint (`from == address(0)`) and burn (`to == address(0)`) also go through the 4-argument overload with the operator as `spender`. Rules that check `spender` must skip or adapt that check for mint/burn to avoid blocking those operations unintentionally. +**CMTAT and ERC-3643 use disjoint entry points.** The 4-argument `transferred` is declared by CMTAT's `IRuleEngine` (`lib/CMTAT/contracts/interfaces/engine/IRuleEngine.sol`), so an ERC-3643 token never reaches it. Conversely `created` / `destroyed` are declared by `IERC3643Compliance` and CMTAT never calls them — CMTAT routes mint and burn through the 4-argument `transferred` instead. Only the 3-argument `transferred` is shared by both. + +CMTAT selects the overload in `ValidationModuleRuleEngine._callRuleEngineTransferred`, branching on `spender != address(0)`. A standard `transfer` has no spender (`CMTATBaseCommon.transfer` passes `address(0)` internally), so the `else` branch calls the **3-argument** `transferred(from, to, value)` — the zero address is a branch condition only and is never forwarded to the engine. `transferFrom`, `mint` and `burn` carry `_msgSender()` as spender and take the **4-argument** overload. Neither is a fallback: which one is called depends purely on the operation. + +Since CMTAT v3.3.0, mint (`from == address(0)`) and burn (`to == address(0)`) therefore also reach the 4-argument overload with the operator as `spender`. Rules that check `spender` must skip or adapt that check for mint/burn to avoid blocking those operations unintentionally. `created` and `destroyed` use the 3-argument `_transferred` path (no spender), consistent with the ERC-3643 spec which does not carry a spender for mint/burn. View path: `detectTransferRestriction()` iterates rules, returns first non-zero code. +**The 3-argument view path fails open for spender-dependent rules.** `detectTransferRestriction` and +`canTransfer` carry no `spender`, so a rule keyed by spender (e.g. a per-minter mint allowance) cannot +evaluate the operation and must answer "no restriction". The engine aggregates that answer, so these two views +can report a mint as allowed that `transferred(spender, ...)` will revert. Use the 4-argument +`detectTransferRestrictionFrom` / `canTransferFrom` to pre-check an operation that has an operator. See +`H-1` in `doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md`. + ### Storage: EnumerableSet Both rules and bound tokens use `EnumerableSet.AddressSet`: @@ -255,8 +280,11 @@ 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 +- All `internal` functions must be marked `virtual`, so inheriting contracts can override them. +- Use `require(condition, CustomError(...))` for custom errors; avoid direct `revert CustomError(...)`. - In `src/`, avoid `super` calls and prefer explicit parent-contract calls (e.g., `AccessControl.grantRole(...)`) for readability and deterministic inheritance behavior. - Section headers: `/* ============ SECTION ============ */` +- **No emoji in code comments or NatSpec.** Use a plain word marker instead: `WARNING:`, `NOTE:`, `IMPORTANT:`. Emoji render inconsistently across editors, terminals, `forge doc` output and diffs; they are not searchable (`grep WARNING` finds the marker, `grep ⚠️` depends on the shell); and they encode as multi-byte sequences that can be silently mangled by tooling. This applies to `src/`, `test/` and `script/`. Markdown documentation may use emoji freely — the restriction is Solidity comments only. - Run `forge fmt` before committing ## Common Tasks diff --git a/README.md b/README.md index f77180c..311b422 100644 --- a/README.md +++ b/README.md @@ -1,1706 +1,123 @@ -> This project has not undergone an audit and is provided as-is without any warranties. - # RuleEngine -This repository includes the RuleEngine contracts for [CMTAT](https://github.com/CMTA/CMTAT) and [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) tokens. - -The RuleEngine is an external contract used to apply transfer restrictions to another contract, such as CMTAT and ERC-3643 tokens. Acting as a controller, it can call different contract rules and apply these rules on each transfer. - -[TOC] - -## Contract Variants - -Three deployable contracts are available: - -| Contract | Access Control | Interface | Use Case | -|----------|---------------|-----------|----------| -| `RuleEngine` | Role-Based (AccessControlEnumerable) | RBAC roles | Multi-operator environments with granular permissions | -| `RuleEngineOwnable` | ERC-173 Ownership | `Ownable` | Single-owner setups, simpler administration | -| `RuleEngineOwnable2Step` | ERC-173 Ownership (two-step transfer) | `Ownable2Step` | Single-owner setups with safer ownership handover | - -ERC-3643 compliance specification indicates the use of ERC-173. - -> The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of setting the Compliance parameters and binding the Compliance to a Token contract. - -All deployable contracts share the same core functionality (`RuleEngineBase`, directly or through `RuleEngineOwnableShared`) and support: - -- ERC-1404 transfer restrictions -- ERC-3643 compliance interface -- ERC-2771 meta-transactions (gasless) -- Multiple token bindings - -> **Warning (shared engine across multiple tokens):** A "multi-tenant" setup here means one RuleEngine instance is shared by several token contracts (all bound through `bindToken`). In this setup, tokens must be equally trusted and governed together. ERC-3643 callbacks (`transferred`, `created`, `destroyed`) do not pass the token address to rules, so stateful/accounting rules are not safe for mutually untrusted tokens sharing the same engine. - -## Motivation - -- Why use a dedicated contract with rules instead of implementing it directly in CMTAT or [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) tokens? - -There are several reasons to do this: +RuleEngine applies transfer restrictions to [CMTAT](https://github.com/CMTA/CMTAT) and [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) tokens. -- Flexibility: These different features are not standard and common to all tokens. From an implementation perspective, using a rule engine with custom rules allows for each issuer or contract user to decide which rules to apply. +It is an *external controller*: the token calls the engine on every transfer, mint and burn, and the engine forwards the call to a configurable list of pluggable rule contracts. This keeps compliance logic out of the token, lets each issuer compose the rules they need, and avoids growing the token's already large bytecode. -- Code efficiency: The CMTAT token (and generally also all ERC-3643 tokens) is currently "heavy," meaning its contract code size is close to the maximum limit. This makes it challenging to add new features directly inside the token contract. +- **Version:** 3.0.0 +- **Solidity:** ^0.8.20 (compiled with 0.8.36) +- **EVM target:** Prague +- **License:** MPL-2.0 -- Reusability: +**Full documentation: [doc/README.md](./doc/README.md)** — interfaces, Ethereum API, deployment, UML and call graphs, audits, and toolchain usage. - - The RuleEngine can be used inside other contracts besides CMTAT. For instance, the RuleEngine has been used in [our contract to distribute dividends](https://www.taurushq.com/blog/equity-tokenization-how-to-pay-dividend-on-chain-using-cmtat/). +> This project has not undergone an audit and is provided as-is without any warranties. - - A same deployed `RuleEngine` can also be used with several different tokens if the rules allow it, which is the case for all read-only rules. +## Contract Variants -Why use this `RuleEngine` contract instead of setting the `rule` directly in the token contract? +Three deployable contracts share the same core logic and differ only in access control: -- Using a RuleEngine allows to call several different rules. For example, a blacklist rule to allow the issuer to manage its own list of blacklisted addresses and a sanctionlist rule to use the [Chainalysis oracle for sanctions screening](https://go.chainalysis.com/chainalysis-oracle-docs.html) to forbid transfers from addresses listed in sanctions designations published by organizations such as the US, EU, or UN. +| Contract | Access Control | Use Case | +|----------|---------------|----------| +| `RuleEngine` | Role-based (`AccessControlEnumerable`) | Multi-operator environments with granular permissions | +| `RuleEngineOwnable` | ERC-173 `Ownable` | Single-owner setups, simpler administration | +| `RuleEngineOwnable2Step` | ERC-173 `Ownable2Step` | Single-owner setups with safer ownership handover | -When may the use of `RuleEngine` not be appropriate? +All three support ERC-1404 transfer restrictions, the ERC-3643 compliance interface, ERC-2771 meta-transactions (gasless), and multiple token bindings. -- If you plan to call only one rule (e.g a whitelist rule), it could make sense to directly set the rule in the token contract instead of using a RuleEngine. This will simplify configuration and reduce runtime gas costs. +> **Warning (shared engine across multiple tokens):** one RuleEngine instance can be bound to several tokens, but they must be equally trusted and governed together. ERC-3643 callbacks do not pass the token address to rules, so stateful rules are not safe for mutually untrusted tokens sharing an engine. ## How it works -This diagram illustrates how a transfer with a CMTAT or ERC-3643 token with a RuleEngine works: - -![RuleEngine.drawio](./doc/schema/RuleEngine.drawio.png) - - - -1. The token holders initiate a transfer transaction on the token contract. -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. 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. - -### How to set it - -#### Compatibility - -| RuleEngine version | Compatible Versions | -| ------------------------------------------------------------ | ------------------------------------------------------------ | -| **v3.0.0-rc4** | CMTAT ≥ v3.0.0
CMTAT target version: [v3.3.0-rc1](https://github.com/CMTA/CMTAT/releases/tag/v3.3.0-rc1) | -| **v3.0.0-rc3** | CMTAT ≥ v3.0.0
CMTAT target version: [v3.3.0](https://github.com/CMTA/CMTAT/releases/tag/v3.3.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) | -| **[v1.0.2.1](https://github.com/CMTA/RuleEngine/releases/tag/v1.0.2.1)** | CMTAT v2.3.0 (audited) | - -#### CMTAT v3.0.0 - -CMTAT provides the following function to set a RuleEngine inside a CMTAT token: - -```solidity -setRuleEngine(IRuleEngine ruleEngine_) -``` - -This function is defined in the extension module `ValidationModuleRuleEngine` - -#### ERC-3643 token - -[ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) defined the following function in the standard interface to set a compliance contract - -```solidity -setCompliance(address _compliance) -``` - -### Making `setCompliance` work with RuleEngine - -RuleEngine supports the ERC-3643/T-REX pattern where the token contract binds and unbinds itself when `setCompliance` is called. - -In other words, a token can call: - -- `bindToken(address(this))` -- `unbindToken(address(this))` - -To keep this feature secure, self-bind/self-unbind is gated: - -- A token can call `bindToken(address(this))` and `unbindToken(address(this))` only if it was explicitly approved first. -- Approval is set by governance/compliance admin using: - - `setTokenSelfBindingApproval(address token, bool approved)` -- Approval status can be checked with: - - `isTokenSelfBindingApproved(address token)` - -This preserves compatibility with ERC-3643 tokens that do: - -```solidity -if (address(_tokenCompliance) != address(0)) { - _tokenCompliance.unbindToken(address(this)); -} -_tokenCompliance = IModularCompliance(_compliance); -_tokenCompliance.bindToken(address(this)); -``` - -while preventing arbitrary third-party contracts from self-binding. - -Recommended operational sequence: - -1. On the target RuleEngine, grant self-binding approval for the token. -2. Call token `setCompliance(newRuleEngine)`. -3. (Optional) Revoke self-binding approval after migration if no longer needed. - - - -## How to include it - -While the RuleEngine has been designed for CMTAT and ERC-3643 tokens, it can be used with other contracts to apply transfer restrictions. - -For that, the only thing to do is to import in your contract the interface `IRuleEngine`(CMTAT) or `IERC3643Compliance` (ERC-3643), which declares the corresponding functions to call by the token contract. This interface can be found [here](https://github.com/CMTA/CMTAT/blob/23a1e59f913d079d0c09d32fafbd95ab2d426093/contracts/interfaces/engine/IRuleEngine.sol). -If you need non-standard helper functions (batch bind/unbind, self-binding approval APIs, multi-token getter), use `IERC3643ComplianceExtended`. - -### Like CMTAT - -Before each ERC-20 transfer, mint, or burn, CMTAT calls the RuleEngine through the internal function `_checkTransferred`, which dispatches to one of two `transferred` overloads depending on whether a non-zero spender is present. - -```solidity -// Called when spender == address(0) -function transferred(address from, address to, uint256 value) - -// Called when spender != address(0) -function transferred(address spender, address from, address to, uint256 value) -``` - -#### CMTAT v3.3.0 — mint and burn use the spender path - -Since CMTAT v3.3.0, **mint and burn operations also go through the 4-argument overload**, with the operator (minter or burner) passed as `spender`: - -| Operation | `spender` | `from` | `to` | -|-----------|-----------|--------|------| -| `transfer` / `transferFrom` | caller / approved spender | token holder | recipient | -| `mint` | minter (`_msgSender()`) | `address(0)` | recipient | -| `burn` | burner (`_msgSender()`) | token holder | `address(0)` | - -The 3-argument overload is only called when `spender == address(0)`, which does not occur in normal CMTAT v3.3.0 flows. - -> **Rule authoring note:** Rules that check the `spender` argument in `transferred(spender, from, to, value)` must explicitly handle the mint (`from == address(0)`) and burn (`to == address(0)`) cases. A spender check that is intended only for `transferFrom` will also fire for mints and burns unless the rule skips it when `from` or `to` is the zero address. See `RuleWhitelist` and `RuleSpenderWhitelist` in the Rules repository for reference implementations. - -For example, CMTAT defines the interaction with the RuleEngine inside a specific module, [ValidationModuleRuleEngine](https://github.com/CMTA/CMTAT/blob/master/contracts/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol) and [CMTATBaseRuleEngine](https://github.com/CMTA/CMTAT/blob/master/contracts/modules/1_CMTATBaseRuleEngine.sol). - -- ValidationModuleRuleEngine - -![transferred](./doc/other/CMTAT/transferred.png) - -- CMTATBaseRuleEngine - -![checkTransferred](./doc/other/CMTAT/checkTransferred.png) - -This function `_transferred` is called before each transfer/burn/mint through the internal function `_checkTransferred`. - -### Like ERC-3643 - -The ERC-3643 defines several functions used as entrypoints for an ERC-3643 token. - -As for CMTAT, the main entrypoint is `transferred` which must be called for each ERC-20 transfer. - -Contrary to CMTAT, ERC-3643 does not apply restriction on the spender address (`transferFrom`). - -They are the following: - -```solidity -// read-only function -function canTransfer(address from, address to, uint256 value) external view returns (bool); -// ERC-20 transfer -function transferred(address from, address to, uint256 value) external; -// mint -function created(address to, uint256 value) external; -// burn -function destroyed(address from, uint256 value) external; -``` - -## Interface - -### CMTAT - -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: `IERC1404`, `IERC1404Extend`, `IERC7551Compliance`, `IERC3643ComplianceContract` - -```solidity -// IRuleEngine -function transferred(address spender, address from, address to, uint256 value) -external; - -// IERC-1404 -function detectTransferRestriction(address from,address to,uint256 value) -external view returns (uint8); - -function messageForTransferRestriction(uint8 restrictionCode) -external view returns (string memory); - -// IERC-1404Extend -enum REJECTED_CODE_BASE { - TRANSFER_OK, - TRANSFER_REJECTED_DEACTIVATED, - TRANSFER_REJECTED_PAUSED, - TRANSFER_REJECTED_FROM_FROZEN, - TRANSFER_REJECTED_TO_FROZEN, - TRANSFER_REJECTED_SPENDER_FROZEN, - TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE - } - -function detectTransferRestrictionFrom(address spender,address from,address to,uint256 value) -external view returns (uint8); - - -// IERC7551Compliance -function canTransferFrom(address spender,address from,address to,uint256 value) -external view returns (bool); - - -// IER3643ComplianceRead -function canTransfer(address from,address to,uint256 value) -external view returns (bool isValid); - -// IERC3643IComplianceContract -function transferred(address from, address to, uint256 value) -external; -``` - -> Note: `IERC7551Compliance` comes from `draft-IERC7551` (not final) and, in this project, is used as a subset compliance interface focused on `canTransferFrom`. - - - -### ERC-3643 - -The [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) compliance interface is defined in [IERC3643Compliance.sol](src/interfaces/IERC3643Compliance.sol). -Non-standard helper functions are defined in [IERC3643ComplianceExtended.sol](src/interfaces/IERC3643ComplianceExtended.sol). - -The RuleEngine modules are split as follows: -- Base ERC-3643 surface: [ERC3643ComplianceModule.sol](src/modules/ERC3643ComplianceModule.sol) -- Non-standard extensions: [ERC3643ComplianceExtendedModule.sol](src/modules/ERC3643ComplianceExtendedModule.sol) - -![ERC3643ComplianceModuleUML](./doc/schema/vscode-uml/ERC3643ComplianceModuleUML.png) - -## Technical - -### Dependencies - -The toolchain includes the following components, where the versions are the latest ones that we tested: - -- Foundry (forge-std) [v1.14.0](https://github.com/foundry-rs/forge-std/releases/tag/v1.14.0) -- Solidity [0.8.34](https://docs.soliditylang.org/en/v0.8.34/) -- OpenZeppelin Contracts (submodule) [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.6.1) -- CMTAT [v3.3.0](https://github.com/CMTA/CMTAT/releases/tag/v3.3.0) - -### Access Control - -Two access control mechanisms are available depending on which contract you deploy: - -#### RuleEngine (RBAC - AccessControlEnumerable) - -The `RuleEngine` contract uses Role-Based Access Control (RBAC) via OpenZeppelin's `AccessControlEnumerable`. - -Each module defines the roles useful to restrict its functions. The contract overrides the OpenZeppelin function `hasRole` to give by default all the roles to the `admin`. -`RulesManagementModule` itself is access-control agnostic; RBAC is wired at the concrete `RuleEngine` level. -Note: this `hasRole` override does not add the admin address to each role's enumerable member set. As a result, `getRoleMember` / `getRoleMemberCount` for a specific role do not include the admin unless that role is explicitly granted. - -See also [docs.openzeppelin.com - AccessControlEnumerable](https://docs.openzeppelin.com/contracts/5.x/api/access#AccessControlEnumerable) - -#### RuleEngineOwnable (ERC-173 Ownership) - -The `RuleEngineOwnable` contract uses [ERC-173](https://eips.ethereum.org/EIPS/eip-173) ownership via OpenZeppelin's `Ownable`. - -All protected functions require the caller to be the contract owner. The owner can: -- Transfer ownership to another address via `transferOwnership(address)` -- Renounce ownership via `renounceOwnership()` (makes the contract ownerless) - -This is a simpler access control model suitable for single-owner deployments. - -See also [docs.openzeppelin.com - Ownable](https://docs.openzeppelin.com/contracts/5.x/api/access#Ownable) - -#### RuleEngineOwnable2Step (ERC-173 Ownership, two-step transfer) - -The `RuleEngineOwnable2Step` contract uses OpenZeppelin's `Ownable2Step`, which keeps the same owner-only protections and adds safer ownership handover with `transferOwnership(address)` + `acceptOwnership()`. - -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` | | | | -| `IRuleEngine` | `0x20c49ce7` | | | | -| `IERC1404` | `0xab84a5c8` | | | | -| `IERC1404Extend` | `0x78a8de7d` | | | | -| `IERC3643Compliance` | `0x3144991c` | | | | -| `IERC7551Compliance` (subset) | `0x7157797f` | | | | -| `IERC173` | `0x7f5828d0` | | | | -| `Ownable2Step` specific (`pendingOwner()`, `acceptOwnership()`) | `0x9ab669ef` | | | | -| `IAccessControl` | `0x7965db0b` | | | | -| `IAccessControlEnumerable` | `0x5a05180f` | | | | - -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. - -The default admin is the address put in argument (`admin`) inside the constructor. - -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 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 | -| ----------------------- | -------------------------------- | ------------------------------------------------------------ | -| DEFAULT_ADMIN_ROLE | OpenZeppelin
AccessControl | 0x0000000000000000000000000000000000000000000000000000000000000000 | -| **Modules** | | | -| COMPLIANCE_MANAGER_ROLE | ERC3643ComplianceModule | 0xe5c50d0927e06141e032cb9a67e1d7092dc85c0b0825191f7e1cede600028568 | -| RULES_MANAGEMENT_ROLE | RulesManagementModuleInvariantStorage | 0xea5f4eb72290e50c32abd6c23e45de3d8300b3286e1cbc2e293114b92e034e5e | - - - -#### Schema (RuleEngine) - -Here is a schema of the Access Control for `RuleEngine`. -![alt text](./doc/security/accessControl/access-control-RuleEngine.png) +![RuleEngine overview](./doc/schema/plantuml/ruleengine-overview.png) -#### Role by modules (RuleEngine) +The token calls the engine on every transfer, mint and burn. The engine checks the caller is a bound token, then runs each configured rule in order. A rule that forbids the transfer reverts, and the whole transaction reverts with it — remaining rules are never reached. -Here is a summary table for each restricted function defined in a module. -For function signatures, struct arguments are represented with their corresponding native type. +**CMTAT and ERC-3643 use disjoint entry points.** The 4-argument overload is declared by CMTAT's `IRuleEngine`, so an ERC-3643 token never reaches it; `created` / `destroyed` belong to `IERC3643Compliance`, and CMTAT never calls them. -> Note: For `RuleEngineOwnable` and `RuleEngineOwnable2Step`, replace the role requirement with `onlyOwner` for all protected functions. +| Token | Transfer | `transferFrom` | Mint | Burn | +|-------|----------|----------------|------|------| +| CMTAT | `transferred(from, to, value)` | `transferred(spender, …)` | `transferred(spender, …)` | `transferred(spender, …)` | +| ERC-3643 | `transferred(from, to, value)` | `transferred(from, to, value)` | `created(to, value)` | `destroyed(from, value)` | -| | Function signature | Visibility [public/external] | Input variables (Function arguments) | Output variables
(return value) | Role Required | -| -------------------- | ------------------ | ---------------------------- | ------------------------------------ | ------------------------------------ | ------------- | -| **Modules** | | | | | | -| RulesManagementModule | | | | | | -| | `setRules(address[] rules_)` | public | `IRule[] rules_` | - | RULES_MANAGEMENT_ROLE | -| | `clearRules()` | public | - |-|RULES_MANAGEMENT_ROLE| -| | `addRule(address rule_)` | public | `IRule rule_` |-|RULES_MANAGEMENT_ROLE| -| | `removeRule(address rule_)` | public | `IRule rule_` |-|RULES_MANAGEMENT_ROLE| -| ERC3643ComplianceModule | | | | | | -| | `bindToken(address token)` | public | `address token` | - | COMPLIANCE_MANAGER_ROLE or approved token self-call | -| | `unbindToken(address token)` | public | `address token` | - | COMPLIANCE_MANAGER_ROLE or approved token self-call | -| ERC3643ComplianceExtendedModule | | | | | | -| | `bindTokens(address[] tokens)` | public | `address[] tokens` | - | COMPLIANCE_MANAGER_ROLE | -| | `unbindTokens(address[] tokens)` | public | `address[] tokens` | - | COMPLIANCE_MANAGER_ROLE | -| | `setTokenSelfBindingApproval(address token,bool approved)` | public | `address token,bool approved` | - | COMPLIANCE_MANAGER_ROLE | -| | `setTokenSelfBindingApprovalBatch(address[] tokens,bool approved)` | public | `address[] tokens,bool approved` | - | COMPLIANCE_MANAGER_ROLE | -| RuleEngineBase | | | | | | -| | `transferred(address from,address to,uint256 value)` | public | `address from,address to, uint256 value` | - | onlyBoundToken (modifier) | -| | `transferred(address spender,address from,address to,uint256 value)` | public | `address spender,address from,address to, uint256 value` | - | onlyBoundToken (modifier) | +CMTAT picks the overload according to whether the operation has a spender. A plain `transfer` has none, so CMTAT calls the 3-argument `transferred(from, to, value)` — the engine is never called with a zero spender. `transferFrom`, `mint` and `burn` do have one (`_msgSender()`), so those call the 4-argument `transferred(spender, from, to, value)`. Whichever entry point is used, the engine then runs the same rule loop. +The view path, `detectTransferRestriction()`, iterates the same rules and returns the first non-zero ERC-1404 restriction code instead of reverting. +Sequence diagrams for each token type: [CMTAT](./doc/schema/plantuml/ruleengine-flow-cmtat.png) — [ERC-3643](./doc/schema/plantuml/ruleengine-flow-erc3643.png) (sources in [doc/schema/plantuml](./doc/schema/plantuml/)). -### UML +## Architecture -Here is the UML of the main contracts: - -#### RuleEngine -![RuleEngineUML](./doc/schema/vscode-uml/RuleEngineUML.png) - -#### RuleEngineOwnable - -![RuleEngineOwnableUML](./doc/schema/vscode-uml/RuleEngineOwnableUML.png) - -`RuleEngineOwnable` shares the same base functionality as `RuleEngine` but uses ERC-173 ownership instead of RBAC. - -``` -RuleEngineOwnable -├── ERC2771ModuleStandalone (gasless support) -├── RuleEngineBase (core functionality) -│ ├── VersionModule -│ ├── RulesManagementModule -│ ├── ERC3643ComplianceModule (core ERC-3643) -│ ├── ERC3643ComplianceExtendedModule (project extensions) -│ └── IRuleEngineERC1404 -└── Ownable (ERC-173 access control) -``` - -**Key differences from RuleEngine:** -- Constructor takes `owner_` instead of `admin` -- All protected functions use `onlyOwner` modifier -- Supports `transferOwnership()` and `renounceOwnership()` -- Implements ERC-173 interface (`supportsInterface(0x7f5828d0)` returns `true`) - -#### RuleEngineOwnable2Step - -![RuleEngineOwnable2StepUML](./doc/schema/vscode-uml/RuleEngineOwnable2StepUML.png) - -`RuleEngineOwnable2Step` shares the same base functionality as `RuleEngineOwnable` but uses OpenZeppelin's `Ownable2Step` for safer ownership handover. - -``` -RuleEngineOwnable2Step -├── ERC2771ModuleStandalone (gasless support) -├── RuleEngineOwnableShared (shared ownable deployment logic) -│ └── RuleEngineBase -│ ├── VersionModule -│ ├── RulesManagementModule -│ ├── ERC3643ComplianceModule (core ERC-3643) -│ ├── ERC3643ComplianceExtendedModule (project extensions) -│ └── IRuleEngineERC1404 -└── Ownable2Step (ERC-173 access control with pending owner) -``` - -**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 (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()` - - - - - -### Graph - -Here is the surya graph of the main contract: - -![surya_graph_RuleEngine](./doc/schema/surya/surya_graph/surya_graph_RuleEngine.sol.png) - -## Functionality - -Several functionalities are not implemented because it makes more sense to directly implement them in the token smart contract - -The RuleEngine can be removed from the main token contract by calling these dedicated functions - -- CMTAT v3.0.0: `setRuleEngine(address ruleEngine)` -- ERC-3643 token: `setCompliance(address _compliance)` - -### Available Rules - -Rules are maintained in a dedicated repository: [github.com/CMTA/Rules](https://github.com/CMTA/Rules). - -Rules can be used in two ways: - -- Directly on CMTAT (single-rule setup, no RuleEngine orchestration). -- Through this RuleEngine (multi-rule orchestration with sequential execution). - -Rule families: - -| Family | Behavior | Examples | -| --- | --- | --- | -| Validation rules (read-only) | Evaluate transfer eligibility without mutating rule state | `RuleWhitelist`, `RuleBlacklist`, `RuleSanctionList`, `RuleIdentityRegistry`, `RuleSpenderWhitelist`, `RuleERC2980`, `RuleMaxTotalSupply` | -| Operation rules (read-write) | Evaluate transfer eligibility and can update rule-specific state on transfer | `RuleConditionalTransferLight` | - -Additional integration notes: - -- For RuleEngine integration, a rule must implement `IRule` (including ERC-165 support for the Rule interface ID). -- RuleEngine executes configured rules in order and reverts on the first failing rule in state-changing paths. -- Restriction codes should remain unique across the composed rule set. Keep CMTAT-reserved ranges free and use dedicated code ranges per rule -- For the latest list of production rules, audits, and status, use the Rules repository as the source of truth. - -#### Rules details - -Here is a summary tab of available rules, see [github.com/CMTA/Rules](https://github.com/CMTA/Rules) - -| Rule | Type
[read-only / read-write] | Description | -| ------------------------------------------------------------ | ----------------------------------- | ------------------------------------------------------------ | -| RuleWhitelist | Read-only | This rule can be used to restrict transfers from/to only addresses inside a whitelist. | -| RuleWhitelistWrapper | Read-Only | This rule can be used to restrict transfers from/to only addresses inside a group of whitelist rules managed by different operators. | -| RuleBlacklist | Read-Only | This rule can be used to forbid transfer from/to addresses in the blacklist | -| RuleSanctionList | Read-Only | The purpose of this contract is to use the oracle contract from [Chainalysis](https://go.chainalysis.com/chainalysis-oracle-docs.html) to forbid transfer from/to an address included in a sanctions designation (US, EU, or UN). | -| RuleMaxTotalSupply | Read-Only | This rule limits minting so that the total supply never exceeds a configured maximum. | -| RuleIdentityRegistry | Read-Only | This rule checks the ERC-3643 Identity Registry for transfer participants when configured. | -| RuleSpenderWhitelist | Read-Only | This rule blocks `transferFrom` when the spender is not in the whitelist. Direct transfers are always allowed. | -| RuleERC2980 | Read-Only | ERC-2980 Swiss Compliant rule combining a whitelist (recipient-only) and a frozenlist (blocks sender, recipient, and spender for `transferFrom`). Frozenlist takes priority over whitelist. | -| RuleConditionalTransferLight | Read-Write | This rule requires that transfers have to be approved by an operator before being executed. Each approval is consumed once and the same transfer can be approved multiple times. | -| [RuleConditionalTransfer](https://github.com/CMTA/RuleConditionalTransfer) (external) | Read-Write | Full-featured approval-based transfer rule implementing Swiss law *Vinkulierung*. Supports automatic approval after three months, automatic transfer execution, and a conditional whitelist for address pairs that bypass approval. Maintained in a separate repository. | -| [RuleSelf](https://github.com/rya-sge/ruleself) (community) | — | Use [Self](https://self.xyz), a zero-knowledge identity solution to determine which is allowed to interact with the token.
Community-maintained rule project. Not developed or maintained by CMTA. | - -### Gasless support (ERC-2771) - -![surya_inheritance_ERC2771ModuleStandalone.sol](./doc/schema/surya/surya_inheritance/surya_inheritance_ERC2771ModuleStandalone.sol.png) - -The RuleEngine supports client-side gasless transactions using the standard [ERC-2771](https://eips.ethereum.org/EIPS/eip-2771). - -The contract uses the OpenZeppelin contract `ERC2771ContextUpgradeable`, which allows a contract to get the original client with `_msgSender()` instead of the feepayer given by `msg.sender`. - -At deployment, the parameter `forwarder` inside the RuleEngine contract constructor has to be set with the defined address of the forwarder. - -After deployment, the forwarder is immutable and can not be changed. - -References: - -- [OpenZeppelin Meta Transactions](https://docs.openzeppelin.com/contracts/5.x/api/metatx) - -- OpenGSN has deployed several forwarders, see their [documentation](https://docs.opengsn.org/contracts/#receiving-a-relayed-call) for examples. - -### Upgradeable - -A proxy architecture (upgradeable) increases the code complexity as well as the runtime gas cost for each transaction. This is why the RuleEngine is not upgradeable. - -Moreover, in a proxy architecture, each new implementation must be compatible (storage) with the precedent implementation, which can reduce the ability to improve the code. - -In case you use the same RuleEngine for several different tokens, unfortunately, you will have to update the address of the RuleEngine set in each token contract separately. - -### Urgency mechanism - -#### Pause - -There are no functionalities to put the RuleEngine in pause . - -The RuleEngine can be removed from the main token contract by calling the dedicated functions to manage the RuleEngine - -#### Kill / Deactivate the contracts - -There are no functionalities to kill/deactivate the contracts. - -Similar to the pause functionality, the RuleEngine can be directly removed from the main token contract. - -## Ethereum API - -### Contract Constructors - -#### RuleEngine Constructor - -```solidity -constructor( - address admin, - address forwarderIrrevocable, - address tokenContract -) -``` - -| Parameter | Type | Description | -|-----------|------|-------------| -| admin | address | Address granted DEFAULT_ADMIN_ROLE (has all roles) | -| forwarderIrrevocable | address | ERC-2771 trusted forwarder address (can be zero) | -| tokenContract | address | Token to bind at deployment (can be zero) | - -#### RuleEngineOwnable Constructor - -```solidity -constructor( - address owner_, - address forwarderIrrevocable, - address tokenContract -) ``` +RuleEngineBase (abstract) — core logic, shared by all variants +├── VersionModule — version() +├── RulesManagementModule — add/remove/set/clear rules, maxRules cap +├── ERC3643ComplianceExtendedModule +│ └── ERC3643ComplianceModule — bind/unbind tokens, compliance hooks +└── IRuleEngineERC1404 — CMTAT interface -| Parameter | Type | Description | -|-----------|------|-------------| -| owner_ | address | Address set as contract owner (ERC-173) | -| forwarderIrrevocable | address | ERC-2771 trusted forwarder address (can be zero) | -| tokenContract | address | Token to bind at deployment (can be zero) | - -### RuleEngineBase - -![RuleEngineBaseUML](./doc/schema/vscode-uml/RuleEngineBaseUML.png) - -#### Contracts Description Table - - -| Contract | Type | Bases | | | -| :----------------: | :---------------------------: | :----------------------------------------------------------: | :------------: | :------------: | -| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | -| | | | | | -| **RuleEngineBase** | Implementation | VersionModule, RulesManagementModule, ERC3643ComplianceExtendedModule, RuleEngineInvariantStorage, IRuleEngine | | | -| └ | transferred | Public ❗️ | 🛑 | onlyBoundToken | -| └ | transferred | Public ❗️ | 🛑 | onlyBoundToken | -| └ | created | Public ❗️ | 🛑 | onlyBoundToken | -| └ | destroyed | Public ❗️ | 🛑 | onlyBoundToken | -| └ | detectTransferRestriction | Public ❗️ | | NO❗️ | -| └ | detectTransferRestrictionFrom | Public ❗️ | | NO❗️ | -| └ | canTransfer | Public ❗️ | | NO❗️ | -| └ | canTransferFrom | Public ❗️ | | NO❗️ | -| └ | messageForTransferRestriction | Public ❗️ | | NO❗️ | -| └ | hasRole | Public ❗️ | | NO❗️ | - - -##### Legend - -| Symbol | Meaning | -| :----: | ------------------------- | -| 🛑 | Function can modify state | -| 💵 | Function is payable | - -#### IRuleEngine - -![IRuleEngineUML](./doc/schema/vscode-uml/IRuleEngineUML.png) - -##### transferred(address spender, address from, address to, uint256 value) - -```solidity -function transferred(address spender,address from,address to,uint256 value) -public virtual override(IRuleEngine) -onlyBoundToken +RuleEngine = RuleEngineBase + AccessControl + ERC2771ModuleStandalone +RuleEngineOwnable = RuleEngineOwnableShared + Ownable + ERC2771ModuleStandalone +RuleEngineOwnable2Step = RuleEngineOwnableShared + Ownable2Step + ERC2771ModuleStandalone ``` -Function called whenever tokens are transferred from one wallet to another. - -Must revert if the transfer is invalid. - Same name as ERC-3643 but with an additional `spender` parameter. - This function can be used to update state variables of the RuleEngine contract. - Can only be called by the token contract bound to the RuleEngine. - -**Input Parameters:** - -| Name | Type | Description | -| ------- | ------- | ---------------------------------------------- | -| spender | address | The spender address initiating the transfer. | -| from | address | The token holder address. | -| to | address | The receiver address. | -| value | uint256 | The amount of tokens involved in the transfer. | - -#### IERC7551Compliance - -![IERC7551ComplianceUML](./doc/schema/vscode-uml/IERC7551ComplianceUML.png) - -> Note: ERC-7551 is draft (not final). The `IERC7551Compliance` interface used here is a subset interface exposing the compliance check `canTransferFrom`. - -##### canTransferFrom(address spender, address from, address to, uint256 value) -> bool - -Checks if `spender` can transfer `value` tokens from `from` to `to` under compliance rules. - -Does not check balances or access rights (Access Control). - -**Input Parameters:** - -| Name | Type | Description | -| ------- | ------- | ------------------------------------ | -| spender | address | The address performing the transfer. | -| from | address | The source address. | -| to | address | The destination address. | -| value | uint256 | The number of tokens to transfer. | - - - -**Return Values:** - -| Type | Description | -| ---- | ------------------------------------------ | -| bool | True if the transfer complies with policy. | - -#### IERC3643ComplianceRead - -![IERC3643ComplianceReadUML](./doc/schema/vscode-uml/IERC3643ComplianceReadUML.png) - ------- - -##### canTransfer(address from, address to, uint256 value) -> bool +Modules declare access control as **virtual internal hooks** (`_onlyRulesManager`, `_onlyComplianceManager`, `_onlyRulesLimitManager`); each deployable contract overrides them with either RBAC roles or `onlyOwner`. Rules and bound tokens are stored in OpenZeppelin `EnumerableSet.AddressSet` for O(1) add/remove/contains plus iteration. -Returns true if the transfer is valid, and false otherwise. +## Repository layout -Does not check balances or access rights (Access Control). - -**Input Parameters:** - -| Name | Type | Description | -| ----- | ------- | --------------------------------- | -| from | address | The source address. | -| to | address | The destination address. | -| value | uint256 | The number of tokens to transfer. | - - - -**Return Values:** - -| Type | Description | -| ---- | ----------------------------------------------- | -| bool | True if the transfer is valid, false otherwise. | - -#### IERC3643IComplianceContract - -![IERC3643IComplianceContractUML](./doc/schema/vscode-uml/IERC3643IComplianceContractUML.png) - ------- - -##### transferred(address from, address to, uint256 value) - -```solidity -function transferred(address from,address to,uint256 value) -public virtual override(IERC3643IComplianceContract) -onlyBoundToken ``` +src/ +├── RuleEngineBase.sol # abstract core logic (not deployable) +├── RuleEngineOwnableShared.sol # shared logic for the two ownable variants +├── deployment/ # the three deployable contracts +│ ├── RuleEngine.sol +│ ├── RuleEngineOwnable.sol +│ └── RuleEngineOwnable2Step.sol +├── interfaces/ # IRule, IRulesManagementModule, IERC3643Compliance(Extended) +├── modules/ # VersionModule, RulesManagementModule, +│ │ # ERC3643Compliance(Extended)Module, ERC2771ModuleStandalone +│ └── library/ # invariant storage (errors/events), role constants, interface IDs +└── mocks/ # reference rules and test doubles — not for production -Updates the compliance contract state whenever tokens are transferred. - -Can only be called by the token contract bound to this compliance logic. - This function can be used to update internal state variables. - -**Input Parameters:** - -| Name | Type | Description | -| ----- | ------- | ---------------------------------------------- | -| from | address | The address of the sender. | -| to | address | The address of the receiver. | -| value | uint256 | The number of tokens involved in the transfer. | - - - -#### IERC3643Compliance - ------- - -##### created(address to, uint256 value) - -```solidity -function created(address to, uint256 value) -public virtual override(IERC3643Compliance) -onlyBoundToken +test/ # Foundry tests, one directory per deployable variant +script/ # Foundry deployment / example scripts +doc/ # full documentation, schemas, coverage, audits ``` -Updates the compliance contract state when tokens are created (minted). +**Key invariant:** rule contracts under `src/mocks/` are reference implementations for testing and examples. Production rules live in a separate repository. -Called by the token contract when new tokens are issued to an account. - Reverts if the minting does not comply with the rules. +## Rules -**Input Parameters:** +Production rules are maintained at [github.com/CMTA/Rules](https://github.com/CMTA/Rules), in two families: -| Name | Type | Description | -| ----- | ------- | ---------------------------------------- | -| to | address | The address receiving the minted tokens. | -| value | uint256 | The number of tokens created. | +- **Validation rules (read-only)** — evaluate eligibility without mutating state: `RuleWhitelist`, `RuleBlacklist`, `RuleSanctionList`, `RuleIdentityRegistry`, `RuleSpenderWhitelist`, `RuleERC2980`, `RuleMaxTotalSupply` +- **Operation rules (read-write)** — may update rule state on transfer: `RuleConditionalTransferLight` +To be usable by the engine, a rule must implement `IRule` and advertise it through ERC-165. Restriction codes should stay unique across the composed rule set. +## Quick start ------- +Dependencies are git submodules. -##### destroyed(address from, uint256 value) +```bash +git submodule update --init --recursive # or: forge install +cd lib/CMTAT && npm install && cd ../.. # CMTAT's own OpenZeppelin deps -```solidity -function destroyed(address from, uint256 value) -public virtual override(IERC3643Compliance) -onlyBoundToken +forge build # compile +forge test # run the test suite +forge coverage # code coverage +forge fmt # format ``` -Updates the compliance contract state when tokens are destroyed (burned). - -Called by the token contract when tokens are redeemed or burned. - Reverts if the burning does not comply with the rules. - -**Input Parameters:** - -| Name | Type | Description | -| ----- | ------- | --------------------------------------------- | -| from | address | The address whose tokens are being destroyed. | -| value | uint256 | The number of tokens destroyed. | +See [doc/README.md](./doc/README.md) for deployment scripts, the production deployment checklist, and Hardhat usage. +Parts of this project were written with the help of AI coding assistants, principally Claude Code (Anthropic) and Codex (OpenAI). +## Security -#### IERC1404 - -![IERC1404UML](./doc/schema/vscode-uml/IERC1404UML.png) - ------- - -##### detectTransferRestriction(address from, address to, uint256 value) -> uint8 - -Returns a uint8 code to indicate if a transfer is restricted or not. - -Implements the restriction logic of {ERC-1404}. - Examples of restriction logic include: - -- checking if the recipient is whitelisted, -- checking if the sender’s tokens are frozen during a lock-up period, etc. - -**Input Parameters:** - -| Name | Type | Description | -| ----- | ------- | --------------------------------- | -| from | address | The source address. | -| to | address | The destination address. | -| value | uint256 | The number of tokens to transfer. | - - - -**Return Values:** - -| Type | Description | -| ----- | ------------------------------------------------------ | -| uint8 | Restriction code (0 means the transfer is authorized). | - - - ------- - -##### messageForTransferRestriction(uint8 restrictionCode) -> string - -Returns a human-readable explanation for a transfer restriction code. - -Implements {ERC-1404} standard message accessor. - -**Input Parameters:** - -| Name | Type | Description | -| --------------- | ----- | ---------------------------------- | -| restrictionCode | uint8 | The restriction code to interpret. | - - - -**Return Values:** - -| Type | Description | -| ------ | ---------------------------------------------------- | -| string | A message describing why the transfer is restricted. | - - - ------- - -#### IERC1404Extend - -![IERC1404ExtendUML](./doc/schema/vscode-uml/IERC1404ExtendUML.png) - -##### enum REJECTED_CODE_BASE - -Error codes for transfer restrictions. - Codes `6–9` are reserved for future CMTAT ruleEngine extensions. - -| Name | Value | Description | -| -------------------------------------------------- | ----- | ------------------------------------------------------------ | -| TRANSFER_OK | 0 | Transfer authorized. | -| TRANSFER_REJECTED_PAUSED | 1 | Transfer rejected because the token is paused. | -| TRANSFER_REJECTED_FROM_FROZEN | 2 | Transfer rejected because the sender’s address is frozen. | -| TRANSFER_REJECTED_TO_FROZEN | 3 | Transfer rejected because the recipient’s address is frozen. | -| TRANSFER_REJECTED_SPENDER_FROZEN | 4 | Transfer rejected because the spender’s address is frozen. | -| TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE | 5 | Transfer rejected because the sender does not have enough active (unfrozen) balance. | - - - ------- - -##### detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) -> uint8 - -Returns a uint8 code to indicate if a transfer is restricted or not. - -This is an extension of {ERC-1404} with an additional `spender` parameter to enforce restriction logic on delegated transfers. - Examples of restriction logic include: - -- verifying if the recipient is whitelisted, -- verifying if tokens are locked for either sender or spender, etc. - -**Input Parameters:** - -| Name | Type | Description | -| ------- | ------- | ------------------------------------------------------------ | -| spender | address | The address initiating the transfer (for delegated transfers). | -| from | address | The source address. | -| to | address | The destination address. | -| value | uint256 | The number of tokens to transfer. | - - - -**Return Values:** - -| Type | Description | -| ----- | ------------------------------------------------------ | -| uint8 | Restriction code (0 means the transfer is authorized). | - - - ------- - -### VersionModule - -![VersionModuleUML](./doc/schema/vscode-uml/VersionModuleUML.png) - -#### Contracts Description Table - - -| Contract | Type | Bases | | | -| :---------------: | :---------------: | :------------: | :------------: | :-----------: | -| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | -| | | | | | -| **VersionModule** | Implementation | IERC3643Base | | | -| └ | version | Public ❗️ | | NO❗️ | - -#### version() - -```solidity -function version() external view returns (string memory version_); -``` - -```solidity -function version() -public view virtual override(IERC3643Base) -returns (string memory version_) -``` - - **Description** - -Returns the current version of the token contract. -Useful for identifying which version of the smart contract is deployed and in use. - -**Return** - -| Name | Type | Description | -| ---------- | ------ | ------------------------------------------------------------ | -| `version_` | string | The version string of the token implementation (e.g., "1.0.0"). | - - - -### ERC3643ComplianceModule - -![ERC3643ComplianceModuleUML](./doc/schema/vscode-uml/ERC3643ComplianceModuleUML.png) - -#### Contracts Description Table - - -| Contract | Type | Bases | | | -| :-------------------------: | :---------------: | :-------------------------------: | :------------: | :-----------: | -| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | -| | | | | | -| **ERC3643ComplianceModule** | Implementation | Context, IERC3643Compliance | | | -| └ | bindToken | Public ❗️ | 🛑 | onlyRole | -| └ | unbindToken | Public ❗️ | 🛑 | onlyRole | -| └ | isTokenBound | Public ❗️ | | NO❗️ | -| └ | getTokenBound | Public ❗️ | | NO❗️ | - -### ERC3643ComplianceExtendedModule - -`ERC3643ComplianceExtendedModule` inherits `ERC3643ComplianceModule` and contains project-specific helpers not part of the ERC-3643 base interface (`IERC3643Compliance`): batch bind/unbind, self-binding approval APIs, and `getTokenBounds()`. -| └ | _unbindToken | Internal 🔒 | 🛑 | | -| └ | _bindToken | Internal 🔒 | 🛑 | | - -#### Events - -##### TokenBound(address token) - -```solidity -event TokenBound(address token) -``` - -Emitted when a token is successfully bound to the compliance contract. - -**Event Parameters:** - -| Name | Type | Description | -| ----- | ------- | ---------------------------------------- | -| token | address | The address of the token that was bound. | - - - ------- - -##### TokenUnbound(address token) - -```solidity -event TokenUnbound(address token) -``` - -Emitted when a token is successfully unbound from the compliance contract. - -**Event Parameters:** - -| Name | Type | Description | -| ----- | ------- | ------------------------------------------ | -| token | address | The address of the token that was unbound. | - - - ------- - -#### Functions - -##### bindToken(address token) - -```solidity -function bindToken(address token) -public override virtual -onlyRole(COMPLIANCE_MANAGER_ROLE) -``` - -Associates a token contract with this compliance contract. - -The compliance contract may restrict operations on the bound token according to its internal compliance logic. - Reverts if the token is already bound. - -**Input Parameters:** - -| Name | Type | Description | -| ----- | ------- | --------------------------------- | -| token | address | The address of the token to bind. | - - - ------- - -##### unbindToken(address token) - -```solidity -function unbindToken(address token) -public override virtual -onlyRole(COMPLIANCE_MANAGER_ROLE) -``` - -Removes the association of a token contract from this compliance contract. - -Reverts if the token is not currently bound. - -**Input Parameters:** - -| Name | Type | Description | -| ----- | ------- | ----------------------------------- | -| token | address | The address of the token to unbind. | - - - ------- - -##### isTokenBound(address token) -> bool - -```solidity -function isTokenBound(address token) -public view virtual override -returns (bool) -``` - -Checks whether a token is currently bound to this compliance contract. - -**Input Parameters:** - -| Name | Type | Description | -| ----- | ------- | ---------------------------- | -| token | address | The token address to verify. | - - - -**Return Values:** - -| Type | Description | -| ---- | -------------------------------------------- | -| bool | True if the token is bound, false otherwise. | - - - ------- - -##### getTokenBound() -> address - -```solidity -function getTokenBound() -public view virtual override -returns (address) -``` - -Returns the single token currently bound to this compliance contract. - -If multiple tokens are supported, consider using `getTokenBounds()`. - -Note that there are no guarantees on the ordering of values inside the array, and it may change when more values are added or removed. - -**Return Values:** - -| Type | Description | -| ------- | ----------------------------------------- | -| address | The address of the currently bound token. | - - - ------- - -##### getTokenBounds() -> address[] - -```solidity -function getTokenBounds() -public view override -returns (address[] memory) -``` - -Returns all tokens currently bound to this compliance contract. - -This is a view-only function and does not modify state. -This function is not part of the original ERC-3643 specification. - -This operation will copy the entire storage to memory, which can be quite expensive. - -This is designed to mostly be used by view accessors that are queried without any gas fees. - -**Return Values:** - -| Type | Description | -| --------- | ----------------------------------------------- | -| address[] | An array of addresses of bound token contracts. | - - - -### RulesManagementModule - -![RuleManagementModuleUML](./doc/schema/vscode-uml/RuleManagementModuleUML.png) - -#### Events - -##### event AddRule(address rule) - -```solidity -event AddRule(IRule indexed rule) -``` - -Emitted when a new rule is added to the rule set. - -**Event Parameters:** - -| Name | Type | Description | -| ---- | ----- | ------------------------------------------------ | -| rule | IRule | The address of the rule contract that was added. | - ------- - -##### event RemoveRule(address rule) - -```solidity -event RemoveRule(IRule indexed rule) -``` - -Emitted when a rule is removed from the rule set. - -**Event Parameters:** - -| Name | Type | Description | -| ---- | ----- | -------------------------------------------------- | -| rule | IRule | The address of the rule contract that was removed. | - ------- - -##### event ClearRules() - -```solidity -event ClearRules() -``` - -Emitted when all rules are cleared from the rule set. - -This event has no parameters. - -#### Functions - -##### setRules(address[] rules_) - -```solidity -function setRules(IRule[] calldata rules_) -public virtual override(IRulesManagementModule) -onlyRole(RULES_MANAGEMENT_ROLE) -``` - -Defines the complete list of rules for the rule engine. - -Any previously configured rules are completely replaced. - Rules must be deployed contracts implementing the expected `IRule` interface. - Reverts if any rule address is zero or if duplicates are detected. - -This function calls _clearRules if at least one rule is still configured - -**Input Parameters:** - -| Name | Type | Description | -| ------ | ------- | ------------------------------------------------------------ | -| rules_ | IRule[] | The array of IRule contracts to configure as the active rules. | - - - ------- - -##### rulesCount() -> uint256 - -```solidity -function rulesCount() -public view virtual override(IRulesManagementModule) -returns (uint256) -``` - -Returns the total number of currently configured rules. - -Equivalent to the length of the internal rules array. - -**Return Values:** - -| Type | Description | -| ------- | --------------------------- | -| uint256 | The number of active rules. | - - - ------- - -##### rule(uint256 ruleId) -> address - -```solidity -function rule(uint256 ruleId) -public view virtual override(IRulesManagementModule) -returns (address) -``` - -Retrieves the rule address at a specific index. - -Return the `zero address` if out of bounds. - -Note that there are no guarantees on the ordering of values inside the array, and it may change when more values are added or removed. - -**Input Parameters:** - -| Name | Type | Description | -| ------ | ------- | ------------------------------------------- | -| ruleId | uint256 | The index of the desired rule in the array. | - - - -**Return Values:** - -| Type | Description | -| ------- | ------------------------------------------------ | -| address | The address of the corresponding IRule contract. | - - - ------- - -##### rules() -> address[] - -```solidity -function rules() -public view virtual override(IRulesManagementModule) -returns (address[] memory) -``` - -Returns the full list of currently configured rules. - -This is a view-only function and does not modify state. - -This operation will copy the entire storage to memory, which can be quite expensive. - -This is designed to mostly be used by view accessors that are queried without any gas fees. - -**Return Values:** - -| Type | Description | -| --------- | ------------------------------------------------------- | -| address[] | An array containing all active rule contract addresses. | - - - ------- - -##### clearRules() - -```solidity -function clearRules() -public virtual override(IRulesManagementModule) -onlyRole(RULES_MANAGEMENT_ROLE) -``` - -Removes all configured rules. - -After calling this function, no rules will remain set. - -Developers should keep in mind that this function has an unbounded cost and using it may render the function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. - ------- - -##### addRule(address rule_) - -```solidity -function addRule(IRule rule_) -public virtual override(IRulesManagementModule) -onlyRole(RULES_MANAGEMENT_ROLE) -``` - -Adds a new rule to the current rule set. - -Reverts if the rule address is zero or already exists in the set. - -**Input Parameters:** - -| Name | Type | Description | -| ----- | ----- | -------------------------- | -| rule_ | IRule | The IRule contract to add. | - - - ------- - -##### removeRule(address rule_) - -```solidity - function removeRule(IRule rule_) - public virtual - override(IRulesManagementModule) - onlyRole(RULES_MANAGEMENT_ROLE) -``` - -Removes a specific rule from the current rule set. - -Reverts if the provided rule is not found or does not match the stored rule at its index. - -**Input Parameters:** - -| Name | Type | Description | -| ----- | ----- | ----------------------------- | -| rule_ | IRule | The IRule contract to remove. | - - - ------- - -##### containsRule(address rule_) -> bool - -```solidity -function containsRule(IRule rule_) -public view virtual override(IRulesManagementModule) -returns (bool) -``` - -Checks whether a specific rule is currently configured. - -**Input Parameters:** - -| Name | Type | Description | -| ----- | ----- | ------------------------------------------- | -| rule_ | IRule | The IRule contract to check for membership. | - - - -**Return Values:** - -| Type | Description | -| ---- | --------------------------------------------- | -| bool | True if the rule is present, false otherwise. | - -## Security - -### Vulnerability disclosure - -Please see [SECURITY.md](https://github.com/CMTA/CMTAT/blob/master/SECURITY.md) (CMTAT main repository). - -### Audit - -#### First Audit - March 2022 - -> The contracts (v.1.0.2) have been audited by [ABDK Consulting](https://www.abdk.consulting/), a globally recognized firm specialized in smart contracts' security. - -Fixed version : [v1.0.2](https://github.com/CMTA/RuleEngine/releases/tag/v1.0.2) - -The first audit was performed by ABDK on the version [1.0.1](https://github.com/CMTA/RuleEngine/releases/tag/1.0.1). - -The release [v1.0.2](https://github.com/CMTA/RuleEngine/releases/tag/v1.0.2) contains the different fixes and improvements related to this audit. - -The final report is available in [ABDK_CMTA_CMTATRuleEngine_v_1_0.pdf](https://github.com/CMTA/CMTAT/blob/master/doc/audits/ABDK_CMTA_CMTATRuleEngine_v_1_0/ABDK_CMTA_CMTATRuleEngine_v_1_0.pdf). - -### Tools - -#### Nethermind AuditAgent - -> **Note:** This scan was performed by an AI-powered automated tool, not a formal human-led audit. - -| Version | Report | Assessment | -|---------|--------|------------| -| Scan #1 (Feb 2026) | [audit_agent_report_1_v3.0.0-rc1.pdf](./doc/security/audits/tools/nethermind-audit-agent/v3.0.0-rc1/audit_agent_report_1_v3.0.0-rc1.pdf) | [feedback.md](./doc/security/audits/tools/nethermind-audit-agent/v3.0.0-rc1/audit_agent_report_1_v3.0.0-rc1-feedback.md) | - -7 findings — 0 High · 1 Medium · 1 Low · 4 Info · 1 Best Practices - -| # | Severity | Finding | Status | -|---|----------|---------|--------| -| 1 | Medium | Cross-token rule state pollution in multi-tenant deployments | NatSpec + README warnings. Interface fix deferred (requires CMTAT coordination). | -| 2 | Low | `RuleEngineOwnable` misreports `IAccessControl` via ERC-165 | Fixed: explicit interface whitelist + negative test added. | -| 3 | Info | Unbounded rules loop — potential permanent DoS | 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()` | 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). | - -#### Slither - -Here is the list of report performed with [Slither](https://github.com/crytic/slither) - -| Version | Report | Assessment | -| ------- | ------ | ---------- | -| v3.0.0-rc4 | [slither-report.md](./doc/security/audits/tools/v3.0.0-rc4/slither-report.md) | [slither-report-feedback.md](./doc/security/audits/tools/v3.0.0-rc4/slither-report-feedback.md) | -| v3.0.0-rc3 | [slither-report.md](./doc/security/audits/tools/v3.0.0-rc3/slither-report.md) | [slither-report-feedback.md](./doc/security/audits/tools/v3.0.0-rc3/slither-report-feedback.md) | -| v3.0.0-rc2 | [slither-report.md](./doc/security/audits/tools/v3.0.0-rc2/slither-report.md) | [slither-report-feedback.md](./doc/security/audits/tools/v3.0.0-rc2/slither-report-feedback.md) | - -```bash -slither . --checklist --filter-paths "openzeppelin-contracts|test|CMTAT|forge-std|mocks" > slither-report.md -``` - -2 finding categories — 0 High · 0 Medium · 10 Low · 2 Informational - -| ID | Detector | Impact | Instances | Assessment | -|----|----------|--------|-----------|------------| -| 0–9 | `calls-loop` | Low | 10 | Accepted by design — fan-out to rule contracts is the core architecture | -| 10–11 | `unindexed-event-address` | Informational | 2 | Deferred — adding `indexed` to `TokenBound`/`TokenUnbound` is interface-breaking | - -#### Aderyn - -Here is the list of report performed with [Aderyn](https://github.com/Cyfrin/aderyn) - -```bash -aderyn -x mocks --output aderyn-report.md -``` - -| Version | Report | Assessment | -| ------- | ------ | ---------- | -| v3.0.0-rc4 | [aderyn-report.md](./doc/security/audits/tools/v3.0.0-rc4/aderyn-report.md) | [aderyn-report-feedback.md](./doc/security/audits/tools/v3.0.0-rc4/aderyn-report-feedback.md) | -| v3.0.0-rc3 | [aderyn-report.md](./doc/security/audits/tools/v3.0.0-rc3/aderyn-report.md) | [aderyn-report-feedback.md](./doc/security/audits/tools/v3.0.0-rc3/aderyn-report-feedback.md) | -| v3.0.0-rc2 | [aderyn-report.md](./doc/security/audits/tools/v3.0.0-rc2/aderyn-report.md) | [aderyn-report-feedback.md](./doc/security/audits/tools/v3.0.0-rc2/aderyn-report-feedback.md) | - -Report scope: 24 Solidity files, 629 nSLOC. - -0 High · 8 Low - -| ID | Finding | Instances | Assessment | -|----|---------|-----------|------------| -| L-1 | Centralization Risk | 14 | Accepted by design — privileged compliance tool | -| L-2 | Unspecific Solidity Pragma | 19 | Accepted by design — intentional for library reusability | -| L-3 | PUSH0 Opcode | 24 | 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` | 4 | Accepted by design — `setRules` and `bindTokens`/`unbindTokens` are atomic batch operations | -| L-7 | Costly Operations Inside Loop | 4 | Accepted — unavoidable `SSTORE` in batch operations | -| L-8 | Unchecked Return | 1 | Accepted — `_grantRole` return is irrelevant in constructor | - -## Documentation - -Here a summary of the main documentation - -| Document | Link/Files | -| ------------ | --------------------------------------- | -| Toolchain | [doc/TOOLCHAIN.md](./doc/TOOLCHAIN.md) | -| Surya report | [doc/schema/surya](./doc/schema/surya/) | - -See also [Taurus - Token Transfer Management: How to Apply Restrictions with CMTAT and ERC-1404](https://www.taurushq.com/blog/token-transfer-management-how-to-apply-restrictions-with-cmtat-and-erc-1404/) (RuleEngine v2.02 and CMTAT v2.4.0) - -## Toolchains and Usage - -This repository is primarily developed and tested with Foundry. - -Hardhat configuration is also present to allow compiling the contracts and running a small smoke test with Hardhat. - -### Configuration - -Here are the settings for [Hardhat](https://hardhat.org) and [Foundry](https://getfoundry.sh). - -- `hardhat.config.js` - - Solidity [v0.8.34](https://docs.soliditylang.org/en/v0.8.34/) - - EVM version: Prague (Pectra upgrade) - - Optimizer: true, 200 runs - -- `foundry.toml` - - Solidity [v0.8.34](https://docs.soliditylang.org/en/v0.8.34/) - - EVM version: Prague (Pectra upgrade) - - Optimizer: true, 200 runs - - - - -### Toolchain installation -The contracts are developed and tested with [Foundry](https://book.getfoundry.sh), a smart contract development toolchain. - -To install the Foundry suite, please refer to the official instructions in the [Foundry book](https://book.getfoundry.sh/getting-started/installation). - -### Initialization - -You must first initialize the submodules, with - -``` -forge install -``` - -See also the command's [documentation](https://book.getfoundry.sh/reference/forge/forge-install). - -Later you can update all the submodules with: - -``` -forge update -``` - -See also the command's [documentation](https://book.getfoundry.sh/reference/forge/forge-update). - -### Compilation - -The official documentation is available in the Foundry [website](https://book.getfoundry.sh/reference/forge/build-commands) - -```bash -# Build all contracts -forge build - -# Build specific contract -forge build --contracts src/deployment/RuleEngine.sol -forge build --contracts src/deployment/RuleEngineOwnable.sol -forge build --contracts src/deployment/RuleEngineOwnable2Step.sol -``` -### Contract size - -```bash -forge build --sizes -``` - -Latest output (`2026-03-18`) for the main RuleEngine contracts: - -| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) | -|----------|------------------:|------------------:|--------------------:|---------------------:| -| RuleEngine | 6,756 | 7,805 | 17,820 | 41,347 | -| RuleEngineOwnable | 6,170 | 6,833 | 18,406 | 42,319 | - -Both `RuleEngine` and `RuleEngineOwnable` remain well below the EIP-170 runtime limit. `RuleEngineOwnable` is slightly smaller because `Ownable` has less overhead than `AccessControl`. - -### Testing - -You can run the tests with - -```bash -forge test -``` - -To run a specific test, use - -```bash -forge test --match-contract --match-test -``` - -Generate gas report - -```bash -forge test --gas-report -``` - -See also the test framework's [official documentation](https://book.getfoundry.sh/forge/tests), and that of the [test commands](https://book.getfoundry.sh/reference/forge/test-commands). - -There is also a small Hardhat smoke test to confirm the main `RuleEngine` contract can be compiled and deployed through Hardhat: - -```bash -npx hardhat test test/hardhat/RuleEngine.smoke.js -``` - -### Coverage - -A code coverage is available in [index.html](./doc/coverage/coverage/index.html). - -![code-coverage](./doc/coverage/code-coverage.png) - -* Perform a code coverage -``` -forge coverage -``` - -* Generate LCOV report -``` -forge coverage --report lcov -``` - -- Generate `index.html` - -```bash -forge coverage --no-match-coverage "(script|mocks|test)" --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage -``` - -See [Solidity Coverage in VS Code with Foundry](https://mirror.xyz/devanon.eth/RrDvKPnlD-pmpuW7hQeR5wWdVjklrpOgPCOA-PJkWFU) & [Foundry forge coverage](https://www.rareskills.io/post/foundry-forge-coverage) - -### Deployment -The official documentation is available in the Foundry [website](https://getfoundry.sh/forge/deploying) - -#### Choosing a Contract - -| Scenario | Recommended Contract | -|----------|---------------------| -| Multiple operators with different permissions | `RuleEngine` | -| Single administrator | `RuleEngineOwnable` | -| Single administrator with safer ownership handover | `RuleEngineOwnable2Step` | -| Integration with existing RBAC systems | `RuleEngine` | -| Simpler deployment and management | `RuleEngineOwnable` | - -#### Script - -The scripts in `script/` are example deployment flows. - -> Warning: `RuleEngineScript.s.sol` and `CMTATWithRuleEngineScript.s.sol` deploy `RuleWhitelist` from `src/mocks/`. That contract is a reference/mock rule for testing and demos, not a production rule contract. - -For production deployments, source rule contracts from the dedicated [CMTA/Rules](https://github.com/CMTA/Rules) repository and adapt the script parameters accordingly. - -To run the example scripts, create a `.env` file. The value for `CMTAT_ADDRESS` is required only for `RuleEngineScript.s.sol`. - -Warning: putting your private key in a .env file is not the most secure approach. - -* File `.env` -``` -PRIVATE_KEY= -CMTAT_ADDRESS= -``` -**Private Keys**: Never expose your private keys. The `.env` file here used in this project should not be used for production. See [getfoundry.sh - Key Management](https://getfoundry.sh/guides/best-practices/key-management/) - -* Command - -CMTAT with RuleEngine - -```bash -forge script script/CMTATWithRuleEngineScript.s.sol:CMTATWithRuleEngineScript --rpc-url=$RPC_URL --broadcast --verify -vvv -``` - - -- Value of YOUR_RPC_URL with a local instance of anvil : [127.0.0.1:8545](http://127.0.0.1:8545) - -```bash -forge script script/CMTATWithRuleEngineScript.s.sol:CMTATWithRuleEngineScript --rpc-url=127.0.0.1:8545 --broadcast --verify -vvv -``` - -Only RuleEngine with the mock/reference `RuleWhitelist` contract - -```bash -forge script script/RuleEngineScript.s.sol:RuleEngineScript --rpc-url=$RPC_URL --broadcast --verify -vvv -``` - -- With anvil - -```bash -forge script script/RuleEngineScript.s.sol:RuleEngineScript --rpc-url=127.0.0.1:8545 --broadcast --verify -vvv -``` - -#### Production Deployment Checklist - -- Choose the deployable variant: `RuleEngine`, `RuleEngineOwnable`, or `RuleEngineOwnable2Step`. -- Choose the trusted forwarder address, or use `address(0)` if ERC-2771 support is not needed. -- Decide whether the token should be bound in the constructor or later via `bindToken`. -- Source production rule contracts from the [CMTA/Rules](https://github.com/CMTA/Rules) repository, not from `src/mocks/`. -- Verify post-deployment permissions: owner for ownable variants, or admin plus role assignments for the RBAC variant. - -### Solidity style guideline - -RuleEngine follows the [solidity style guideline](https://docs.soliditylang.org/en/latest/style-guide.html) and the [natspec format](https://docs.soliditylang.org/en/latest/natspec-format.html) for comments - -#### Formatting & Linting - -We use Foundry's built-in formatter and linter: - -```bash -# Format all Solidity files -forge fmt - -# Check formatting without modifying files -forge fmt --check - -# Run the Solidity linter -forge lint -``` - -- Orders of Functions - -Functions are grouped according to their visibility and ordered: - -``` -1. constructor - -2. receive function (if exists) - -3. fallback function (if exists) - -4. external - -5. public - -6. internal - -7. private -``` - -Within a grouping, place the `view` and `pure` functions last - -- Function declaration - -``` -1. Visibility -2. Mutability -3. Virtual -4. Override -5. Custom modifiers -``` +- Vulnerability disclosure: [SECURITY.md](https://github.com/CMTA/CMTAT/blob/master/SECURITY.md) (CMTAT main repository) +- v1.0.2 was audited by [ABDK Consulting](https://www.abdk.consulting/) in March 2022; the current 3.0.0 line has **not** been audited +- Static-analysis reports (Slither, Aderyn, Nethermind AuditAgent) are in [doc/security](./doc/security/) ## Intellectual property diff --git a/doc/ERCSpecification/rework/erc-1404-analysis.md b/doc/ERCSpecification/rework/erc-1404-analysis.md new file mode 100644 index 0000000..d926be1 --- /dev/null +++ b/doc/ERCSpecification/rework/erc-1404-analysis.md @@ -0,0 +1,272 @@ +# ERC-1404 (rework) Conformance Analysis — RuleEngine + +**Scope:** Does the RuleEngine implement the reworked ERC-1404 draft +(`doc/ERCSpecification/rework/erc-1404.md`) correctly? + +**Verdict:** ✅ **Conformant.** The RuleEngine implements both mandatory +methods, the optional spender-aware extension, and both ERC-165 identifiers +exactly as specified. It is used in the "standalone compliance contract" mode +the reworked spec explicitly blesses. The one minor observation that was noted +on the `messageForTransferRestriction(0)` return value has since been fixed +(§7). + +Analysis date: 2026-07-21. Files cited are at the paths shown. +Item 13 resolved on 2026-07-29. + +--- + +## 1. What the reworked spec requires + +The rework keeps the original two-method core and adds an OPTIONAL spender-aware +extension plus explicit ERC-165 identifiers: + +| Requirement | Level | Source (spec §) | +|---|---|---| +| `detectTransferRestriction(address,address,uint256) → uint8` | MUST | Methods | +| `messageForTransferRestriction(uint8) → string` | MUST | Methods | +| `detectTransferRestrictionFrom(address,address,address,uint256) → uint8` | OPTIONAL (extension) | Extension | +| Enforcement consistent with `detectTransferRestriction` | MUST | Methods, Security | +| Extension consistent with `transferFrom` enforcement | MUST (if exposed) | Extension | +| `spender == from` evaluated through spender-aware path | SHOULD (if exposed) | Extension, Security | +| ERC-165 base id `0xab84a5c8` (2 mandatory methods) | MUST if ERC-165 | §Additional | +| ERC-165 extension id `0x78a8de7d` (all 3 methods) | MUST if extension + ERC-165 | Extension | +| A compliance contract MAY implement the restriction interface without being a token | Allowed | §Additional | +| Restriction checks on mint/burn | MAY | §Additional | +| `messageForTransferRestriction(0)` → deterministic "no restriction" string | Test case (SHOULD verify) | Test Cases | + +--- + +## 2. Mandatory interface — ✅ + +Both mandatory methods are declared on `RuleEngineBase` and inherited by all +three deployable variants (`RuleEngine`, `RuleEngineOwnable`, +`RuleEngineOwnable2Step`). + +`src/RuleEngineBase.sol:84` — `detectTransferRestriction(from, to, value)` +delegates to `_detectTransferRestriction`, which iterates the active rule set +and returns the first non-zero code, else `0` (`TRANSFER_OK`): + +```solidity +function _detectTransferRestriction(address from, address to, uint256 value) internal view virtual returns (uint8) { + for (uint256 i = 0; i < rulesCount(); ++i) { + uint8 restriction = IRule(rule(i)).detectTransferRestriction(from, to, value); + if (restriction > 0) return restriction; + } + return uint8(REJECTED_CODE_BASE.TRANSFER_OK); +} +``` + +`src/RuleEngineBase.sol` — `messageForTransferRestriction(uint8)` returns +`"NoRestriction"` for the reserved code `0`, otherwise the message of the first +rule that claims the code (via `canReturnTransferRestrictionCode`), else +`"Unknown restriction code"`. + +Signatures match the spec exactly (`address,address,uint256 → uint8` and +`uint8 → string memory`), both `view`. ✅ + +--- + +## 3. Standalone compliance-contract mode — ✅ + +The reworked spec added explicit language (§Additional Specifications, and +Security Considerations final bullet) that a **rule engine / compliance module +MAY implement the restriction interface without implementing any token +interface**. The RuleEngine is exactly this: it holds no balances and performs +no transfers; it is consulted by the bound CMTAT/ERC-3643 token. This is the +mode the rework was written to legitimize, so the RuleEngine matches the +spec's intent, not merely its letter. ✅ + +--- + +## 4. Enforcement consistency — ✅ (by construction, per-rule) + +The spec's central MUST: a transfer MUST be rejected iff +`detectTransferRestriction` returns non-zero for the same state/inputs. + +The RuleEngine does not re-implement policy — it aggregates rules. The reporting +path (`_detectTransferRestriction`) and the enforcement path (`_transferred`) +iterate **the same `_rules` set in the same order**: + +- Reporting: `rule.detectTransferRestriction(from,to,value)` — `RuleEngineBase.sol:149` +- Enforcement: `rule.transferred(from,to,value)` — `RulesManagementModule.sol:203` + +So the RuleEngine's aggregate behavior is consistent **iff each individual rule +keeps its own `transferred` consistent with its `detectTransferRestriction`**. +The reference rule does this correctly — `RuleWhitelist.transferred` literally +calls `detectTransferRestriction` and reverts on non-zero +(`src/mocks/rules/validation/RuleWhitelist.sol:93`): + +```solidity +function transferred(address from, address to, uint256 value) public { + uint8 code = detectTransferRestriction(from, to, value); + require(code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), RuleWhitelist_InvalidTransfer(...)); +} +``` + +This is the RECOMMENDED "invoke `detectTransferRestriction` inside the transfer +path" pattern, which the spec says guarantees consistency by construction. ✅ + +> **Note (design boundary, not a defect):** the RuleEngine cannot *force* +> third-party rules to stay consistent. The spec places the +> reporting/enforcement equivalence invariant on the implementation and +> recommends verifying it under test. The RuleEngine's aggregation preserves +> whatever consistency each rule provides; a buggy custom rule could break it. +> This is inherent to the pluggable-rule architecture and is the correct place +> to draw the line. + +--- + +## 5. Optional spender-aware extension — ✅ + +The extension is fully implemented. + +`src/RuleEngineBase.sol:97` — `detectTransferRestrictionFrom(spender,from,to,value)` +delegates to `_detectTransferRestrictionFrom`, aggregating +`rule.detectTransferRestrictionFrom(...)` with the same first-non-zero logic. +It **shares the restriction-code space** of the base method (same enum, same +`messageForTransferRestriction` lookup), as the spec requires. + +Delegated-transfer enforcement uses the 4-arg +`_transferred(spender,from,to,value)` path +(`RulesManagementModule.sol:222`), which calls `rule.transferred(spender,...)`. +For the reference rule that path calls `detectTransferRestrictionFrom` and +reverts on non-zero (`RuleWhitelist.sol:98`), so the extension predictor and +`transferFrom` enforcement agree. ✅ + +### `spender == from` — SHOULD honored ✅ + +The spec's SHOULD: because `transferFrom` is a distinct delegated-transfer +entry point, `spender == from` should be evaluated through the spender-aware +path (not collapsed to the direct predictor) whenever the policy restricts +operator identity. The reference rule does **not** short-circuit; it always +checks the operator (`RuleWhitelist.sol:79`): + +```solidity +function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) public view override returns (uint8) { + // Mint / burn are exempt from the spender check + if (from != address(0) && to != address(0) && !addressIsListed(spender)) { + return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; + } + return detectTransferRestriction(from, to, value); +} +``` + +The whitelist policy *does* restrict operator identity, so evaluating the +spender even when `spender == from` is exactly what the spec's Security +Considerations warn must not be skipped. It also correctly stays consistent: +if `spender == from` and `from` is whitelisted, the spender check passes and it +falls through to the base predictor — the two coincide, as the spec permits. +✅ + +### Mint / burn handling — ✅ (spec MAY) + +The spec permits restriction checks on mint/burn and allows the `spender` +parameter to carry the operator. The RuleEngine routes mint/burn through +`created`/`destroyed` → the 3-arg `_transferred` (no spender) +(`RuleEngineBase.sol:66`), while the 4-arg `transferred` path (CMTAT v3.3.0) +carries the operator as `spender`. The reference rule exempts mint +(`from == 0`) and burn (`to == 0`) from the spender check, so those operations +are not blocked by an unlisted operator. This matches the spec's guidance that +rules checking `spender` "must skip or adapt that check for mint/burn." ✅ + +--- + +## 6. ERC-165 identifiers — ✅ (values verified) + +Both identifiers are advertised. `RuleEngineBase._supportsRuleEngineBaseInterface` +(`src/RuleEngineBase.sol:206`) returns `true` for: + +- `ERC1404InterfaceId.IERC1404_INTERFACE_ID = 0xab84a5c8` (`src/modules/library/ERC1404InterfaceId.sol:10`) +- `ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID = 0x78a8de7d` (CMTAT `library/ERC1404ExtendInterfaceId.sol`) + +Each deployable variant chains this into its `supportsInterface` +(`RuleEngine.sol:82` via `AccessControlEnumerable`; ownable variants via +`RuleEngineOwnableShared.sol:29` and `RuleEngineOwnable2Step.sol:49`), and OZ's +ERC-165 base supplies `0x01ffc9a7`. + +I recomputed both selectors with `cast` to confirm they match the spec's pinned +values: + +``` +detectTransferRestriction(address,address,uint256) = 0xd4ce1415 +messageForTransferRestriction(uint8) = 0x7f4ab1dd +detectTransferRestrictionFrom(address,address,address,uint256) = 0xd32c7bb5 + +base id = detect ^ msg = 0xab84a5c8 ✅ (matches spec) +extension id = detect ^ msg ^ detectFrom = 0x78a8de7d ✅ (matches spec) +``` + +This satisfies the spec's requirements precisely: + +- The base id is the XOR of the **two mandatory methods only** — the extension + does not alter it (spec §Extension, "MUST NOT change the meaning of…"). ✅ +- The extension id is the XOR of **all three selectors**, not just the added + method — self-contained, so a single `supportsInterface(0x78a8de7d)` proves + all three are present (spec §Extension rationale). ✅ +- An extension-exposing implementation advertises **both** ids. ✅ + +--- + +## 7. Minor observation — ✅ resolved + +**`messageForTransferRestriction(0)` used to return `"Unknown restriction code"`.** + +The spec's Test Cases table lists `messageForTransferRestriction(0) →` "a +deterministic human-readable string indicating no restriction (e.g. `"No +restriction"`)". Previously, code `0` was `TRANSFER_OK` and no rule claimed it +(`RuleWhitelistCommon.canReturnTransferRestrictionCode` returns `false` for +`0`), so the aggregate lookup fell through to `"Unknown restriction code"`. + +- **Severity:** cosmetic / informational only. The return was still + deterministic and human-readable, and `0` never accompanies a rejected + transfer, so no reporting/enforcement invariant was affected. The spec text is + a SHOULD-verify test case using "e.g.", not a normative MUST on the exact + string. +- **Impact:** a UI that special-cases the ERC-1404 sentinel `0` read + `"Unknown restriction code"` for a perfectly valid transfer, which was + slightly misleading. +- **Fix applied:** `RuleEngineBase._messageForTransferRestriction` now + short-circuits `restrictionCode == REJECTED_CODE_BASE.TRANSFER_OK` and + returns `TEXT_TRANSFER_OK` before iterating the rules. The string is + `"NoRestriction"` — the same value CMTAT's `ValidationModuleERC1404` returns + for code `0` — so a UI reading the token or the engine directly gets an + identical answer. The spec only gives `"No restriction"` as an example, so + either value satisfies the SHOULD; matching CMTAT was preferred for + consistency across the stack. +- **Reference rules:** the same short-circuit was added to the reference rules + that advertise the ERC-1404 identifiers (`RuleWhitelistCommon`, + `RuleConditionalTransferLight`, `RuleMintAllowance`), so they answer code `0` + the same way when queried directly. `RuleOperationRevert` is intentionally + left unchanged — it is a deliberately non-conformant mock used to exercise + the engine's revert paths. +- **Tests:** `messageForTransferRestriction(0)` is asserted for all three + deployable variants (`RuleEngine`, `RuleEngineOwnable`, + `RuleEngineOwnable2Step`), both with an active rule and with an empty rule + set, plus at the rule level in `test/RuleWhitelist/RuleWhitelist.t.sol`. + +--- + +## 8. Requirement-by-requirement summary + +| # | Requirement | Level | Status | +|---|---|---|---| +| 1 | `detectTransferRestriction(address,address,uint256)` | MUST | ✅ | +| 2 | `messageForTransferRestriction(uint8)` | MUST | ✅ | +| 3 | Enforcement consistent with reporting | MUST | ✅ (per-rule, by construction) | +| 4 | `detectTransferRestrictionFrom` extension | OPTIONAL | ✅ implemented | +| 5 | Extension consistent with `transferFrom` enforcement | MUST (if exposed) | ✅ | +| 6 | `spender == from` via spender-aware path | SHOULD | ✅ (no collapse; operator checked) | +| 7 | Extension shares code space + message lookup | MUST (if exposed) | ✅ | +| 8 | ERC-165 base id `0xab84a5c8` | MUST if ERC-165 | ✅ (recomputed) | +| 9 | ERC-165 extension id `0x78a8de7d` | MUST if extension | ✅ (recomputed) | +| 10 | ERC-165 base id unchanged by extension | MUST | ✅ | +| 11 | Standalone compliance contract (no token iface) | Allowed | ✅ (intended mode) | +| 12 | Mint/burn restriction checks | MAY | ✅ (adapted for spender) | +| 13 | `messageForTransferRestriction(0)` → "no restriction" string | SHOULD-verify | ✅ returns `"NoRestriction"` | + +**Conclusion:** The RuleEngine is a correct and complete implementation of the +reworked ERC-1404 draft, including the optional spender-aware extension and +both ERC-165 identifiers, operating in the standalone-compliance-contract mode +the rework explicitly permits. The only deviation — the cosmetic message +returned for code `0` — has been fixed; every row of the table above is now +satisfied. diff --git a/doc/ERCSpecification/rework/erc-1404-improvement29.md b/doc/ERCSpecification/rework/erc-1404-improvement29.md new file mode 100644 index 0000000..2b73c36 --- /dev/null +++ b/doc/ERCSpecification/rework/erc-1404-improvement29.md @@ -0,0 +1,232 @@ +# ERC-1404 (rework) — Suggested Improvements + +**Scope:** Review of the reworked ERC-1404 draft (`doc/ERCSpecification/rework/erc-1404.md`) +for normative gaps, from the perspective of an implementer building a +standalone rule engine against it. + +**Summary:** The draft is in good shape — the OPTIONAL spender-aware extension, +the split ERC-165 identifiers, and the reporting/enforcement invariant are all +well argued. What follows targets the gaps rather than the parts that work. +Two of the seven items below are places where an implementation can be fully +conformant on paper and still surprise an integrator; those are ranked first. + +Review date: 2026-07-29. Line numbers refer to `erc-1404.md` as of that date. +Implementation evidence is drawn from this repository (RuleEngine v3.0.0). + +--- + +## 1. For a standalone compliance contract, the central MUST has no addressee + +**Severity:** highest — this is a normative hole, not a wording issue. + +Line 48 places the load-bearing requirement on *enforcement*: + +> Enforcement MUST be consistent with `detectTransferRestriction` for the same +> state and inputs: a transfer MUST be rejected whenever +> `detectTransferRestriction` would return a non-zero code for that transfer […] + +Line 117 then blesses contracts that implement the interface without being a +token, and narrows what the standard covers for them: + +> Compliance-related contracts (for example, a rule engine or compliance module) +> MAY implement the ERC-1404 restriction interface without implementing any +> token interface at all. In such cases, this standard only defines the behavior +> of `detectTransferRestriction` and `messageForTransferRestriction`, and the +> contract is expected to be consulted by a token or other caller that performs +> the actual transfer. + +A rule engine has no transfer path, so line 48 is vacuous for it. The token that +consults it is not bound by line 48 either, because the token is not the contract +implementing the interface. The clause that would close the gap — "the contract is +**expected** to be consulted by a token or other caller" — uses non-RFC-2119 +language. + +**Consequence:** as written, a token may call a compliance contract, receive a +non-zero code, ignore it, and no party is in violation of the standard. + +**Suggested addition** (§Additional Specifications): + +> Where the restriction interface is implemented by a contract that does not +> itself perform transfers, the enforcement-consistency requirement binds the +> token or caller that consults it: that caller MUST reject a transfer whenever +> the consulted contract returns a non-zero code for the same state and inputs, +> and MUST NOT reject it for restriction reasons when the consulted contract +> returns `0`. + +This also gives line 250's "verify it under test" advice something to attach to +across a contract boundary, which it currently cannot reach. + +--- + +## 2. Mint/burn — the zero-address encoding is load-bearing but never stated + +**Severity:** high. Mint and burn are the operations permissioned-token issuers +care most about, and they receive one MAY sentence (line 113). + +### 2a. The `address(0)` convention is unspecified + +Line 113 permits mint/burn checks and permits `spender` to carry the initiating +operator, but never states how `from` and `to` encode a mint or a burn. In +practice every implementation uses `from == address(0)` for mint and +`to == address(0)` for burn — and **policies branch on it**: + +```solidity +// src/mocks/rules/validation/RuleWhitelist.sol:86 +// Mint (from == address(0)) and burn (to == address(0)) are exempt from spender check +if (from != address(0) && to != address(0) && !addressIsListed(spender)) { + return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; +} +``` + +A convention that policies branch on, and that two implementations could +reasonably disagree about, belongs in the specification. + +The unstated convention also has a sharp edge. A whitelist policy evaluating +`detectTransferRestriction(address(0), to, value)` returns "sender not +whitelisted", because `address(0)` is not on the list. The workaround this +repository is forced into is to whitelist the zero address in order to permit +minting: + +```solidity +// test/RuleWhitelist/CMTATIntegrationBase.sol:270-272 +// Add address zero to the whitelist +vm.prank(DEFAULT_ADMIN_ADDRESS); +ruleWhitelist.addAddressToTheList(ZERO_ADDRESS); +``` + +One sentence in the spec would prevent this wart. + +### 2b. No enforcement-consistency requirement for mint/burn + +The MUST at line 88 is scoped to `transferFrom`. Nothing binds the mint/burn +case. An implementation may therefore report a mint restriction through +`detectTransferRestrictionFrom` and not enforce it, without violating the +standard — which defeats the core promise for precisely the operation issuers +most depend on. + +### 2c. No guidance on which predictor matches which entry point + +For a mint there are two plausible predictors: + +| Predictor | Sees the operator? | +|---|---| +| `detectTransferRestriction(address(0), to, value)` | no | +| `detectTransferRestrictionFrom(spender, address(0), to, value)` | yes | + +They legitimately disagree whenever the policy restricts the minter. This is not +hypothetical — the RuleEngine has two mint/burn enforcement paths, and only one +carries the operator: + +| Token calls | Engine path | Rule hook | Matching predictor | Operator visible? | +|---|---|---|---|---| +| `transferred(spender, 0, to, value)` (CMTAT ≥ v3.3.0) | 4-arg `_transferred` | `rule.transferred(spender,…)` | `detectTransferRestrictionFrom` | yes | +| `created(to, value)` / `destroyed(from, value)` (ERC-3643) | 3-arg `_transferred` | `rule.transferred(from,to,value)` | `detectTransferRestriction` | no | + +Each path is internally consistent, but they are not interchangeable: predicting +a `created()` mint with `detectTransferRestrictionFrom` over-reports a +restriction that path never enforces. The spec should state that the predictor +must match the entry point the token actually uses. + +**Recommendation:** promote line 113 from a single bullet to a short subsection +covering the `address(0)` encoding, an enforcement-consistency requirement +parallel to line 88, and predictor/entry-point correspondence. + +--- + +## 3. Nothing about aggregation, which is the dominant real architecture + +Line 117 acknowledges that rule engines exist, then stops. The common deployed +shape is token → engine → N independently-authored rules, but the spec's model +implicitly assumes a single policy owns the whole restriction-code space. Three +consequences go unaddressed: + +- **Code collision.** Line 123 advises allocating codes "carefully". This is not + actionable when rules are pluggable and third-party-authored, all sharing the + same 255 non-zero values. +- **Reverse lookup.** An aggregator's `messageForTransferRestriction` must map a + code back to the rule that owns it. This repository does so with a + `canReturnTransferRestrictionCode` probe and a first-claimant-wins scan + (`src/RuleEngineBase.sol`) — a mechanism the standard neither provides nor + acknowledges is necessary. +- **Ordering.** Which code surfaces when several rules reject? Line 156 concedes + that evaluation order is implementation-defined within one policy; across + policies, order should at least be REQUIRED to be deterministic, since a + non-deterministic aggregator breaks the very reporting/enforcement equivalence + the Security Considerations defend at length. + +--- + +## 4. Unknown-code behavior is unspecified + +Line 62 pins the reserved code `0`, and the Test Cases table (lines 174–175) +covers `0` and known codes. Nothing states what an *unknown* code must return. + +Reverting there is a real hazard for a UI iterating codes, and implementations +diverge in practice. Within this single stack, CMTAT's `ValidationModuleERC1404` +returns `"UnknownCode"` while `RuleEngineBase` returns +`"Unknown restriction code"`. + +**Suggested addition** (§`messageForTransferRestriction`): + +> For a code the implementation does not recognize, this SHOULD return a +> deterministic, non-empty human-readable string and SHOULD NOT revert. + +--- + +## 5. Move the "only `0` is reserved" statement into Specification + +The statement that the standard reserves only code `0` and leaves all other +values issuer-defined currently appears only at line 222, inside the Reference +Implementation section: + +> Note that codes `1` and `2` are specific to this implementation; ERC-1404 does +> not standardize restriction code values beyond reserving `0` as the "no +> restriction" sentinel. + +In that location it reads as a note about the example rather than a normative +statement about the standard. It belongs in §Specification or §Additional +Specifications. + +--- + +## 6. Test-case row for `detectTransferRestrictionFrom` needs the ordering caveat + +Line 168 states: + +| Scenario | Expected return | +|---|---| +| `from` or `to` violates the policy | The same non-zero code `detectTransferRestriction` returns for that condition | + +This lacks the caveat that line 156 carries for the base method. Row 167 already +establishes that a spender-specific code may take precedence, so row 168 should +be scoped: *when no spender-specific restriction applies*. As written the two +rows can be read as contradictory when both a spender restriction and a +`from`/`to` violation apply simultaneously. + +--- + +## 7. Editorial balance + +The `spender == from` case is argued at length in three separate places — +Specification (lines 89–91), Rationale (lines 137–139), and Security +Considerations (line 254). It is the most re-litigated point in the document, +while mint/burn receives a single sentence and aggregation receives none. +Consolidating the first would make room for items 2 and 3. + +--- + +## Summary + +| # | Item | Type | Severity | +|---|---|---|---| +| 1 | Enforcement MUST has no addressee for standalone compliance contracts | Normative hole | High | +| 2 | Mint/burn: `address(0)` encoding, consistency requirement, predictor correspondence | Normative gap | High | +| 3 | No aggregation semantics (code collision, reverse lookup, ordering) | Normative gap | Medium | +| 4 | Unknown-code behavior of `messageForTransferRestriction` unspecified | Normative gap | Medium | +| 5 | "Only `0` is reserved" stated only in the Reference Implementation section | Placement | Low | +| 6 | Test-case row 168 missing the evaluation-order caveat | Consistency | Low | +| 7 | `spender == from` argued three times; mint/burn once, aggregation never | Editorial | Low | + +**Priority:** items 1 and 2 are the ones worth acting on first — they are the two +places where an implementation can be fully conformant on paper and still +surprise an integrator. diff --git a/doc/ERCSpecification/rework/erc-1404.md b/doc/ERCSpecification/rework/erc-1404.md new file mode 100644 index 0000000..4e62dd2 --- /dev/null +++ b/doc/ERCSpecification/rework/erc-1404.md @@ -0,0 +1,266 @@ +--- +eip: 1404 +title: Simple Restricted Token +description: An interface for enforcing token transfer restrictions with machine-readable status codes. +author: Ron Gierlach (@rongierlach), James Poole (@pooleja), Mason Borda (@masonicgit), Lawson Baker (@lwsnbaker), Ryan Sauge (@rya-sge) +discussions-to: https://ethereum-magicians.org/t/erc-1404-simple-restricted-token-standard/1405 +status: Draft +type: Standards Track +category: ERC +created: 2018-07-27 +--- + +## Abstract + +Current 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 tokens to be compliant with securities laws and other contractual obligations. This is most commonly required for [ERC-20](./eip-20.md) tokens, but the same need applies to any token that moves a single amount between two accounts, such as [ERC-777](./eip-777.md). Current implementations do not address these requirements. + +A few 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, this standard provides 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. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +[ERC-1404](./eip-1404.md) defines a transfer-restriction interface consisting of the two methods below. It is designed to be applied to a fungible token whose transfer moves a single `uint256` amount from one address to another — canonically [ERC-20](./eip-20.md), and equally applicable to compatible token interfaces such as [ERC-777](./eip-777.md). + +A token claiming [ERC-1404](./eip-1404.md) compliance MUST implement the two methods below. It SHOULD preserve the function signatures and events of its underlying token interface (for example, [ERC-20](./eip-20.md)) and SHOULD NOT remove or alter them, so that integrators can continue to treat it as that interface; the restriction logic affects only whether a transfer succeeds, not the interface itself. + +### Methods + +- #### `detectTransferRestriction(address,address,uint256)` + + Returns a restriction code for the proposed transfer of `value` tokens from `from` to `to`, or `0` if the transfer is unrestricted. The restriction logic is defined by the issuer. + + Enforcement MUST be consistent with `detectTransferRestriction` for the same state and inputs: a transfer MUST be rejected whenever `detectTransferRestriction` would return a non-zero code for that transfer, and MUST NOT be rejected for restriction reasons when it would return `0`. + + - Implementations MAY satisfy this requirement by invoking `detectTransferRestriction` directly inside the token's transfer entry points (for [ERC-20](./eip-20.md), `transfer` and `transferFrom`; for other token interfaces, the analogous transfer methods, such as [ERC-777](./eip-777.md) `send`) and rejecting on a non-zero code. This is RECOMMENDED, because it guarantees the consistency above by construction. + - Implementations MAY instead re-evaluate the equivalent conditions in the transfer path — including reverting with typed errors (for example, [ERC-7943](./eip-7943.md)) rather than a restriction code — provided the observable outcome remains consistent with `detectTransferRestriction`. + - When a transfer is rejected, reverting is RECOMMENDED; implementations MAY return `false` instead, consistently with the underlying token's transfer expectations. + + ```solidity + function detectTransferRestriction(address from, address to, uint256 value) external view returns (uint8); + ``` + +- #### `messageForTransferRestriction(uint8)` + + Returns the human-readable message corresponding to `restrictionCode`. + + For the reserved code `0`, this SHOULD return a message denoting the absence of a restriction (for example, `"No restriction"`) and SHOULD NOT report `0` as an unknown or invalid code. + + ```solidity + function messageForTransferRestriction(uint8 restrictionCode) external view returns (string memory); + ``` + +### Extension: spender-aware restriction detection (OPTIONAL) + +`detectTransferRestriction(address,address,uint256)` describes a transfer solely in terms of `from`, `to`, and `value`. It has no `spender` parameter and therefore cannot express a restriction that depends on the party *initiating* a delegated transfer. + +For [ERC-20](./eip-20.md), `transferFrom` is executed by a `spender` that may differ from `from`; a policy that restricts the spender itself — for example a frozen operator, or an operator that has not passed its own AML/KYC check — is invisible to `detectTransferRestriction`. + +An integrator that predicts a `transferFrom` outcome by calling only `detectTransferRestriction` will therefore mispredict exactly these cases. + +An implementation MAY expose a spender-aware companion method: + +```solidity +function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) external view returns (uint8); +``` + +`detectTransferRestrictionFrom` returns a restriction code for the delegated transfer of `value` tokens from `from` to `to` initiated by `spender`, or `0` if the transfer is unrestricted. + +It shares the restriction code space of `detectTransferRestriction`, and its codes are looked up through the same `messageForTransferRestriction`. + +An implementation that exposes `detectTransferRestrictionFrom`: + +- MUST keep it consistent with delegated-transfer enforcement, in the same sense the base method is consistent with the transfer path: a `transferFrom(from, to, value)` executed by `spender` MUST be rejected whenever `detectTransferRestrictionFrom(spender, from, to, value)` would return a non-zero code, and MUST NOT be rejected for restriction reasons when it would return `0`. +- SHOULD evaluate the `spender == from` case through the spender-aware path, because `transferFrom` is a distinct delegated-transfer entry point whose authorization may depend on the initiating operator even when that operator equals `from`. + - An implementation MAY instead evaluate `spender == from` through the non-spender-aware path — equivalently, skip the spender-specific checks — as an optimization, but only when doing so is observably equivalent, i.e. its policy imposes no restriction on the initiating operator's identity beyond the `from`/`to`/`value` conditions. + - An implementation whose policy *does* restrict the operator identity (for example, an allow-listed or frozen operator set) MUST NOT skip, since skipping could return `0` (or an owner-only code) for a self-initiated `transferFrom` that enforcement rejects, violating the enforcement-consistency requirement above. When the policy does not distinguish operator identity from ownership, `detectTransferRestrictionFrom(from, from, to, value)` and `detectTransferRestriction(from, to, value)` return the same code, and skipping is always safe. + +- MUST NOT change the meaning of, or the enforcement consistency required of, `detectTransferRestriction`. The base method continues to describe the `from`/`to`/`value` conditions; the extension adds only the `spender` dimension, so a caller that does not care about the spender can keep using the base method unchanged. + +The extension is OPTIONAL and does not change the mandatory [ERC-1404](./eip-1404.md) interface identifier `0xab84a5c8`, which is the exclusive-or of the selectors of the two mandatory methods alone. + +The extension is advertised under a *second, separate* identifier, `0x78a8de7d`, which is the exclusive-or of the selectors of **all three** methods — `detectTransferRestriction`, `messageForTransferRestriction`, and `detectTransferRestrictionFrom`. + +An implementation that exposes `detectTransferRestrictionFrom` and supports [ERC-165](./eip-165.md): + +- MUST return `true` for `0xab84a5c8`, exactly as a non-extended implementation does — the mandatory interface is still fully present, so a base-only integrator continues to detect it. +- MUST return `true` for `0x78a8de7d`, so that an integrator can detect the spender-aware predictor by a single `supportsInterface` call rather than probing for it and interpreting a revert. + +The extension identifier deliberately covers all three selectors rather than `detectTransferRestrictionFrom` alone. + +- An identifier over the single added method would let a contract advertise the spender-aware predictor without asserting the mandatory pair it depends on; folding the two base selectors into the extension identifier keeps it self-contained, so `supportsInterface(0x78a8de7d) == true` is sufficient evidence that all three methods are present. +- Because it spans all three selectors, this identifier is their explicit exclusive-or, not that of the single added method alone; the [Reference Implementation](#reference-implementation) shows how it is computed and pins the value under test. + +### Additional Specifications + +- Implementations MAY add [ERC-165](./eip-165.md) interface detection support for [ERC-1404](./eip-1404.md). + +- Implementations MAY apply analogous restriction checks to token supply-changing operations (for example, mint and burn). Such checks are optional and are not required for [ERC-1404](./eip-1404.md) compliance. Where such checks are applied and the implementation exposes the optional spender-aware extension, the `spender` parameter of [`detectTransferRestrictionFrom`](#extension-spender-aware-restriction-detection-optional) MAY be used to pass the operator initiating the mint or burn, so that the policy can restrict that operator directly. + +- The [ERC-165](./eip-165.md) interface identifier for [ERC-1404](./eip-1404.md) is `0xab84a5c8` (the two mandatory methods). The identifier for the optional spender-aware extension is `0x78a8de7d` (all three methods, including `detectTransferRestrictionFrom`); an implementation exposing the extension advertises both, as described under [Extension: spender-aware restriction detection](#extension-spender-aware-restriction-detection-optional). + +- Compliance-related contracts (for example, a rule engine or compliance module) MAY implement the [ERC-1404](./eip-1404.md) restriction interface without implementing any token interface at all. In such cases, this standard only defines the behavior of `detectTransferRestriction` and `messageForTransferRestriction`, and the contract is expected to be consulted by a token or other caller that performs the actual transfer. + +- Restriction checks SHOULD be deterministic for the same state and inputs, so that the reporting and enforcement results remain consistent. Guidance on the data a restriction policy relies on is given in [Security Considerations](#security-considerations) and is non-normative. + +- The string returned by `messageForTransferRestriction` SHOULD NOT be treated as an authorization primitive. + +- Implementations SHOULD define and manage restriction code allocation carefully, because `uint8` limits the available code space to 256 values (`0` to `255`). + +## Rationale + +The standard proposes two functions on top of an underlying token interface (canonically [ERC-20](./eip-20.md)). The rationale for each is described below. + +1. `detectTransferRestriction` - This function encodes the restriction logic of an issuer's 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 to standardize the _result_ that transfer enforcement must agree with, rather than mandating a specific call site: an implementation may invoke it directly inside the transfer path or re-evaluate the equivalent conditions, as long as the outcome is consistent (see Specification). + - 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. +3. Optional [ERC-165](./eip-165.md) support - Implementations may expose [ERC-165](./eip-165.md) support for interface discovery. +4. Token-agnostic interface - Although [ERC-1404](./eip-1404.md) was originally designed as an [ERC-20](./eip-20.md) extension, its two methods only assume a transfer that moves a single `uint256` amount between two addresses. The interface is therefore reused unchanged with other compatible token interfaces such as [ERC-777](./eip-777.md), and can be implemented by a standalone compliance contract that is itself not a token. The standard deliberately does not require [ERC-20](./eip-20.md), so that it can describe the restriction behavior independently of the token it is attached to. +5. Optional spender-aware detection - The mandatory `detectTransferRestriction` is kept at three parameters (`from`, `to`, `value`) so that it stays token-agnostic and matches the original [ERC-1404](./eip-1404.md) signature that integrators already call. + - The `spender` dimension needed to predict a delegated transfer (`transferFrom`) is therefore split into an OPTIONAL extension rather than added to the base method, which would have broken its selector and its [ERC-165](./eip-165.md) identifier and imposed a parameter on the many policies that do not restrict the spender. Splitting it also lets an integrator detect, via a distinct [ERC-165](./eip-165.md) identifier, whether a given token can predict `transferFrom` outcomes at all — a token without the extension simply cannot, and the integrator learns this rather than silently mispredicting. The extension does not require `detectTransferRestrictionFrom(from, from, …)` to equal `detectTransferRestriction(from, …)`. + - A `transferFrom` initiated by the holder is still a delegated transfer — it flows through the allowance path and is a different entry point than a direct `transfer` — so a policy that restricts the initiating operator (even when the operator is the holder) is legitimate, and the spender-aware predictor must be free to describe it. The only invariant that matters for integrators is that `detectTransferRestrictionFrom` agree with `transferFrom` enforcement; requiring it to instead mirror the direct-transfer predictor would force it to misreport, and for operator-restricting policies would conflict with that agreement. + - Implementations whose policy does not distinguish operator identity from ownership will observe the two predictors coincide for `spender == from` and MAY skip the spender-aware evaluation as an optimization. + +## Backwards Compatibility + +By design [ERC-1404](./eip-1404.md) only adds new methods and leaves the underlying token interface untouched, so it is interface-compatible with [ERC-20](./eip-20.md) (and other compatible token interfaces such as [ERC-777](./eip-777.md)). Transfer restrictions may, however, introduce behavioral differences, because otherwise-valid transfers can be rejected. + +## Test Cases + +The following table-driven cases SHOULD be verified for every implementation. The examples use a whitelist-based policy (code `0` = no restriction, `1` = sender not whitelisted, `2` = recipient not whitelisted) to keep the expected values concrete, but the same structure applies to any issuer-defined policy. + +### `detectTransferRestriction` + +| Scenario | Expected return | +|---|---| +| Both `from` and `to` satisfy the policy | `0` | +| `from` violates the policy | Non-zero restriction code | +| `to` violates the policy; `from` does not | Non-zero restriction code distinct from the sender case | +| Both `from` and `to` violate the policy | Non-zero code; the specific code returned depends on the implementation's evaluation order | + +### `detectTransferRestrictionFrom` (optional extension) + +For implementations that expose the spender-aware extension: + +| Scenario | Expected return | +|---|---| +| `spender`, `from`, and `to` all satisfy the policy | `0` | +| `spender == from`, policy does **not** restrict operator identity | Equal to `detectTransferRestriction(from, to, value)` | +| `spender == from`, policy **does** restrict the initiating operator | Reflects the delegated-transfer policy for that operator, and is consistent with `transferFrom` enforcement; MAY differ from `detectTransferRestriction(from, to, value)` | +| A spender-specific restriction applies (e.g., `spender` is frozen); `from` and `to` satisfy the policy | Non-zero code. Note `detectTransferRestriction(from, to, value)` MAY still return `0` for the same `from`/`to`/`value`, because it cannot observe the spender — this divergence is expected, not a defect | +| `from` or `to` violates the policy | The same non-zero code `detectTransferRestriction` returns for that condition | + +### `messageForTransferRestriction` + +| Input | Expected output | +|---|---| +| `0` | A deterministic human-readable string indicating no restriction (e.g., `"No restriction"`) | +| Any known restriction code | The corresponding deterministic human-readable message | + +### Transfer enforcement + +| Scenario | Expected behavior for `transfer` and `transferFrom` | +|---|---| +| `detectTransferRestriction` returns `0` | Transfer succeeds | +| `detectTransferRestriction` returns a non-zero code | Transfer reverts (preferred) or returns `false` | + +For implementations that expose the optional extension, `transferFrom` enforcement is additionally checked against the spender-aware predictor: + +| Scenario | Expected behavior for `transferFrom` executed by `spender` | +|---|---| +| `detectTransferRestrictionFrom(spender, from, to, value)` returns `0` | `transferFrom` succeeds | +| `detectTransferRestrictionFrom(spender, from, to, value)` returns a non-zero code | `transferFrom` reverts (preferred) or returns `false` | + +### ERC-165 interface detection (optional) + +For implementations that expose [ERC-165](./eip-165.md) support: + +| Input to `supportsInterface` | Expected return | +|---|---| +| `0xab84a5c8` ([ERC-1404](./eip-1404.md) interface identifier) | `true` | +| `0x01ffc9a7` ([ERC-165](./eip-165.md) interface identifier) | `true` | +| Any unrecognized selector | `false` | + +For implementations that also expose the optional spender-aware extension: + +| Input to `supportsInterface` | Expected return | +|---|---| +| `0xab84a5c8` (mandatory [ERC-1404](./eip-1404.md) methods) | `true` | +| `0x78a8de7d` (extension: all three methods, including `detectTransferRestrictionFrom`) | `true` | +| `0x01ffc9a7` ([ERC-165](./eip-165.md) interface identifier) | `true` | +| Any unrecognized selector | `false` | + +A complete Foundry test suite covering all the cases above is available at [`test/ERC1404.t.sol`](../assets/eip-1404/test/ERC1404.t.sol). + +## Reference Implementation + +A complete reference implementation built with Foundry and OpenZeppelin Contracts v5 is provided in the assets folder: + +| File | Description | +|------|-------------| +| [`src/IERC1404.sol`](../assets/eip-1404/src/IERC1404.sol) | Interface — extends `IERC20` with the two [ERC-1404](./eip-1404.md) functions | +| [`src/ERC1404.sol`](../assets/eip-1404/src/ERC1404.sol) | Concrete implementation — whitelist-based, with [ERC-165](./eip-165.md) support | +| [`test/ERC1404.t.sol`](../assets/eip-1404/test/ERC1404.t.sol) | Foundry test suite covering all mandatory behaviors | + +The concrete implementation applies a whitelist policy and defines the following restriction codes. Note that codes `1` and `2` are specific to this implementation; [ERC-1404](./eip-1404.md) does not standardize restriction code values beyond reserving `0` as the "no restriction" sentinel. + +| Code | Constant | Message | +|------|----------|---------| +| `0` | `TRANSFER_OK` | `"No restriction"` | +| `1` | `SENDER_NOT_WHITELISTED` | `"Sender not whitelisted"` | +| `2` | `RECIPIENT_NOT_WHITELISTED` | `"Recipient not whitelisted"` | + +Notable design decisions in this implementation: + +- `transfer` and `transferFrom` revert with a typed `TransferRestricted(uint8 code, string message)` error on non-zero codes, rather than returning `false`. +- `detectTransferRestriction` checks the sender before the recipient, so callers can distinguish the two failure cases with a single view call before submitting a transaction. +- `supportsInterface(0xab84a5c8)` returns `true`, enabling on-chain interface discovery. +- Mint and burn operations apply analogous restriction checks, as permitted by the specification. +- Implementations that also conform to [ERC-7943](./eip-7943.md) will likely revert with that standard's typed errors rather than `TransferRestricted(uint8 code, string message)`. Those errors do not carry a restriction code or human-readable message. + +This example is provided for educational purposes only and has not been audited. Do not use in production without a thorough independent security review. + +## Security Considerations + +- Implementations are expected to encode policy in `detectTransferRestriction`, so mistakes in this logic can block valid transfers or allow restricted transfers. + +- Restriction checks that are not deterministic for the same state and inputs can create inconsistent behavior between the reporting and enforcement paths. + +- Because `detectTransferRestriction` is a `view` function, any result obtained off-chain before a transfer is a prediction: the relevant state may change before the transfer executes, so a transfer predicted to succeed may later be rejected (and vice-versa). Implementations and integrators MUST treat the on-chain enforcement at execution time as authoritative, not an earlier off-chain reading. + +- The following is informational guidance, not a conformance requirement. Because the policy in `detectTransferRestriction` is defined entirely by the issuer, its security depends on the data that policy reads. A policy that branches on values the transacting party can influence within the transfer transaction — for example a spot price read from an automated market maker, which a flash loan can move and restore in a single transaction — can be forced to evaluate as unrestricted at execution time, even though the check runs during the transfer. Issuers are encouraged to base restriction decisions on state they control (such as whitelist or frozen flags) rather than on inputs a counterparty can manipulate. + +- When an implementation enforces transfers by re-evaluating the restriction conditions separately rather than calling `detectTransferRestriction` directly (for example, to revert with typed errors such as [ERC-7943](./eip-7943.md)), the reporting function and the enforcement path become two codepaths that can drift apart. If they diverge, `detectTransferRestriction` may return `0` for a transfer that reverts, or a non-zero code for a transfer that succeeds, defeating the purpose of the machine-readable code for exchanges and user interfaces. Such implementations SHOULD treat the equivalence between `detectTransferRestriction` and the enforcement path as an invariant and verify it under test (for example, asserting over many inputs that a non-zero code holds if and only if the transfer is rejected). + +- `detectTransferRestriction` is blind to the spender of a delegated transfer. A `transferFrom` can be rejected for a spender-specific reason (for example a frozen operator) that `detectTransferRestriction(from, to, value)` cannot see, so it may return `0` for a `transferFrom` that then reverts. This is not a violation of the enforcement-consistency requirement, which is defined over the `from`/`to`/`value` conditions the base method describes; it is a limitation of the three-parameter signature. Integrators MUST NOT treat a `0` from `detectTransferRestriction` as a prediction that a `transferFrom` will succeed. Where spender-aware prediction is required, use the optional `detectTransferRestrictionFrom` extension if the token exposes it; a token that does not expose it cannot predict `transferFrom` outcomes on-chain, and integrators MUST fall back to submitting the transfer and handling the revert. + +- Because `transferFrom` is a delegated-transfer entry point, an implementation that restricts the initiating operator SHOULD evaluate `spender == from` through the spender-aware path rather than collapsing it to the direct-transfer predictor. Collapsing it (for example, an unconditional `if (spender == from) return detectTransferRestriction(from, to, value);` short-circuit) can make `detectTransferRestrictionFrom` return `0` for a self-initiated `transferFrom` that enforcement then rejects, reintroducing a reporting/enforcement divergence. The non-spender-aware evaluation is a safe optimization *only* for policies that do not restrict operator identity beyond ownership. + +- Returning machine-readable codes improves integration, but the string returned by `messageForTransferRestriction` remains informational and is not an authorization primitive. + +- Using `uint8` for restriction codes limits the available code space to 256 values (`0` to `255`), which can create ambiguity if code allocation is not managed carefully. + +- [ERC-1404](./eip-1404.md) interface support alone is not evidence that a contract implements a full token interface such as [ERC-20](./eip-20.md) or [ERC-777](./eip-777.md); a compliance-focused contract can expose the [ERC-1404](./eip-1404.md) restriction interface and interface support while implementing no token transfer behavior at all. Callers MUST NOT assume that a contract reporting [ERC-1404](./eip-1404.md) support is itself a transferable token. + +- The original [ERC-1404](./eip-1404.md) text did not require [ERC-165](./eip-165.md) signaling. Therefore, older implementations may still implement [ERC-1404](./eip-1404.md) while not returning `true` for the [ERC-165](./eip-165.md) interface identifier `0xab84a5c8`. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..6895182 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,1786 @@ +# RuleEngine + +This repository includes the RuleEngine contracts for [CMTAT](https://github.com/CMTA/CMTAT) and [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) tokens. + +The RuleEngine is an external contract used to apply transfer restrictions to another contract, such as CMTAT and ERC-3643 tokens. Acting as a controller, it can call different contract rules and apply these rules on each transfer. + +> This project has not undergone an audit and is provided as-is without any warranties. + +## Table of Contents + +- [Contract Variants](#contract-variants) +- [Motivation](#motivation) +- [How it works](#how-it-works) + - [How to set it](#how-to-set-it) + - [Making `setCompliance` work with RuleEngine](#making-setcompliance-work-with-ruleengine) +- [How to include it](#how-to-include-it) + - [Like CMTAT](#like-cmtat) + - [Like ERC-3643](#like-erc-3643) +- [Interface](#interface) + - [CMTAT](#cmtat) + - [ERC-3643](#erc-3643) +- [Technical](#technical) + - [Dependencies](#dependencies) + - [Access Control](#access-control) + - [ERC-165 Support by Deployment Version](#erc-165-support-by-deployment-version) + - [UML](#uml) + - [Graph](#graph) +- [Functionality](#functionality) + - [Available Rules](#available-rules) + - [Gasless support (ERC-2771)](#gasless-support-erc-2771) + - [Upgradeable](#upgradeable) + - [Urgency mechanism](#urgency-mechanism) +- [Ethereum API](#ethereum-api) + - [Contract Constructors](#contract-constructors) + - [RuleEngineBase](#ruleenginebase) + - [VersionModule](#versionmodule) + - [ERC3643ComplianceModule](#erc3643compliancemodule) + - [ERC3643ComplianceExtendedModule](#erc3643complianceextendedmodule) + - [RulesManagementModule](#rulesmanagementmodule) +- [Security](#security) + - [Vulnerability disclosure](#vulnerability-disclosure) + - [Audit](#audit) + - [Tools](#tools) +- [Documentation](#documentation) +- [Toolchains and Usage](#toolchains-and-usage) + - [Configuration](#configuration) + - [Toolchain installation](#toolchain-installation) + - [Initialization](#initialization) + - [Compilation](#compilation) + - [Contract size](#contract-size) + - [Testing](#testing) + - [Coverage](#coverage) + - [Deployment](#deployment) + - [Solidity style guideline](#solidity-style-guideline) +- [Intellectual property](#intellectual-property) + + +## Contract Variants + +Three deployable contracts are available: + +| Contract | Access Control | Interface | Use Case | +|----------|---------------|-----------|----------| +| `RuleEngine` | Role-Based (AccessControlEnumerable) | RBAC roles | Multi-operator environments with granular permissions | +| `RuleEngineOwnable` | ERC-173 Ownership | `Ownable` | Single-owner setups, simpler administration | +| `RuleEngineOwnable2Step` | ERC-173 Ownership (two-step transfer) | `Ownable2Step` | Single-owner setups with safer ownership handover | + +ERC-3643 compliance specification indicates the use of ERC-173. + +> The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of setting the Compliance parameters and binding the Compliance to a Token contract. + +All deployable contracts share the same core functionality (`RuleEngineBase`, directly or through `RuleEngineOwnableShared`) and support: + +- ERC-1404 transfer restrictions +- ERC-3643 compliance interface +- ERC-2771 meta-transactions (gasless) +- Multiple token bindings + +> **Warning (shared engine across multiple tokens):** A "multi-tenant" setup here means one RuleEngine instance is shared by several token contracts (all bound through `bindToken`). In this setup, tokens must be equally trusted and governed together. ERC-3643 callbacks (`transferred`, `created`, `destroyed`) do not pass the token address to rules, so stateful/accounting rules are not safe for mutually untrusted tokens sharing the same engine. + +## Motivation + +- Why use a dedicated contract with rules instead of implementing it directly in CMTAT or [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) tokens? + +There are several reasons to do this: + +- Flexibility: These different features are not standard and common to all tokens. From an implementation perspective, using a rule engine with custom rules allows for each issuer or contract user to decide which rules to apply. + +- Code efficiency: The CMTAT token (and generally also all ERC-3643 tokens) is currently "heavy," meaning its contract code size is close to the maximum limit. This makes it challenging to add new features directly inside the token contract. + +- Reusability: + + - The RuleEngine can be used inside other contracts besides CMTAT. For instance, the RuleEngine has been used in [our contract to distribute dividends](https://www.taurushq.com/blog/equity-tokenization-how-to-pay-dividend-on-chain-using-cmtat/). + + - A same deployed `RuleEngine` can also be used with several different tokens if the rules allow it, which is the case for all read-only rules. + +Why use this `RuleEngine` contract instead of setting the `rule` directly in the token contract? + +- Using a RuleEngine allows to call several different rules. For example, a blacklist rule to allow the issuer to manage its own list of blacklisted addresses and a sanctionlist rule to use the [Chainalysis oracle for sanctions screening](https://go.chainalysis.com/chainalysis-oracle-docs.html) to forbid transfers from addresses listed in sanctions designations published by organizations such as the US, EU, or UN. + +When may the use of `RuleEngine` not be appropriate? + +- If you plan to call only one rule (e.g a whitelist rule), it could make sense to directly set the rule in the token contract instead of using a RuleEngine. This will simplify configuration and reduce runtime gas costs. + +## How it works + +This diagram illustrates how a transfer with a CMTAT or ERC-3643 token with a RuleEngine works: + +![RuleEngine overview](./schema/plantuml/ruleengine-overview.png) + +_Diagram source: [doc/schema/plantuml/ruleengine-overview.puml](./schema/plantuml/ruleengine-overview.puml)._ + + + +1. The token holders initiate a transfer transaction on the token contract. +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). + +CMTAT and ERC-3643 tokens use **disjoint entry points** on the RuleEngine, so each has its own sequence diagram. The 4-argument `transferred(spender, from, to, value)` is declared by CMTAT's `IRuleEngine` and is never reached by an ERC-3643 token; conversely `created` / `destroyed` are declared by `IERC3643Compliance` and are never called by CMTAT. Only the 3-argument `transferred(from, to, value)` is shared. + +#### With a CMTAT token + +![RuleEngine flow with a CMTAT token](./schema/plantuml/ruleengine-flow-cmtat.png) + +_Diagram source: [doc/schema/plantuml/ruleengine-flow-cmtat.puml](./schema/plantuml/ruleengine-flow-cmtat.puml)._ + +#### With an ERC-3643 token + +![RuleEngine flow with an ERC-3643 token](./schema/plantuml/ruleengine-flow-erc3643.png) + +_Diagram source: [doc/schema/plantuml/ruleengine-flow-erc3643.puml](./schema/plantuml/ruleengine-flow-erc3643.puml)._ + +> **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. + +### How to set it + +#### Compatibility + +| RuleEngine version | Compatible Versions | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| **v3.0.0-rc4** | CMTAT ≥ v3.0.0
CMTAT target version: [v3.3.0-rc1](https://github.com/CMTA/CMTAT/releases/tag/v3.3.0-rc1) | +| **v3.0.0-rc3** | CMTAT ≥ v3.0.0
CMTAT target version: [v3.3.0](https://github.com/CMTA/CMTAT/releases/tag/v3.3.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) | +| **[v1.0.2.1](https://github.com/CMTA/RuleEngine/releases/tag/v1.0.2.1)** | CMTAT v2.3.0 (audited) | + +#### CMTAT v3.0.0 + +CMTAT provides the following function to set a RuleEngine inside a CMTAT token: + +```solidity +setRuleEngine(IRuleEngine ruleEngine_) +``` + +This function is defined in the extension module `ValidationModuleRuleEngine` + +#### ERC-3643 token + +[ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) defined the following function in the standard interface to set a compliance contract + +```solidity +setCompliance(address _compliance) +``` + +### Making `setCompliance` work with RuleEngine + +RuleEngine supports the ERC-3643/T-REX pattern where the token contract binds and unbinds itself when `setCompliance` is called. + +In other words, a token can call: + +- `bindToken(address(this))` +- `unbindToken(address(this))` + +To keep this feature secure, self-bind/self-unbind is gated: + +- A token can call `bindToken(address(this))` and `unbindToken(address(this))` only if it was explicitly approved first. +- Approval is set by governance/compliance admin using: + - `setTokenSelfBindingApproval(address token, bool approved)` +- Approval status can be checked with: + - `isTokenSelfBindingApproved(address token)` + +This preserves compatibility with ERC-3643 tokens that do: + +```solidity +if (address(_tokenCompliance) != address(0)) { + _tokenCompliance.unbindToken(address(this)); +} +_tokenCompliance = IModularCompliance(_compliance); +_tokenCompliance.bindToken(address(this)); +``` + +while preventing arbitrary third-party contracts from self-binding. + +Recommended operational sequence: + +1. On the target RuleEngine, grant self-binding approval for the token. +2. Call token `setCompliance(newRuleEngine)`. +3. (Optional) Revoke self-binding approval after migration if no longer needed. + + + +## How to include it + +While the RuleEngine has been designed for CMTAT and ERC-3643 tokens, it can be used with other contracts to apply transfer restrictions. + +For that, the only thing to do is to import in your contract the interface `IRuleEngine`(CMTAT) or `IERC3643Compliance` (ERC-3643), which declares the corresponding functions to call by the token contract. This interface can be found [here](https://github.com/CMTA/CMTAT/blob/23a1e59f913d079d0c09d32fafbd95ab2d426093/contracts/interfaces/engine/IRuleEngine.sol). +If you need non-standard helper functions (batch bind/unbind, self-binding approval APIs, multi-token getter), use `IERC3643ComplianceExtended`. + +### Like CMTAT + +Before each ERC-20 transfer, mint, or burn, CMTAT calls the RuleEngine through the internal function `_checkTransferred`, which dispatches to one of two `transferred` overloads depending on whether a non-zero spender is present. + +```solidity +// Called when spender == address(0) +function transferred(address from, address to, uint256 value) + +// Called when spender != address(0) +function transferred(address spender, address from, address to, uint256 value) +``` + +#### CMTAT v3.3.0 — mint and burn use the spender path + +Since CMTAT v3.3.0, **mint and burn operations also go through the 4-argument overload**, with the operator (minter or burner) passed as `spender`: + +| Operation | `spender` | `from` | `to` | +|-----------|-----------|--------|------| +| `transfer` / `transferFrom` | caller / approved spender | token holder | recipient | +| `mint` | minter (`_msgSender()`) | `address(0)` | recipient | +| `burn` | burner (`_msgSender()`) | token holder | `address(0)` | + +The 3-argument overload is only called when `spender == address(0)`, which does not occur in normal CMTAT v3.3.0 flows. + +> **Rule authoring note:** Rules that check the `spender` argument in `transferred(spender, from, to, value)` must explicitly handle the mint (`from == address(0)`) and burn (`to == address(0)`) cases. A spender check that is intended only for `transferFrom` will also fire for mints and burns unless the rule skips it when `from` or `to` is the zero address. See `RuleWhitelist` and `RuleSpenderWhitelist` in the Rules repository for reference implementations. + +For example, CMTAT defines the interaction with the RuleEngine inside a specific module, [ValidationModuleRuleEngine](https://github.com/CMTA/CMTAT/blob/master/contracts/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol) and [CMTATBaseRuleEngine](https://github.com/CMTA/CMTAT/blob/master/contracts/modules/1_CMTATBaseRuleEngine.sol). + +- ValidationModuleRuleEngine + +![transferred](./other/CMTAT/transferred.png) + +- CMTATBaseRuleEngine + +![checkTransferred](./other/CMTAT/checkTransferred.png) + +This function `_transferred` is called before each transfer/burn/mint through the internal function `_checkTransferred`. + +### Like ERC-3643 + +The ERC-3643 defines several functions used as entrypoints for an ERC-3643 token. + +As for CMTAT, the main entrypoint is `transferred` which must be called for each ERC-20 transfer. + +Contrary to CMTAT, ERC-3643 does not apply restriction on the spender address (`transferFrom`). + +They are the following: + +```solidity +// read-only function +function canTransfer(address from, address to, uint256 value) external view returns (bool); +// ERC-20 transfer +function transferred(address from, address to, uint256 value) external; +// mint +function created(address to, uint256 value) external; +// burn +function destroyed(address from, uint256 value) external; +``` + +## Interface + +### CMTAT + +The `RuleEngine` base interface is defined in the CMTAT repository. + +![cmtat_surya_inheritance_IRuleEngine.sol](./schema/cmtat_surya_inheritance_IRuleEngine.sol.png) + +It inherits from several others interfaces: `IERC1404`, `IERC1404Extend`, `IERC7551Compliance`, `IERC3643ComplianceContract` + +```solidity +// IRuleEngine +function transferred(address spender, address from, address to, uint256 value) +external; + +// IERC-1404 +function detectTransferRestriction(address from,address to,uint256 value) +external view returns (uint8); + +function messageForTransferRestriction(uint8 restrictionCode) +external view returns (string memory); + +// IERC-1404Extend +enum REJECTED_CODE_BASE { + TRANSFER_OK, + TRANSFER_REJECTED_DEACTIVATED, + TRANSFER_REJECTED_PAUSED, + TRANSFER_REJECTED_FROM_FROZEN, + TRANSFER_REJECTED_TO_FROZEN, + TRANSFER_REJECTED_SPENDER_FROZEN, + TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE + } + +function detectTransferRestrictionFrom(address spender,address from,address to,uint256 value) +external view returns (uint8); + + +// IERC7551Compliance +function canTransferFrom(address spender,address from,address to,uint256 value) +external view returns (bool); + + +// IER3643ComplianceRead +function canTransfer(address from,address to,uint256 value) +external view returns (bool isValid); + +// IERC3643IComplianceContract +function transferred(address from, address to, uint256 value) +external; +``` + +> Note: `IERC7551Compliance` comes from `draft-IERC7551` (not final) and, in this project, is used as a subset compliance interface focused on `canTransferFrom`. + + + +### ERC-3643 + +The [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) compliance interface is defined in [IERC3643Compliance.sol](../src/interfaces/IERC3643Compliance.sol). +Non-standard helper functions are defined in [IERC3643ComplianceExtended.sol](../src/interfaces/IERC3643ComplianceExtended.sol). + +The RuleEngine modules are split as follows: +- Base ERC-3643 surface: [ERC3643ComplianceModule.sol](../src/modules/ERC3643ComplianceModule.sol) +- Non-standard extensions: [ERC3643ComplianceExtendedModule.sol](../src/modules/ERC3643ComplianceExtendedModule.sol) + +![ERC3643ComplianceModuleUML](./schema/vscode-uml/ERC3643ComplianceModuleUML.png) + +## Technical + +### Dependencies + +The toolchain includes the following components, where the versions are the latest ones that we tested: + +- Foundry (forge-std) [v1.14.0](https://github.com/foundry-rs/forge-std/releases/tag/v1.14.0) +- Solidity [0.8.36](https://docs.soliditylang.org/en/v0.8.36/) +- OpenZeppelin Contracts (submodule) [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.6.1) +- CMTAT [v3.3.0](https://github.com/CMTA/CMTAT/releases/tag/v3.3.0) + +### Access Control + +Two access control mechanisms are available depending on which contract you deploy: + +#### RuleEngine (RBAC - AccessControlEnumerable) + +The `RuleEngine` contract uses Role-Based Access Control (RBAC) via OpenZeppelin's `AccessControlEnumerable`. + +Each module defines the roles useful to restrict its functions. The contract overrides the OpenZeppelin function `hasRole` to give by default all the roles to the `admin`. +`RulesManagementModule` itself is access-control agnostic; RBAC is wired at the concrete `RuleEngine` level. +Note: this `hasRole` override does not add the admin address to each role's enumerable member set. As a result, `getRoleMember` / `getRoleMemberCount` for a specific role do not include the admin unless that role is explicitly granted. + +See also [docs.openzeppelin.com - AccessControlEnumerable](https://docs.openzeppelin.com/contracts/5.x/api/access#AccessControlEnumerable) + +#### RuleEngineOwnable (ERC-173 Ownership) + +The `RuleEngineOwnable` contract uses [ERC-173](https://eips.ethereum.org/EIPS/eip-173) ownership via OpenZeppelin's `Ownable`. + +All protected functions require the caller to be the contract owner. The owner can: +- Transfer ownership to another address via `transferOwnership(address)` +- Renounce ownership via `renounceOwnership()` (makes the contract ownerless) + +This is a simpler access control model suitable for single-owner deployments. + +See also [docs.openzeppelin.com - Ownable](https://docs.openzeppelin.com/contracts/5.x/api/access#Ownable) + +#### RuleEngineOwnable2Step (ERC-173 Ownership, two-step transfer) + +The `RuleEngineOwnable2Step` contract uses OpenZeppelin's `Ownable2Step`, which keeps the same owner-only protections and adds safer ownership handover with `transferOwnership(address)` + `acceptOwnership()`. + +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` | | | | +| `IRuleEngine` | `0x20c49ce7` | | | | +| `IERC1404` | `0xab84a5c8` | | | | +| `IERC1404Extend` | `0x78a8de7d` | | | | +| `IERC3643Compliance` | `0x3144991c` | | | | +| `IERC7551Compliance` (subset) | `0x7157797f` | | | | +| `IERC173` | `0x7f5828d0` | | | | +| `Ownable2Step` specific (`pendingOwner()`, `acceptOwnership()`) | `0x9ab669ef` | | | | +| `IAccessControl` | `0x7965db0b` | | | | +| `IAccessControlEnumerable` | `0x5a05180f` | | | | + +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. + +The default admin is the address put in argument (`admin`) inside the constructor. + +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 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 | +| ----------------------- | -------------------------------- | ------------------------------------------------------------ | +| DEFAULT_ADMIN_ROLE | OpenZeppelin
AccessControl | 0x0000000000000000000000000000000000000000000000000000000000000000 | +| **Modules** | | | +| COMPLIANCE_MANAGER_ROLE | ERC3643ComplianceModule | 0xe5c50d0927e06141e032cb9a67e1d7092dc85c0b0825191f7e1cede600028568 | +| RULES_MANAGEMENT_ROLE | RulesManagementModuleInvariantStorage | 0xea5f4eb72290e50c32abd6c23e45de3d8300b3286e1cbc2e293114b92e034e5e | + + + +#### Schema (RuleEngine) + +Here is a schema of the Access Control for `RuleEngine`. +![alt text](./security/accessControl/access-control-RuleEngine.png) + +#### Role by modules (RuleEngine) + +Here is a summary table for each restricted function defined in a module. +For function signatures, struct arguments are represented with their corresponding native type. + +> Note: For `RuleEngineOwnable` and `RuleEngineOwnable2Step`, replace the role requirement with `onlyOwner` for all protected functions. + +| | Function signature | Visibility [public/external] | Input variables (Function arguments) | Output variables
(return value) | Role Required | +| -------------------- | ------------------ | ---------------------------- | ------------------------------------ | ------------------------------------ | ------------- | +| **Modules** | | | | | | +| RulesManagementModule | | | | | | +| | `setRules(address[] rules_)` | public | `IRule[] rules_` | - | RULES_MANAGEMENT_ROLE | +| | `clearRules()` | public | - |-|RULES_MANAGEMENT_ROLE| +| | `addRule(address rule_)` | public | `IRule rule_` |-|RULES_MANAGEMENT_ROLE| +| | `removeRule(address rule_)` | public | `IRule rule_` |-|RULES_MANAGEMENT_ROLE| +| ERC3643ComplianceModule | | | | | | +| | `bindToken(address token)` | public | `address token` | - | COMPLIANCE_MANAGER_ROLE or approved token self-call | +| | `unbindToken(address token)` | public | `address token` | - | COMPLIANCE_MANAGER_ROLE or approved token self-call | +| ERC3643ComplianceExtendedModule | | | | | | +| | `bindTokens(address[] tokens)` | public | `address[] tokens` | - | COMPLIANCE_MANAGER_ROLE | +| | `unbindTokens(address[] tokens)` | public | `address[] tokens` | - | COMPLIANCE_MANAGER_ROLE | +| | `setTokenSelfBindingApproval(address token,bool approved)` | public | `address token,bool approved` | - | COMPLIANCE_MANAGER_ROLE | +| | `setTokenSelfBindingApprovalBatch(address[] tokens,bool approved)` | public | `address[] tokens,bool approved` | - | COMPLIANCE_MANAGER_ROLE | +| RuleEngineBase | | | | | | +| | `transferred(address from,address to,uint256 value)` | public | `address from,address to, uint256 value` | - | onlyBoundToken (modifier) | +| | `transferred(address spender,address from,address to,uint256 value)` | public | `address spender,address from,address to, uint256 value` | - | onlyBoundToken (modifier) | + + + +### UML + +Here is the UML of the main contracts: + +#### RuleEngine +![RuleEngineUML](./schema/vscode-uml/RuleEngineUML.png) + +#### RuleEngineOwnable + +![RuleEngineOwnableUML](./schema/vscode-uml/RuleEngineOwnableUML.png) + +`RuleEngineOwnable` shares the same base functionality as `RuleEngine` but uses ERC-173 ownership instead of RBAC. + +``` +RuleEngineOwnable +├── ERC2771ModuleStandalone (gasless support) +├── RuleEngineBase (core functionality) +│ ├── VersionModule +│ ├── RulesManagementModule +│ ├── ERC3643ComplianceModule (core ERC-3643) +│ ├── ERC3643ComplianceExtendedModule (project extensions) +│ └── IRuleEngineERC1404 +└── Ownable (ERC-173 access control) +``` + +**Key differences from RuleEngine:** +- Constructor takes `owner_` instead of `admin` +- All protected functions use `onlyOwner` modifier +- Supports `transferOwnership()` and `renounceOwnership()` +- Implements ERC-173 interface (`supportsInterface(0x7f5828d0)` returns `true`) + +#### RuleEngineOwnable2Step + +![RuleEngineOwnable2StepUML](./schema/vscode-uml/RuleEngineOwnable2StepUML.png) + +`RuleEngineOwnable2Step` shares the same base functionality as `RuleEngineOwnable` but uses OpenZeppelin's `Ownable2Step` for safer ownership handover. + +``` +RuleEngineOwnable2Step +├── ERC2771ModuleStandalone (gasless support) +├── RuleEngineOwnableShared (shared ownable deployment logic) +│ └── RuleEngineBase +│ ├── VersionModule +│ ├── RulesManagementModule +│ ├── ERC3643ComplianceModule (core ERC-3643) +│ ├── ERC3643ComplianceExtendedModule (project extensions) +│ └── IRuleEngineERC1404 +└── Ownable2Step (ERC-173 access control with pending owner) +``` + +**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 (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()` + + + + + +### Graph + +Here is the surya graph of the main contract: + +![surya_graph_RuleEngine](./schema/surya/surya_graph/surya_graph_RuleEngine.sol.png) + +## Functionality + +Several functionalities are not implemented because it makes more sense to directly implement them in the token smart contract + +The RuleEngine can be removed from the main token contract by calling these dedicated functions + +- CMTAT v3.0.0: `setRuleEngine(address ruleEngine)` +- ERC-3643 token: `setCompliance(address _compliance)` + +### Available Rules + +Rules are maintained in a dedicated repository: [github.com/CMTA/Rules](https://github.com/CMTA/Rules). + +Rules can be used in two ways: + +- Directly on CMTAT (single-rule setup, no RuleEngine orchestration). +- Through this RuleEngine (multi-rule orchestration with sequential execution). + +Rule families: + +| Family | Behavior | Examples | +| --- | --- | --- | +| Validation rules (read-only) | Evaluate transfer eligibility without mutating rule state | `RuleWhitelist`, `RuleBlacklist`, `RuleSanctionList`, `RuleIdentityRegistry`, `RuleSpenderWhitelist`, `RuleERC2980`, `RuleMaxTotalSupply` | +| Operation rules (read-write) | Evaluate transfer eligibility and can update rule-specific state on transfer | `RuleConditionalTransferLight` | + +Additional integration notes: + +- For RuleEngine integration, a rule must implement `IRule` (including ERC-165 support for the Rule interface ID). +- RuleEngine executes configured rules in order and reverts on the first failing rule in state-changing paths. +- Restriction codes should remain unique across the composed rule set. Keep CMTAT-reserved ranges free and use dedicated code ranges per rule +- For the latest list of production rules, audits, and status, use the Rules repository as the source of truth. + +#### Rules details + +Here is a summary tab of available rules, see [github.com/CMTA/Rules](https://github.com/CMTA/Rules) + +| Rule | Type
[read-only / read-write] | Description | +| ------------------------------------------------------------ | ----------------------------------- | ------------------------------------------------------------ | +| RuleWhitelist | Read-only | This rule can be used to restrict transfers from/to only addresses inside a whitelist. | +| RuleWhitelistWrapper | Read-Only | This rule can be used to restrict transfers from/to only addresses inside a group of whitelist rules managed by different operators. | +| RuleBlacklist | Read-Only | This rule can be used to forbid transfer from/to addresses in the blacklist | +| RuleSanctionList | Read-Only | The purpose of this contract is to use the oracle contract from [Chainalysis](https://go.chainalysis.com/chainalysis-oracle-docs.html) to forbid transfer from/to an address included in a sanctions designation (US, EU, or UN). | +| RuleMaxTotalSupply | Read-Only | This rule limits minting so that the total supply never exceeds a configured maximum. | +| RuleIdentityRegistry | Read-Only | This rule checks the ERC-3643 Identity Registry for transfer participants when configured. | +| RuleSpenderWhitelist | Read-Only | This rule blocks `transferFrom` when the spender is not in the whitelist. Direct transfers are always allowed. | +| RuleERC2980 | Read-Only | ERC-2980 Swiss Compliant rule combining a whitelist (recipient-only) and a frozenlist (blocks sender, recipient, and spender for `transferFrom`). Frozenlist takes priority over whitelist. | +| RuleConditionalTransferLight | Read-Write | This rule requires that transfers have to be approved by an operator before being executed. Each approval is consumed once and the same transfer can be approved multiple times. | +| [RuleConditionalTransfer](https://github.com/CMTA/RuleConditionalTransfer) (external) | Read-Write | Full-featured approval-based transfer rule implementing Swiss law *Vinkulierung*. Supports automatic approval after three months, automatic transfer execution, and a conditional whitelist for address pairs that bypass approval. Maintained in a separate repository. | +| [RuleSelf](https://github.com/rya-sge/ruleself) (community) | — | Use [Self](https://self.xyz), a zero-knowledge identity solution to determine which is allowed to interact with the token.
Community-maintained rule project. Not developed or maintained by CMTA. | + +### Gasless support (ERC-2771) + +![surya_inheritance_ERC2771ModuleStandalone.sol](./schema/surya/surya_inheritance/surya_inheritance_ERC2771ModuleStandalone.sol.png) + +The RuleEngine supports client-side gasless transactions using the standard [ERC-2771](https://eips.ethereum.org/EIPS/eip-2771). + +The contract uses the OpenZeppelin contract `ERC2771ContextUpgradeable`, which allows a contract to get the original client with `_msgSender()` instead of the feepayer given by `msg.sender`. + +At deployment, the parameter `forwarder` inside the RuleEngine contract constructor has to be set with the defined address of the forwarder. + +After deployment, the forwarder is immutable and can not be changed. + +References: + +- [OpenZeppelin Meta Transactions](https://docs.openzeppelin.com/contracts/5.x/api/metatx) + +- OpenGSN has deployed several forwarders, see their [documentation](https://docs.opengsn.org/contracts/#receiving-a-relayed-call) for examples. + +### Upgradeable + +A proxy architecture (upgradeable) increases the code complexity as well as the runtime gas cost for each transaction. This is why the RuleEngine is not upgradeable. + +Moreover, in a proxy architecture, each new implementation must be compatible (storage) with the precedent implementation, which can reduce the ability to improve the code. + +In case you use the same RuleEngine for several different tokens, unfortunately, you will have to update the address of the RuleEngine set in each token contract separately. + +### Urgency mechanism + +#### Pause + +There are no functionalities to put the RuleEngine in pause . + +The RuleEngine can be removed from the main token contract by calling the dedicated functions to manage the RuleEngine + +#### Kill / Deactivate the contracts + +There are no functionalities to kill/deactivate the contracts. + +Similar to the pause functionality, the RuleEngine can be directly removed from the main token contract. + +## Ethereum API + +### Contract Constructors + +#### RuleEngine Constructor + +```solidity +constructor( + address admin, + address forwarderIrrevocable, + address tokenContract +) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| admin | address | Address granted DEFAULT_ADMIN_ROLE (has all roles) | +| forwarderIrrevocable | address | ERC-2771 trusted forwarder address (can be zero) | +| tokenContract | address | Token to bind at deployment (can be zero) | + +#### RuleEngineOwnable Constructor + +```solidity +constructor( + address owner_, + address forwarderIrrevocable, + address tokenContract +) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| owner_ | address | Address set as contract owner (ERC-173) | +| forwarderIrrevocable | address | ERC-2771 trusted forwarder address (can be zero) | +| tokenContract | address | Token to bind at deployment (can be zero) | + +### RuleEngineBase + +![RuleEngineBaseUML](./schema/vscode-uml/RuleEngineBaseUML.png) + +#### Contracts Description Table + + +| Contract | Type | Bases | | | +| :----------------: | :---------------------------: | :----------------------------------------------------------: | :------------: | :------------: | +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +| | | | | | +| **RuleEngineBase** | Implementation | VersionModule, RulesManagementModule, ERC3643ComplianceExtendedModule, RuleEngineInvariantStorage, IRuleEngine | | | +| └ | transferred | Public ❗️ | 🛑 | onlyBoundToken | +| └ | transferred | Public ❗️ | 🛑 | onlyBoundToken | +| └ | created | Public ❗️ | 🛑 | onlyBoundToken | +| └ | destroyed | Public ❗️ | 🛑 | onlyBoundToken | +| └ | detectTransferRestriction | Public ❗️ | | NO❗️ | +| └ | detectTransferRestrictionFrom | Public ❗️ | | NO❗️ | +| └ | canTransfer | Public ❗️ | | NO❗️ | +| └ | canTransferFrom | Public ❗️ | | NO❗️ | +| └ | messageForTransferRestriction | Public ❗️ | | NO❗️ | +| └ | hasRole | Public ❗️ | | NO❗️ | + + +##### Legend + +| Symbol | Meaning | +| :----: | ------------------------- | +| 🛑 | Function can modify state | +| 💵 | Function is payable | + +#### IRuleEngine + +![IRuleEngineUML](./schema/vscode-uml/IRuleEngineUML.png) + +##### transferred(address spender, address from, address to, uint256 value) + +```solidity +function transferred(address spender,address from,address to,uint256 value) +public virtual override(IRuleEngine) +onlyBoundToken +``` + +Function called whenever tokens are transferred from one wallet to another. + +Must revert if the transfer is invalid. + Same name as ERC-3643 but with an additional `spender` parameter. + This function can be used to update state variables of the RuleEngine contract. + Can only be called by the token contract bound to the RuleEngine. + +**Input Parameters:** + +| Name | Type | Description | +| ------- | ------- | ---------------------------------------------- | +| spender | address | The spender address initiating the transfer. | +| from | address | The token holder address. | +| to | address | The receiver address. | +| value | uint256 | The amount of tokens involved in the transfer. | + +#### IERC7551Compliance + +![IERC7551ComplianceUML](./schema/vscode-uml/IERC7551ComplianceUML.png) + +> Note: ERC-7551 is draft (not final). The `IERC7551Compliance` interface used here is a subset interface exposing the compliance check `canTransferFrom`. + +##### canTransferFrom(address spender, address from, address to, uint256 value) -> bool + +Checks if `spender` can transfer `value` tokens from `from` to `to` under compliance rules. + +Does not check balances or access rights (Access Control). + +**Input Parameters:** + +| Name | Type | Description | +| ------- | ------- | ------------------------------------ | +| spender | address | The address performing the transfer. | +| from | address | The source address. | +| to | address | The destination address. | +| value | uint256 | The number of tokens to transfer. | + + + +**Return Values:** + +| Type | Description | +| ---- | ------------------------------------------ | +| bool | True if the transfer complies with policy. | + +#### IERC3643ComplianceRead + +![IERC3643ComplianceReadUML](./schema/vscode-uml/IERC3643ComplianceReadUML.png) + +------ + +##### canTransfer(address from, address to, uint256 value) -> bool + +Returns true if the transfer is valid, and false otherwise. + +Does not check balances or access rights (Access Control). + +> **Warning (spender-dependent rules fail open here):** this signature carries no `spender`. A rule whose +> decision depends on the spender — for example a per-minter mint allowance — cannot evaluate the operation +> on this path and must answer "no restriction". The RuleEngine aggregates that answer, so `canTransfer` and +> `detectTransferRestriction` can report a mint as allowed that the state-changing +> `transferred(spender, from, to, value)` will revert. To pre-check an operation that has an operator, use +> the 4-argument `canTransferFrom` / `detectTransferRestrictionFrom`. + +**Input Parameters:** + +| Name | Type | Description | +| ----- | ------- | --------------------------------- | +| from | address | The source address. | +| to | address | The destination address. | +| value | uint256 | The number of tokens to transfer. | + + + +**Return Values:** + +| Type | Description | +| ---- | ----------------------------------------------- | +| bool | True if the transfer is valid, false otherwise. | + +#### IERC3643IComplianceContract + +![IERC3643IComplianceContractUML](./schema/vscode-uml/IERC3643IComplianceContractUML.png) + +------ + +##### transferred(address from, address to, uint256 value) + +```solidity +function transferred(address from,address to,uint256 value) +public virtual override(IERC3643IComplianceContract) +onlyBoundToken +``` + +Updates the compliance contract state whenever tokens are transferred. + +Can only be called by the token contract bound to this compliance logic. + This function can be used to update internal state variables. + +**Input Parameters:** + +| Name | Type | Description | +| ----- | ------- | ---------------------------------------------- | +| from | address | The address of the sender. | +| to | address | The address of the receiver. | +| value | uint256 | The number of tokens involved in the transfer. | + + + +#### IERC3643Compliance + +------ + +##### created(address to, uint256 value) + +```solidity +function created(address to, uint256 value) +public virtual override(IERC3643Compliance) +onlyBoundToken +``` + +Updates the compliance contract state when tokens are created (minted). + +Called by the token contract when new tokens are issued to an account. + Reverts if the minting does not comply with the rules. + +**Input Parameters:** + +| Name | Type | Description | +| ----- | ------- | ---------------------------------------- | +| to | address | The address receiving the minted tokens. | +| value | uint256 | The number of tokens created. | + + + +------ + +##### destroyed(address from, uint256 value) + +```solidity +function destroyed(address from, uint256 value) +public virtual override(IERC3643Compliance) +onlyBoundToken +``` + +Updates the compliance contract state when tokens are destroyed (burned). + +Called by the token contract when tokens are redeemed or burned. + Reverts if the burning does not comply with the rules. + +**Input Parameters:** + +| Name | Type | Description | +| ----- | ------- | --------------------------------------------- | +| from | address | The address whose tokens are being destroyed. | +| value | uint256 | The number of tokens destroyed. | + + + +#### IERC1404 + +![IERC1404UML](./schema/vscode-uml/IERC1404UML.png) + +------ + +##### detectTransferRestriction(address from, address to, uint256 value) -> uint8 + +Returns a uint8 code to indicate if a transfer is restricted or not. + +Implements the restriction logic of {ERC-1404}. + Examples of restriction logic include: + +- checking if the recipient is whitelisted, +- checking if the sender’s tokens are frozen during a lock-up period, etc. + +**Input Parameters:** + +| Name | Type | Description | +| ----- | ------- | --------------------------------- | +| from | address | The source address. | +| to | address | The destination address. | +| value | uint256 | The number of tokens to transfer. | + + + +**Return Values:** + +| Type | Description | +| ----- | ------------------------------------------------------ | +| uint8 | Restriction code (0 means the transfer is authorized). | + + + +------ + +##### messageForTransferRestriction(uint8 restrictionCode) -> string + +Returns a human-readable explanation for a transfer restriction code. + +Implements {ERC-1404} standard message accessor. + +**Input Parameters:** + +| Name | Type | Description | +| --------------- | ----- | ---------------------------------- | +| restrictionCode | uint8 | The restriction code to interpret. | + + + +**Return Values:** + +| Type | Description | +| ------ | ---------------------------------------------------- | +| string | A message describing why the transfer is restricted. | + + + +------ + +#### IERC1404Extend + +![IERC1404ExtendUML](./schema/vscode-uml/IERC1404ExtendUML.png) + +##### enum REJECTED_CODE_BASE + +Error codes for transfer restrictions. + Codes `6–9` are reserved for future CMTAT ruleEngine extensions. + +| Name | Value | Description | +| -------------------------------------------------- | ----- | ------------------------------------------------------------ | +| TRANSFER_OK | 0 | Transfer authorized. | +| TRANSFER_REJECTED_PAUSED | 1 | Transfer rejected because the token is paused. | +| TRANSFER_REJECTED_FROM_FROZEN | 2 | Transfer rejected because the sender’s address is frozen. | +| TRANSFER_REJECTED_TO_FROZEN | 3 | Transfer rejected because the recipient’s address is frozen. | +| TRANSFER_REJECTED_SPENDER_FROZEN | 4 | Transfer rejected because the spender’s address is frozen. | +| TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE | 5 | Transfer rejected because the sender does not have enough active (unfrozen) balance. | + + + +------ + +##### detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) -> uint8 + +Returns a uint8 code to indicate if a transfer is restricted or not. + +This is an extension of {ERC-1404} with an additional `spender` parameter to enforce restriction logic on delegated transfers. + Examples of restriction logic include: + +- verifying if the recipient is whitelisted, +- verifying if tokens are locked for either sender or spender, etc. + +**Input Parameters:** + +| Name | Type | Description | +| ------- | ------- | ------------------------------------------------------------ | +| spender | address | The address initiating the transfer (for delegated transfers). | +| from | address | The source address. | +| to | address | The destination address. | +| value | uint256 | The number of tokens to transfer. | + + + +**Return Values:** + +| Type | Description | +| ----- | ------------------------------------------------------ | +| uint8 | Restriction code (0 means the transfer is authorized). | + + + +------ + +### VersionModule + +![VersionModuleUML](./schema/vscode-uml/VersionModuleUML.png) + +#### Contracts Description Table + + +| Contract | Type | Bases | | | +| :---------------: | :---------------: | :------------: | :------------: | :-----------: | +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +| | | | | | +| **VersionModule** | Implementation | IERC3643Base | | | +| └ | version | Public ❗️ | | NO❗️ | + +#### version() + +```solidity +function version() external view returns (string memory version_); +``` + +```solidity +function version() +public view virtual override(IERC3643Base) +returns (string memory version_) +``` + + **Description** + +Returns the current version of the token contract. +Useful for identifying which version of the smart contract is deployed and in use. + +**Return** + +| Name | Type | Description | +| ---------- | ------ | ------------------------------------------------------------ | +| `version_` | string | The version string of the token implementation (e.g., "1.0.0"). | + + + +### ERC3643ComplianceModule + +![ERC3643ComplianceModuleUML](./schema/vscode-uml/ERC3643ComplianceModuleUML.png) + +#### Contracts Description Table + + +| Contract | Type | Bases | | | +| :-------------------------: | :---------------: | :-------------------------------: | :------------: | :-----------: | +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +| | | | | | +| **ERC3643ComplianceModule** | Implementation | Context, IERC3643Compliance | | | +| └ | bindToken | Public ❗️ | 🛑 | onlyRole | +| └ | unbindToken | Public ❗️ | 🛑 | onlyRole | +| └ | isTokenBound | Public ❗️ | | NO❗️ | +| └ | getTokenBound | Public ❗️ | | NO❗️ | + +### ERC3643ComplianceExtendedModule + +`ERC3643ComplianceExtendedModule` inherits `ERC3643ComplianceModule` and contains project-specific helpers not part of the ERC-3643 base interface (`IERC3643Compliance`): batch bind/unbind, self-binding approval APIs, and `getTokenBounds()`. +| └ | _unbindToken | Internal 🔒 | 🛑 | | +| └ | _bindToken | Internal 🔒 | 🛑 | | + +#### Events + +##### TokenBound(address token) + +```solidity +event TokenBound(address token) +``` + +Emitted when a token is successfully bound to the compliance contract. + +**Event Parameters:** + +| Name | Type | Description | +| ----- | ------- | ---------------------------------------- | +| token | address | The address of the token that was bound. | + + + +------ + +##### TokenUnbound(address token) + +```solidity +event TokenUnbound(address token) +``` + +Emitted when a token is successfully unbound from the compliance contract. + +**Event Parameters:** + +| Name | Type | Description | +| ----- | ------- | ------------------------------------------ | +| token | address | The address of the token that was unbound. | + + + +------ + +#### Functions + +##### bindToken(address token) + +```solidity +function bindToken(address token) +public override virtual +onlyRole(COMPLIANCE_MANAGER_ROLE) +``` + +Associates a token contract with this compliance contract. + +The compliance contract may restrict operations on the bound token according to its internal compliance logic. + Reverts if the token is already bound. + +**Input Parameters:** + +| Name | Type | Description | +| ----- | ------- | --------------------------------- | +| token | address | The address of the token to bind. | + + + +------ + +##### unbindToken(address token) + +```solidity +function unbindToken(address token) +public override virtual +onlyRole(COMPLIANCE_MANAGER_ROLE) +``` + +Removes the association of a token contract from this compliance contract. + +Reverts if the token is not currently bound. + +**Input Parameters:** + +| Name | Type | Description | +| ----- | ------- | ----------------------------------- | +| token | address | The address of the token to unbind. | + + + +------ + +##### isTokenBound(address token) -> bool + +```solidity +function isTokenBound(address token) +public view virtual override +returns (bool) +``` + +Checks whether a token is currently bound to this compliance contract. + +**Input Parameters:** + +| Name | Type | Description | +| ----- | ------- | ---------------------------- | +| token | address | The token address to verify. | + + + +**Return Values:** + +| Type | Description | +| ---- | -------------------------------------------- | +| bool | True if the token is bound, false otherwise. | + + + +------ + +##### getTokenBound() -> address + +```solidity +function getTokenBound() +public view virtual override +returns (address) +``` + +Returns the single token currently bound to this compliance contract. + +If multiple tokens are supported, consider using `getTokenBounds()`. + +Note that there are no guarantees on the ordering of values inside the array, and it may change when more values are added or removed. + +**Return Values:** + +| Type | Description | +| ------- | ----------------------------------------- | +| address | The address of the currently bound token. | + + + +------ + +##### getTokenBounds() -> address[] + +```solidity +function getTokenBounds() +public view override +returns (address[] memory) +``` + +Returns all tokens currently bound to this compliance contract. + +This is a view-only function and does not modify state. +This function is not part of the original ERC-3643 specification. + +This operation will copy the entire storage to memory, which can be quite expensive. + +This is designed to mostly be used by view accessors that are queried without any gas fees. + +**Return Values:** + +| Type | Description | +| --------- | ----------------------------------------------- | +| address[] | An array of addresses of bound token contracts. | + + + +### RulesManagementModule + +![RuleManagementModuleUML](./schema/vscode-uml/RuleManagementModuleUML.png) + +#### Events + +##### event AddRule(address rule) + +```solidity +event AddRule(IRule indexed rule) +``` + +Emitted when a new rule is added to the rule set. + +**Event Parameters:** + +| Name | Type | Description | +| ---- | ----- | ------------------------------------------------ | +| rule | IRule | The address of the rule contract that was added. | + +------ + +##### event RemoveRule(address rule) + +```solidity +event RemoveRule(IRule indexed rule) +``` + +Emitted when a rule is removed from the rule set. + +**Event Parameters:** + +| Name | Type | Description | +| ---- | ----- | -------------------------------------------------- | +| rule | IRule | The address of the rule contract that was removed. | + +------ + +##### event ClearRules() + +```solidity +event ClearRules() +``` + +Emitted when all rules are cleared from the rule set. + +This event has no parameters. + +#### Functions + +##### setRules(address[] rules_) + +```solidity +function setRules(IRule[] calldata rules_) +public virtual override(IRulesManagementModule) +onlyRole(RULES_MANAGEMENT_ROLE) +``` + +Defines the complete list of rules for the rule engine. + +Any previously configured rules are completely replaced. + Rules must be deployed contracts implementing the expected `IRule` interface. + Reverts if any rule address is zero or if duplicates are detected. + +This function calls _clearRules if at least one rule is still configured + +**Input Parameters:** + +| Name | Type | Description | +| ------ | ------- | ------------------------------------------------------------ | +| rules_ | IRule[] | The array of IRule contracts to configure as the active rules. | + + + +------ + +##### rulesCount() -> uint256 + +```solidity +function rulesCount() +public view virtual override(IRulesManagementModule) +returns (uint256) +``` + +Returns the total number of currently configured rules. + +Equivalent to the length of the internal rules array. + +**Return Values:** + +| Type | Description | +| ------- | --------------------------- | +| uint256 | The number of active rules. | + + + +------ + +##### rule(uint256 ruleId) -> address + +```solidity +function rule(uint256 ruleId) +public view virtual override(IRulesManagementModule) +returns (address) +``` + +Retrieves the rule address at a specific index. + +Return the `zero address` if out of bounds. + +Note that there are no guarantees on the ordering of values inside the array, and it may change when more values are added or removed. + +**Input Parameters:** + +| Name | Type | Description | +| ------ | ------- | ------------------------------------------- | +| ruleId | uint256 | The index of the desired rule in the array. | + + + +**Return Values:** + +| Type | Description | +| ------- | ------------------------------------------------ | +| address | The address of the corresponding IRule contract. | + + + +------ + +##### rules() -> address[] + +```solidity +function rules() +public view virtual override(IRulesManagementModule) +returns (address[] memory) +``` + +Returns the full list of currently configured rules. + +This is a view-only function and does not modify state. + +This operation will copy the entire storage to memory, which can be quite expensive. + +This is designed to mostly be used by view accessors that are queried without any gas fees. + +**Return Values:** + +| Type | Description | +| --------- | ------------------------------------------------------- | +| address[] | An array containing all active rule contract addresses. | + + + +------ + +##### clearRules() + +```solidity +function clearRules() +public virtual override(IRulesManagementModule) +onlyRole(RULES_MANAGEMENT_ROLE) +``` + +Removes all configured rules. + +After calling this function, no rules will remain set. + +Developers should keep in mind that this function has an unbounded cost and using it may render the function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + +------ + +##### addRule(address rule_) + +```solidity +function addRule(IRule rule_) +public virtual override(IRulesManagementModule) +onlyRole(RULES_MANAGEMENT_ROLE) +``` + +Adds a new rule to the current rule set. + +Reverts if the rule address is zero or already exists in the set. + +**Input Parameters:** + +| Name | Type | Description | +| ----- | ----- | -------------------------- | +| rule_ | IRule | The IRule contract to add. | + + + +------ + +##### removeRule(address rule_) + +```solidity + function removeRule(IRule rule_) + public virtual + override(IRulesManagementModule) + onlyRole(RULES_MANAGEMENT_ROLE) +``` + +Removes a specific rule from the current rule set. + +Reverts if the provided rule is not found or does not match the stored rule at its index. + +**Input Parameters:** + +| Name | Type | Description | +| ----- | ----- | ----------------------------- | +| rule_ | IRule | The IRule contract to remove. | + + + +------ + +##### containsRule(address rule_) -> bool + +```solidity +function containsRule(IRule rule_) +public view virtual override(IRulesManagementModule) +returns (bool) +``` + +Checks whether a specific rule is currently configured. + +**Input Parameters:** + +| Name | Type | Description | +| ----- | ----- | ------------------------------------------- | +| rule_ | IRule | The IRule contract to check for membership. | + + + +**Return Values:** + +| Type | Description | +| ---- | --------------------------------------------- | +| bool | True if the rule is present, false otherwise. | + +## Security + +### Vulnerability disclosure + +Please see [SECURITY.md](https://github.com/CMTA/CMTAT/blob/master/SECURITY.md) (CMTAT main repository). + +### Audit + +#### First Audit - March 2022 + +> The contracts (v.1.0.2) have been audited by [ABDK Consulting](https://www.abdk.consulting/), a globally recognized firm specialized in smart contracts' security. + +Fixed version : [v1.0.2](https://github.com/CMTA/RuleEngine/releases/tag/v1.0.2) + +The first audit was performed by ABDK on the version [1.0.1](https://github.com/CMTA/RuleEngine/releases/tag/1.0.1). + +The release [v1.0.2](https://github.com/CMTA/RuleEngine/releases/tag/v1.0.2) contains the different fixes and improvements related to this audit. + +The final report is available in [ABDK_CMTA_CMTATRuleEngine_v_1_0.pdf](https://github.com/CMTA/CMTAT/blob/master/doc/audits/ABDK_CMTA_CMTATRuleEngine_v_1_0/ABDK_CMTA_CMTATRuleEngine_v_1_0.pdf). + +### Tools + +#### Nethermind AuditAgent + +> **Note:** This scan was performed by an AI-powered automated tool, not a formal human-led audit. + +| Version | Report | Assessment | +|---------|--------|------------| +| Scan #1 (Feb 2026) | [audit_agent_report_1_v3.0.0-rc1.pdf](./security/audits/tools/nethermind-audit-agent/v3.0.0-rc1/audit_agent_report_1_v3.0.0-rc1.pdf) | [feedback.md](./security/audits/tools/nethermind-audit-agent/v3.0.0-rc1/audit_agent_report_1_v3.0.0-rc1-feedback.md) | + +7 findings — 0 High · 1 Medium · 1 Low · 4 Info · 1 Best Practices + +| # | Severity | Finding | Status | +|---|----------|---------|--------| +| 1 | Medium | Cross-token rule state pollution in multi-tenant deployments | NatSpec + README warnings. Interface fix deferred (requires CMTAT coordination). | +| 2 | Low | `RuleEngineOwnable` misreports `IAccessControl` via ERC-165 | Fixed: explicit interface whitelist + negative test added. | +| 3 | Info | Unbounded rules loop — potential permanent DoS | 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()` | 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). | + +#### Slither + +Here is the list of report performed with [Slither](https://github.com/crytic/slither) + +| Version | Report | Assessment | +| ------- | ------ | ---------- | +| v3.0.0-rc5 | [slither-report.md](./security/audits/tools/v3.0.0-rc5/slither-report.md) | [slither-report-feedback.md](./security/audits/tools/v3.0.0-rc5/slither-report-feedback.md) | +| v3.0.0-rc4 | [slither-report.md](./security/audits/tools/v3.0.0-rc4/slither-report.md) | [slither-report-feedback.md](./security/audits/tools/v3.0.0-rc4/slither-report-feedback.md) | +| v3.0.0-rc3 | [slither-report.md](./security/audits/tools/v3.0.0-rc3/slither-report.md) | [slither-report-feedback.md](./security/audits/tools/v3.0.0-rc3/slither-report-feedback.md) | +| v3.0.0-rc2 | [slither-report.md](./security/audits/tools/v3.0.0-rc2/slither-report.md) | [slither-report-feedback.md](./security/audits/tools/v3.0.0-rc2/slither-report-feedback.md) | + +```bash +slither . --checklist --filter-paths "openzeppelin-contracts|test|CMTAT|forge-std|mocks" > slither-report.md +``` + +2 finding categories — 0 High · 0 Medium · 10 Low · 2 Informational + +| ID | Detector | Impact | Instances | Assessment | +|----|----------|--------|-----------|------------| +| 0–9 | `calls-loop` | Low | 10 | Accepted by design — fan-out to rule contracts is the core architecture | +| 10–11 | `unindexed-event-address` | Informational | 2 | Deferred — adding `indexed` to `TokenBound`/`TokenUnbound` is interface-breaking | + +#### Aderyn + +Here is the list of report performed with [Aderyn](https://github.com/Cyfrin/aderyn) + +```bash +aderyn -x mocks --output aderyn-report.md +``` + +| Version | Report | Assessment | +| ------- | ------ | ---------- | +| v3.0.0-rc5 | [aderyn-report.md](./security/audits/tools/v3.0.0-rc5/aderyn-report.md) | [aderyn-report-feedback.md](./security/audits/tools/v3.0.0-rc5/aderyn-report-feedback.md) | +| v3.0.0-rc4 | [aderyn-report.md](./security/audits/tools/v3.0.0-rc4/aderyn-report.md) | [aderyn-report-feedback.md](./security/audits/tools/v3.0.0-rc4/aderyn-report-feedback.md) | +| v3.0.0-rc3 | [aderyn-report.md](./security/audits/tools/v3.0.0-rc3/aderyn-report.md) | [aderyn-report-feedback.md](./security/audits/tools/v3.0.0-rc3/aderyn-report-feedback.md) | +| v3.0.0-rc2 | [aderyn-report.md](./security/audits/tools/v3.0.0-rc2/aderyn-report.md) | [aderyn-report-feedback.md](./security/audits/tools/v3.0.0-rc2/aderyn-report-feedback.md) | + +Report scope: 24 Solidity files, 629 nSLOC. + +0 High · 8 Low + +| ID | Finding | Instances | Assessment | +|----|---------|-----------|------------| +| L-1 | Centralization Risk | 14 | Accepted by design — privileged compliance tool | +| L-2 | Unspecific Solidity Pragma | 19 | Accepted by design — intentional for library reusability | +| L-3 | PUSH0 Opcode | 24 | 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` | 4 | Accepted by design — `setRules` and `bindTokens`/`unbindTokens` are atomic batch operations | +| L-7 | Costly Operations Inside Loop | 4 | Accepted — unavoidable `SSTORE` in batch operations | +| L-8 | Unchecked Return | 1 | Accepted — `_grantRole` return is irrelevant in constructor | + +## Documentation + +Here a summary of the main documentation + +| Document | Link/Files | +| ------------ | --------------------------------------- | +| Integration with CMTAT | [doc/technical/RuleEngine-with-CMTAT.md](./technical/RuleEngine-with-CMTAT.md) | +| Integration with ERC-3643 | [doc/technical/RuleEngine-with-ERC3643.md](./technical/RuleEngine-with-ERC3643.md) | +| Toolchain | [doc/TOOLCHAIN.md](./TOOLCHAIN.md) | +| Surya report | [doc/schema/surya](./schema/surya/) | +| Code-quality review | [doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md](./security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md) | +| Script review | [doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS_SCRIPT.md](./security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS_SCRIPT.md) | +| Audit overview | [doc/security/audits/AUDIT_OVERVIEW.md](./security/audits/AUDIT_OVERVIEW.md) | + +See also [Taurus - Token Transfer Management: How to Apply Restrictions with CMTAT and ERC-1404](https://www.taurushq.com/blog/token-transfer-management-how-to-apply-restrictions-with-cmtat-and-erc-1404/) (RuleEngine v2.02 and CMTAT v2.4.0) + +## Toolchains and Usage + +This repository is primarily developed and tested with Foundry. + +Hardhat configuration is also present to allow compiling the contracts and running a small smoke test with Hardhat. + +Parts of this project were written with the help of AI coding assistants, principally Claude Code (Anthropic) and Codex (OpenAI). + +### Configuration + +Here are the settings for [Hardhat](https://hardhat.org) and [Foundry](https://getfoundry.sh). + +- `hardhat.config.js` + - Solidity [v0.8.36](https://docs.soliditylang.org/en/v0.8.36/) + - EVM version: Prague (Pectra upgrade) + - Optimizer: true, 200 runs + +- `foundry.toml` + - Solidity [v0.8.36](https://docs.soliditylang.org/en/v0.8.36/) + - EVM version: Prague (Pectra upgrade) + - Optimizer: true, 200 runs + + + + +### Toolchain installation +The contracts are developed and tested with [Foundry](https://book.getfoundry.sh), a smart contract development toolchain. + +To install the Foundry suite, please refer to the official instructions in the [Foundry book](https://book.getfoundry.sh/getting-started/installation). + +### Initialization + +You must first initialize the submodules, with + +``` +forge install +``` + +See also the command's [documentation](https://book.getfoundry.sh/reference/forge/forge-install). + +Later you can update all the submodules with: + +``` +forge update +``` + +See also the command's [documentation](https://book.getfoundry.sh/reference/forge/forge-update). + +### Compilation + +The official documentation is available in the Foundry [website](https://book.getfoundry.sh/reference/forge/build-commands) + +```bash +# Build all contracts +forge build + +# Build specific contract +forge build --contracts src/deployment/RuleEngine.sol +forge build --contracts src/deployment/RuleEngineOwnable.sol +forge build --contracts src/deployment/RuleEngineOwnable2Step.sol +``` +### Contract size + +```bash +forge build --sizes +``` + +Latest output (`2026-03-18`) for the main RuleEngine contracts: + +| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) | +|----------|------------------:|------------------:|--------------------:|---------------------:| +| RuleEngine | 6,756 | 7,805 | 17,820 | 41,347 | +| RuleEngineOwnable | 6,170 | 6,833 | 18,406 | 42,319 | + +Both `RuleEngine` and `RuleEngineOwnable` remain well below the EIP-170 runtime limit. `RuleEngineOwnable` is slightly smaller because `Ownable` has less overhead than `AccessControl`. + +### Testing + +You can run the tests with + +```bash +forge test +``` + +To run a specific test, use + +```bash +forge test --match-contract --match-test +``` + +Generate gas report + +```bash +forge test --gas-report +``` + +See also the test framework's [official documentation](https://book.getfoundry.sh/forge/tests), and that of the [test commands](https://book.getfoundry.sh/reference/forge/test-commands). + +There is also a small Hardhat smoke test to confirm the main `RuleEngine` contract can be compiled and deployed through Hardhat: + +```bash +npx hardhat test test/hardhat/RuleEngine.smoke.js +``` + +### Coverage + +A code coverage is available in [index.html](./coverage/coverage/index.html). + +![code-coverage](./coverage/code-coverage.png) + +* Perform a code coverage +``` +forge coverage +``` + +* Generate LCOV report +``` +forge coverage --report lcov +``` + +- Generate `index.html` + +```bash +forge coverage --no-match-coverage "(mocks|test)" --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage +``` + +See [Solidity Coverage in VS Code with Foundry](https://mirror.xyz/devanon.eth/RrDvKPnlD-pmpuW7hQeR5wWdVjklrpOgPCOA-PJkWFU) & [Foundry forge coverage](https://www.rareskills.io/post/foundry-forge-coverage) + +### Deployment +The official documentation is available in the Foundry [website](https://getfoundry.sh/forge/deploying) + +#### Choosing a Contract + +| Scenario | Recommended Contract | +|----------|---------------------| +| Multiple operators with different permissions | `RuleEngine` | +| Single administrator | `RuleEngineOwnable` | +| Single administrator with safer ownership handover | `RuleEngineOwnable2Step` | +| Integration with existing RBAC systems | `RuleEngine` | +| Simpler deployment and management | `RuleEngineOwnable` | + +#### Script + +The scripts in `script/` are example deployment flows. + +> Warning: `RuleEngineScript.s.sol` and `CMTATWithRuleEngineScript.s.sol` deploy `RuleWhitelistMock` from `src/mocks/`. That contract is a reference/mock rule for testing and demos, not a production rule contract. + +For production deployments, source rule contracts from the dedicated [CMTA/Rules](https://github.com/CMTA/Rules) repository and adapt the script parameters accordingly. + +To run the example scripts, create a `.env` file. The value for `CMTAT_ADDRESS` is required only for `RuleEngineScript.s.sol`. + +Warning: putting your private key in a .env file is not the most secure approach. + +* File `.env` +``` +PRIVATE_KEY= +CMTAT_ADDRESS= +``` +**Private Keys**: Never expose your private keys. The `.env` file here used in this project should not be used for production. See [getfoundry.sh - Key Management](https://getfoundry.sh/guides/best-practices/key-management/) + +* Command + +CMTAT with RuleEngine + +```bash +forge script script/CMTATWithRuleEngineScript.s.sol:CMTATWithRuleEngineScript --rpc-url=$RPC_URL --broadcast --verify -vvv +``` + + +- Value of YOUR_RPC_URL with a local instance of anvil : [127.0.0.1:8545](http://127.0.0.1:8545) + +```bash +forge script script/CMTATWithRuleEngineScript.s.sol:CMTATWithRuleEngineScript --rpc-url=127.0.0.1:8545 --broadcast --verify -vvv +``` + +Only RuleEngine with the mock/reference `RuleWhitelistMock` contract + +```bash +forge script script/RuleEngineScript.s.sol:RuleEngineScript --rpc-url=$RPC_URL --broadcast --verify -vvv +``` + +- With anvil + +```bash +forge script script/RuleEngineScript.s.sol:RuleEngineScript --rpc-url=127.0.0.1:8545 --broadcast --verify -vvv +``` + +#### Production Deployment Checklist + +- Choose the deployable variant: `RuleEngine`, `RuleEngineOwnable`, or `RuleEngineOwnable2Step`. +- Choose the trusted forwarder address, or use `address(0)` if ERC-2771 support is not needed. +- Decide whether the token should be bound in the constructor or later via `bindToken`. +- Source production rule contracts from the [CMTA/Rules](https://github.com/CMTA/Rules) repository, not from `src/mocks/`. +- Verify post-deployment permissions: owner for ownable variants, or admin plus role assignments for the RBAC variant. + +### Solidity style guideline + +RuleEngine follows the [solidity style guideline](https://docs.soliditylang.org/en/latest/style-guide.html) and the [natspec format](https://docs.soliditylang.org/en/latest/natspec-format.html) for comments + +#### Formatting & Linting + +We use Foundry's built-in formatter and linter: + +```bash +# Format all Solidity files +forge fmt + +# Check formatting without modifying files +forge fmt --check + +# Run the Solidity linter +forge lint +``` + +- Orders of Functions + +Functions are grouped according to their visibility and ordered: + +``` +1. constructor + +2. receive function (if exists) + +3. fallback function (if exists) + +4. external + +5. public + +6. internal + +7. private +``` + +Within a grouping, place the `view` and `pure` functions last + +- Function declaration + +``` +1. Visibility +2. Mutability +3. Virtual +4. Override +5. Custom modifiers +``` + +## Intellectual property + +The code is copyright (c) Capital Market and Technology Association, 2022-2026, and is released under [Mozilla Public License 2.0](https://github.com/CMTA/CMTAT/blob/master/LICENSE.md). diff --git a/doc/TOOLCHAIN.md b/doc/TOOLCHAIN.md index 51256bc..f3c6e6c 100644 --- a/doc/TOOLCHAIN.md +++ b/doc/TOOLCHAIN.md @@ -42,7 +42,7 @@ Utility tool for smart contract systems. **[OpenZeppelin Contracts](https://github.com/OpenZeppelin/openzeppelin-contracts)** OpenZeppelin Contracts -The version of the library used is available in the [README](../README.md) +The version of the library used is available in the [README](./README.md#dependencies) Warning: - Submodules are not automatically updated when the host repository is updated. @@ -52,7 +52,7 @@ Warning: The current tested baseline is: -- Solidity: [0.8.34](https://docs.soliditylang.org/en/v0.8.34/) +- Solidity: [0.8.36](https://docs.soliditylang.org/en/v0.8.36/) - OpenZeppelin Contracts (submodule): [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.6.1) - CMTAT: [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0) @@ -85,7 +85,7 @@ Or only specified contracts npx sol2uml class -i -c src/RuleEngine.sol ``` -The related component can be installed with `npm install` (see [package.json](./package.json)). +The related component can be installed with `npm install` (see [package.json](../package.json)). > To avoid the error "Maximum call stack size exceeded", you can flatten the contract before > @@ -114,7 +114,7 @@ npm run-script surya:graph ``` ```bash -npx surya graph src/RuleEngine.sol | dot -Tpng > surya_graph_RuleEngine.png +npx surya graph src/deployment/RuleEngine.sol | dot -Tpng > surya_graph_RuleEngine.png ``` #### Report @@ -139,6 +139,81 @@ slither . --checklist --filter-paths "openzeppelin-contracts|test|mocks|CMTAT|f aderyn -x mocks --output aderyn-report.md ``` +## Code coverage + +**[forge coverage](https://book.getfoundry.sh/reference/forge/forge-coverage)** — test coverage + +```bash +# Summary in the terminal +forge coverage + +# Production coverage as an HTML report in ./coverage, mocks and tests excluded +forge coverage --no-match-coverage "(mocks|test)" --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage +``` + +`genhtml` ships with [LCOV](https://github.com/linux-test-project/lcov) (`apt install lcov`). Open +`coverage/index.html` to browse the result. `--no-match-coverage "(mocks|test)"` keeps the figure meaningful by +excluding the reference rules in `src/mocks/` and the test contracts, which would otherwise inflate it. + +Both `lcov.info` and the generated `coverage/` directory are gitignored scratch. The **published** report is +committed under [doc/coverage](./coverage/); refresh it by copying the generated directory there. + +Add `script` to the exclusion — `--no-match-coverage "(script|mocks|test)"` — to measure `src/` alone, without +the Foundry deployment scripts. Measured on v3.0.0-rc5 the difference is marginal, since both scripts are +covered by tests: + +| Exclusion | Lines | Functions | Branches | +|---|---|---|---| +| `(mocks\|test)` | 98.5% (270/274) | 95.3% (81/85) | 93.0% (40/43) | +| `(script\|mocks\|test)` | 98.3% (234/238) | 95.2% (79/83) | 93.0% (40/43) | + +### Reading the report: abstract declarations always show 0 + +An `internal virtual;` declaration with **no body** is reported with a hit count of `0`, which looks like a +coverage gap but is not one. There is no bytecode at that line: the declaration only fixes the signature, and +the executable code lives in the contracts that override it. + +This affects the access-control hooks, which the project declares in the abstract modules and implements in +each deployable contract: + +```solidity +// src/modules/ERC3643ComplianceModule.sol — reported as 0, no body to execute +function _authorizeComplianceBindingChange(address token) internal virtual; +function _onlyComplianceManager() internal virtual; + +// src/modules/RulesManagementModule.sol — likewise +function _onlyRulesManager() internal virtual; +function _onlyRulesLimitManager() internal virtual; +``` + +The implementations are covered. Measured on v3.0.0-rc5 (`FNDA` records in `lcov.info`): + +| Implementation | Hits | +|---|---| +| `RuleEngine._onlyRulesManager` | 217 | +| `ERC3643ComplianceExtendedModule._authorizeComplianceBindingChange` | 87 | +| `RuleEngineOwnable._onlyRulesManager` | 69 | +| `RuleEngine._onlyComplianceManager` | 56 | +| `RuleEngineOwnable._onlyComplianceManager` | 46 | +| `RuleEngineOwnable2Step._onlyComplianceManager` | 25 | +| `RuleEngine._onlyRulesLimitManager` | 5 | + +The `onlyX` **modifiers** that call these hooks are counted in the abstract module itself +(`onlyComplianceManager` 9 hits, `onlyRulesManager` 19), which is the giveaway: the modifier executes there, +while the hook it dispatches to resolves to the derived contract. + +To check a specific hook rather than trusting the HTML, read the function records directly: + +```bash +grep -E "^FNDA:.*_onlyComplianceManager" lcov.info +``` + +**Rule of thumb: a `0` on a line that declares an abstract function means "implemented elsewhere", not +"untested".** + +See also [Solidity Coverage in VS Code with Foundry](https://mirror.xyz/devanon.eth/RrDvKPnlD-pmpuW7hQeR5wWdVjklrpOgPCOA-PJkWFU) +and [Foundry forge coverage](https://www.rareskills.io/post/foundry-forge-coverage). + ## Code style guidelines We use the following Foundry tools to ensure consistent coding style: diff --git a/doc/coverage/code-coverage.png b/doc/coverage/code-coverage.png index 4b9eb22..8ae5e00 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 17ae624..de6d7c8 100644 --- a/doc/coverage/coverage/index-sort-b.html +++ b/doc/coverage/coverage/index-sort-b.html @@ -31,27 +31,27 @@ lcov.info Lines: - 232 - 236 - 98.3 % + 270 + 274 + 98.5 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: - 79 - 83 - 95.2 % + 81 + 85 + 95.3 % Branches: - 41 - 46 - 89.1 % + 40 + 43 + 93.0 % @@ -84,14 +84,26 @@ src/modules -
96.7%96.7%
+
96.6%96.6%
- 96.7 % - 116 / 120 + 96.6 % + 114 / 118 89.5 % 34 / 38 - 85.7 % - 30 / 35 + 90.3 % + 28 / 31 + + + script + +
100.0%
+ + 100.0 % + 36 / 36 + 100.0 % + 2 / 2 + - + 0 / 0 src/deployment @@ -99,7 +111,7 @@
100.0%
100.0 % - 52 / 52 + 53 / 53 100.0 % 25 / 25 100.0 % @@ -111,11 +123,11 @@
100.0%
100.0 % - 64 / 64 + 67 / 67 100.0 % 20 / 20 100.0 % - 6 / 6 + 7 / 7 diff --git a/doc/coverage/coverage/index-sort-f.html b/doc/coverage/coverage/index-sort-f.html index 7eb445a..0d8d1de 100644 --- a/doc/coverage/coverage/index-sort-f.html +++ b/doc/coverage/coverage/index-sort-f.html @@ -31,27 +31,27 @@ lcov.info Lines: - 232 - 236 - 98.3 % + 270 + 274 + 98.5 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: - 79 - 83 - 95.2 % + 81 + 85 + 95.3 % Branches: - 41 - 46 - 89.1 % + 40 + 43 + 93.0 % @@ -84,14 +84,26 @@ src/modules -
96.7%96.7%
+
96.6%96.6%
- 96.7 % - 116 / 120 + 96.6 % + 114 / 118 89.5 % 34 / 38 - 85.7 % - 30 / 35 + 90.3 % + 28 / 31 + + + script + +
100.0%
+ + 100.0 % + 36 / 36 + 100.0 % + 2 / 2 + - + 0 / 0 src @@ -99,11 +111,11 @@
100.0%
100.0 % - 64 / 64 + 67 / 67 100.0 % 20 / 20 100.0 % - 6 / 6 + 7 / 7 src/deployment @@ -111,7 +123,7 @@
100.0%
100.0 % - 52 / 52 + 53 / 53 100.0 % 25 / 25 100.0 % diff --git a/doc/coverage/coverage/index-sort-l.html b/doc/coverage/coverage/index-sort-l.html index 48fefd9..c42e123 100644 --- a/doc/coverage/coverage/index-sort-l.html +++ b/doc/coverage/coverage/index-sort-l.html @@ -31,27 +31,27 @@ lcov.info Lines: - 232 - 236 - 98.3 % + 270 + 274 + 98.5 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: - 79 - 83 - 95.2 % + 81 + 85 + 95.3 % Branches: - 41 - 46 - 89.1 % + 40 + 43 + 93.0 % @@ -84,14 +84,26 @@ src/modules -
96.7%96.7%
+
96.6%96.6%
- 96.7 % - 116 / 120 + 96.6 % + 114 / 118 89.5 % 34 / 38 - 85.7 % - 30 / 35 + 90.3 % + 28 / 31 + + + script + +
100.0%
+ + 100.0 % + 36 / 36 + 100.0 % + 2 / 2 + - + 0 / 0 src/deployment @@ -99,7 +111,7 @@
100.0%
100.0 % - 52 / 52 + 53 / 53 100.0 % 25 / 25 100.0 % @@ -111,11 +123,11 @@
100.0%
100.0 % - 64 / 64 + 67 / 67 100.0 % 20 / 20 100.0 % - 6 / 6 + 7 / 7 diff --git a/doc/coverage/coverage/index.html b/doc/coverage/coverage/index.html index 40a7f7b..8c088b1 100644 --- a/doc/coverage/coverage/index.html +++ b/doc/coverage/coverage/index.html @@ -31,27 +31,27 @@ lcov.info Lines: - 232 - 236 - 98.3 % + 270 + 274 + 98.5 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: - 79 - 83 - 95.2 % + 81 + 85 + 95.3 % Branches: - 41 - 46 - 89.1 % + 40 + 43 + 93.0 % @@ -81,17 +81,29 @@ Functions Sort by function coverage Branches Sort by branch coverage + + script + +
100.0%
+ + 100.0 % + 36 / 36 + 100.0 % + 2 / 2 + - + 0 / 0 + src
100.0%
100.0 % - 64 / 64 + 67 / 67 100.0 % 20 / 20 100.0 % - 6 / 6 + 7 / 7 src/deployment @@ -99,7 +111,7 @@
100.0%
100.0 % - 52 / 52 + 53 / 53 100.0 % 25 / 25 100.0 % @@ -108,14 +120,14 @@ src/modules -
96.7%96.7%
+
96.6%96.6%
- 96.7 % - 116 / 120 + 96.6 % + 114 / 118 89.5 % 34 / 38 - 85.7 % - 30 / 35 + 90.3 % + 28 / 31 diff --git a/doc/coverage/coverage/script/CMTATWithRuleEngineScript.s.sol.func-sort-c.html b/doc/coverage/coverage/script/CMTATWithRuleEngineScript.s.sol.func-sort-c.html new file mode 100644 index 0000000..2cf717f --- /dev/null +++ b/doc/coverage/coverage/script/CMTATWithRuleEngineScript.s.sol.func-sort-c.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - lcov.info - script/CMTATWithRuleEngineScript.s.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - script - CMTATWithRuleEngineScript.s.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:2020100.0 %
Date:2026-08-13 15:50:18Functions:11100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
CMTATWithRuleEngineScript.run1
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/script/CMTATWithRuleEngineScript.s.sol.func.html b/doc/coverage/coverage/script/CMTATWithRuleEngineScript.s.sol.func.html new file mode 100644 index 0000000..a4a33ae --- /dev/null +++ b/doc/coverage/coverage/script/CMTATWithRuleEngineScript.s.sol.func.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - lcov.info - script/CMTATWithRuleEngineScript.s.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - script - CMTATWithRuleEngineScript.s.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:2020100.0 %
Date:2026-08-13 15:50:18Functions:11100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
CMTATWithRuleEngineScript.run1
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/script/CMTATWithRuleEngineScript.s.sol.gcov.html b/doc/coverage/coverage/script/CMTATWithRuleEngineScript.s.sol.gcov.html new file mode 100644 index 0000000..df6245e --- /dev/null +++ b/doc/coverage/coverage/script/CMTATWithRuleEngineScript.s.sol.gcov.html @@ -0,0 +1,137 @@ + + + + + + + LCOV - lcov.info - script/CMTATWithRuleEngineScript.s.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - script - CMTATWithRuleEngineScript.s.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:2020100.0 %
Date:2026-08-13 15:50:18Functions:11100.0 %
Branches:00-
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : // Documentation :
+       4                 :            : // https://book.getfoundry.sh/tutorials/solidity-scripting
+       5                 :            : pragma solidity ^0.8.20;
+       6                 :            : 
+       7                 :            : import {Script, console} from "forge-std/Script.sol";
+       8                 :            : import {ICMTATConstructor, CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol";
+       9                 :            : import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol";
+      10                 :            : import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
+      11                 :            : import {RuleEngine} from "src/deployment/RuleEngine.sol";
+      12                 :            : import {RuleWhitelistMock} from "src/mocks/rules/validation/RuleWhitelistMock.sol";
+      13                 :            : 
+      14                 :            : /**
+      15                 :            :  * @title Example deployment of a CMTAT, a mock RuleWhitelistMock and a RuleEngine
+      16                 :            :  * @dev This script deploys a reference/mock rule from `src/mocks/` for demo and testing flows.
+      17                 :            :  * It is not a production deployment recipe for rule contracts.
+      18                 :            :  */
+      19                 :            : contract CMTATWithRuleEngineScript is Script {
+      20                 :          1 :     function run() external {
+      21                 :            :         // Get env variable
+      22                 :          1 :         uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
+      23                 :          1 :         address admin = vm.addr(deployerPrivateKey);
+      24                 :          1 :         address trustedForwarder = address(0x0);
+      25                 :          1 :         vm.startBroadcast(deployerPrivateKey);
+      26                 :            :         // CMTAT
+      27                 :          1 :         ICMTATConstructor.ERC20Attributes memory erc20Attributes =
+      28                 :          1 :             ICMTATConstructor.ERC20Attributes("CMTA Token", "CMTAT", 0);
+      29                 :          1 :         ICMTATConstructor.ExtraInformationAttributes memory extraInformationAttributes =
+      30                 :          1 :             ICMTATConstructor.ExtraInformationAttributes(
+      31                 :            :                 "CMTAT_ISIN",
+      32                 :            :                 IERC1643CMTAT.DocumentInfo(
+      33                 :            :                     "Terms", "https://cmta.ch", 0x9ff867f6592aa9d6d039e7aad6bd71f1659720cbc4dd9eae1554f6eab490098b
+      34                 :            :                 ),
+      35                 :            :                 "CMTAT_info"
+      36                 :            :             );
+      37                 :          1 :         ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(address(0)));
+      38                 :          1 :         CMTATStandardStandalone cmtatContract =
+      39                 :          1 :             new CMTATStandardStandalone(trustedForwarder, admin, erc20Attributes, extraInformationAttributes, engines);
+      40                 :          1 :         console.log("CMTAT cmtatContract : ", address(cmtatContract));
+      41                 :            :         // whitelist
+      42                 :          1 :         RuleWhitelistMock ruleWhitelist = new RuleWhitelistMock(admin, trustedForwarder);
+      43                 :          1 :         console.log("whitelist: ", address(ruleWhitelist));
+      44                 :            :         // ruleEngine
+      45                 :          1 :         RuleEngine ruleEngine = new RuleEngine(admin, trustedForwarder, address(cmtatContract));
+      46                 :          1 :         console.log("RuleEngine : ", address(ruleEngine));
+      47                 :          1 :         ruleEngine.addRule(ruleWhitelist);
+      48                 :          1 :         cmtatContract.setRuleEngine(ruleEngine);
+      49                 :            : 
+      50                 :          1 :         vm.stopBroadcast();
+      51                 :            :     }
+      52                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/script/RuleEngineScript.s.sol.func-sort-c.html b/doc/coverage/coverage/script/RuleEngineScript.s.sol.func-sort-c.html new file mode 100644 index 0000000..4717471 --- /dev/null +++ b/doc/coverage/coverage/script/RuleEngineScript.s.sol.func-sort-c.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - lcov.info - script/RuleEngineScript.s.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - script - RuleEngineScript.s.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1616100.0 %
Date:2026-08-13 15:50:18Functions:11100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleEngineScript.run1
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/script/RuleEngineScript.s.sol.func.html b/doc/coverage/coverage/script/RuleEngineScript.s.sol.func.html new file mode 100644 index 0000000..b18ea40 --- /dev/null +++ b/doc/coverage/coverage/script/RuleEngineScript.s.sol.func.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - lcov.info - script/RuleEngineScript.s.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - script - RuleEngineScript.s.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1616100.0 %
Date:2026-08-13 15:50:18Functions:11100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleEngineScript.run1
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/script/RuleEngineScript.s.sol.gcov.html b/doc/coverage/coverage/script/RuleEngineScript.s.sol.gcov.html new file mode 100644 index 0000000..9905e9d --- /dev/null +++ b/doc/coverage/coverage/script/RuleEngineScript.s.sol.gcov.html @@ -0,0 +1,141 @@ + + + + + + + LCOV - lcov.info - script/RuleEngineScript.s.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - script - RuleEngineScript.s.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1616100.0 %
Date:2026-08-13 15:50:18Functions:11100.0 %
Branches:00-
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : // Documentation :
+       4                 :            : // https://book.getfoundry.sh/tutorials/solidity-scripting
+       5                 :            : pragma solidity ^0.8.20;
+       6                 :            : 
+       7                 :            : import {Script, console} from "forge-std/Script.sol";
+       8                 :            : import {RuleEngine} from "src/deployment/RuleEngine.sol";
+       9                 :            : import {RuleWhitelistMock} from "src/mocks/rules/validation/RuleWhitelistMock.sol";
+      10                 :            : import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
+      11                 :            : import {
+      12                 :            :     ValidationModuleRuleEngine
+      13                 :            : } from "CMTAT/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol";
+      14                 :            : 
+      15                 :            : /**
+      16                 :            :  * @title Example deployment of a mock RuleWhitelistMock and a RuleEngine
+      17                 :            :  * @dev This script deploys a reference/mock rule from `src/mocks/` for demo and testing flows.
+      18                 :            :  * It is not a production deployment recipe for rule contracts.
+      19                 :            :  *
+      20                 :            :  * Expects an already-deployed CMTAT at `CMTAT_ADDRESS`. The deployer must hold `DEFAULT_ADMIN_ROLE`
+      21                 :            :  * on that token, otherwise {setRuleEngine} reverts.
+      22                 :            :  *
+      23                 :            :  * The token is bound to the engine through the constructor: without it, every transfer, mint and burn
+      24                 :            :  * reverts with `RuleEngine_ERC3643Compliance_UnauthorizedCaller`, because the compliance callbacks are
+      25                 :            :  * guarded by `onlyBoundToken`.
+      26                 :            :  *
+      27                 :            :  * The deployer and the zero address are added to the whitelist so the resulting deployment is usable
+      28                 :            :  * as-is: the zero address is required for mint and burn, since the rule treats it as an ordinary
+      29                 :            :  * participant. Replace this with the real address list for anything beyond a demo.
+      30                 :            :  */
+      31                 :            : contract RuleEngineScript is Script {
+      32                 :          1 :     function run() external {
+      33                 :            :         // Get env variable
+      34                 :          1 :         uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
+      35                 :          1 :         address admin = vm.addr(deployerPrivateKey);
+      36                 :          1 :         address cmtatAddress = vm.envAddress("CMTAT_ADDRESS");
+      37                 :          1 :         vm.startBroadcast(deployerPrivateKey);
+      38                 :            :         //whitelist
+      39                 :          1 :         RuleWhitelistMock ruleWhitelist = new RuleWhitelistMock(admin, address(0));
+      40                 :          1 :         console.log("whitelist: ", address(ruleWhitelist));
+      41                 :            :         // Seed the list so the demo deployment can actually transfer, mint and burn.
+      42                 :          1 :         address[] memory listed = new address[](2);
+      43                 :          1 :         listed[0] = admin;
+      44                 :          1 :         listed[1] = address(0);
+      45                 :          1 :         ruleWhitelist.addAddressesToTheList(listed);
+      46                 :            :         // ruleEngine, bound to the CMTAT token
+      47                 :          1 :         RuleEngine ruleEngine = new RuleEngine(admin, address(0), cmtatAddress);
+      48                 :          1 :         console.log("RuleEngine: ", address(ruleEngine));
+      49                 :          1 :         ruleEngine.addRule(ruleWhitelist);
+      50                 :            :         // Configure the new ruleEngine for CMTAT.
+      51                 :            :         // A typed call is used deliberately: a low-level `.call` would return success even when
+      52                 :            :         // `cmtatAddress` holds no code, silently producing an unconfigured deployment.
+      53                 :          1 :         ValidationModuleRuleEngine(cmtatAddress).setRuleEngine(IRuleEngine(address(ruleEngine)));
+      54                 :          1 :         vm.stopBroadcast();
+      55                 :            :     }
+      56                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/script/index-sort-b.html b/doc/coverage/coverage/script/index-sort-b.html new file mode 100644 index 0000000..4eb2998 --- /dev/null +++ b/doc/coverage/coverage/script/index-sort-b.html @@ -0,0 +1,119 @@ + + + + + + + LCOV - lcov.info - script + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - scriptHitTotalCoverage
Test:lcov.infoLines:3636100.0 %
Date:2026-08-13 15:50:18Functions:22100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
RuleEngineScript.s.sol +
100.0%
+
100.0 %16 / 16100.0 %1 / 1-0 / 0
CMTATWithRuleEngineScript.s.sol +
100.0%
+
100.0 %20 / 20100.0 %1 / 1-0 / 0
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/script/index-sort-f.html b/doc/coverage/coverage/script/index-sort-f.html new file mode 100644 index 0000000..ba6c670 --- /dev/null +++ b/doc/coverage/coverage/script/index-sort-f.html @@ -0,0 +1,119 @@ + + + + + + + LCOV - lcov.info - script + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - scriptHitTotalCoverage
Test:lcov.infoLines:3636100.0 %
Date:2026-08-13 15:50:18Functions:22100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
RuleEngineScript.s.sol +
100.0%
+
100.0 %16 / 16100.0 %1 / 1-0 / 0
CMTATWithRuleEngineScript.s.sol +
100.0%
+
100.0 %20 / 20100.0 %1 / 1-0 / 0
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/script/index-sort-l.html b/doc/coverage/coverage/script/index-sort-l.html new file mode 100644 index 0000000..9bdb270 --- /dev/null +++ b/doc/coverage/coverage/script/index-sort-l.html @@ -0,0 +1,119 @@ + + + + + + + LCOV - lcov.info - script + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - scriptHitTotalCoverage
Test:lcov.infoLines:3636100.0 %
Date:2026-08-13 15:50:18Functions:22100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
RuleEngineScript.s.sol +
100.0%
+
100.0 %16 / 16100.0 %1 / 1-0 / 0
CMTATWithRuleEngineScript.s.sol +
100.0%
+
100.0 %20 / 20100.0 %1 / 1-0 / 0
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/script/index.html b/doc/coverage/coverage/script/index.html new file mode 100644 index 0000000..d8e861d --- /dev/null +++ b/doc/coverage/coverage/script/index.html @@ -0,0 +1,119 @@ + + + + + + + LCOV - lcov.info - script + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - scriptHitTotalCoverage
Test:lcov.infoLines:3636100.0 %
Date:2026-08-13 15:50:18Functions:22100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
CMTATWithRuleEngineScript.s.sol +
100.0%
+
100.0 %20 / 20100.0 %1 / 1-0 / 0
RuleEngineScript.s.sol +
100.0%
+
100.0 %16 / 16100.0 %1 / 1-0 / 0
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + 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 b18dccb..fe2eaff 100644 --- a/doc/coverage/coverage/src/RuleEngineBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/RuleEngineBase.sol.func-sort-c.html @@ -31,13 +31,13 @@ lcov.info Lines: - 49 - 49 + 51 + 51 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 14 @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 5 + 5 100.0 % @@ -69,60 +69,60 @@ Hit count Sort by hit count - RuleEngineBase.created - 4 - - - RuleEngineBase.destroyed - 4 - - - RuleEngineBase.transferred.0 + RuleEngineBase.destroyed 6 - RuleEngineBase.transferred.1 - 17 + RuleEngineBase.transferred.0 + 7 - RuleEngineBase.detectTransferRestrictionFrom - 18 + RuleEngineBase.created + 9 - RuleEngineBase._messageForTransferRestriction + RuleEngineBase.detectTransferRestrictionFrom 19 - RuleEngineBase.messageForTransferRestriction + RuleEngineBase.transferred.1 19 - RuleEngineBase.canTransferFrom - 21 + RuleEngineBase.canTransferFrom + 22 - RuleEngineBase.canTransfer - 25 + RuleEngineBase._messageForTransferRestriction + 29 - RuleEngineBase.detectTransferRestriction + RuleEngineBase.messageForTransferRestriction + 29 + + + RuleEngineBase.canTransfer 34 - RuleEngineBase._detectTransferRestrictionFrom - 39 + RuleEngineBase.detectTransferRestriction + 36 + + + RuleEngineBase._detectTransferRestrictionFrom + 41 - RuleEngineBase._supportsRuleEngineBaseInterface + RuleEngineBase._supportsRuleEngineBaseInterface 56 - RuleEngineBase._detectTransferRestriction - 59 + RuleEngineBase._detectTransferRestriction + 70 - RuleEngineBase._checkRule - 253 + RuleEngineBase._checkRule + 277
diff --git a/doc/coverage/coverage/src/RuleEngineBase.sol.func.html b/doc/coverage/coverage/src/RuleEngineBase.sol.func.html index ba1ee8b..68b56cd 100644 --- a/doc/coverage/coverage/src/RuleEngineBase.sol.func.html +++ b/doc/coverage/coverage/src/RuleEngineBase.sol.func.html @@ -31,13 +31,13 @@ lcov.info Lines: - 49 - 49 + 51 + 51 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 14 @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 5 + 5 100.0 % @@ -69,60 +69,60 @@ Hit count Sort by hit count - RuleEngineBase._checkRule - 253 + RuleEngineBase._checkRule + 277 - RuleEngineBase._detectTransferRestriction - 59 + RuleEngineBase._detectTransferRestriction + 70 - RuleEngineBase._detectTransferRestrictionFrom - 39 + RuleEngineBase._detectTransferRestrictionFrom + 41 - RuleEngineBase._messageForTransferRestriction - 19 + RuleEngineBase._messageForTransferRestriction + 29 - RuleEngineBase._supportsRuleEngineBaseInterface + RuleEngineBase._supportsRuleEngineBaseInterface 56 - RuleEngineBase.canTransfer - 25 + RuleEngineBase.canTransfer + 34 - RuleEngineBase.canTransferFrom - 21 + RuleEngineBase.canTransferFrom + 22 - RuleEngineBase.created - 4 + RuleEngineBase.created + 9 - RuleEngineBase.destroyed - 4 + RuleEngineBase.destroyed + 6 - RuleEngineBase.detectTransferRestriction - 34 + RuleEngineBase.detectTransferRestriction + 36 - RuleEngineBase.detectTransferRestrictionFrom - 18 + RuleEngineBase.detectTransferRestrictionFrom + 19 - RuleEngineBase.messageForTransferRestriction - 19 + RuleEngineBase.messageForTransferRestriction + 29 - RuleEngineBase.transferred.0 - 6 + RuleEngineBase.transferred.0 + 7 - RuleEngineBase.transferred.1 - 17 + RuleEngineBase.transferred.1 + 19
diff --git a/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html b/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html index 38a1d68..dc13b32 100644 --- a/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html +++ b/doc/coverage/coverage/src/RuleEngineBase.sol.gcov.html @@ -31,13 +31,13 @@ lcov.info Lines: - 49 - 49 + 51 + 51 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 14 @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 5 + 5 100.0 % @@ -107,182 +107,217 @@ 36 : : RuleEngineInvariantStorage, 37 : : IRuleEngineERC1404 38 : : { - 39 : : /* ============ State functions ============ */ - 40 : : /* - 41 : : * @inheritdoc IRuleEngine - 42 : : */ - 43 : 6 : function transferred(address spender, address from, address to, uint256 value) - 44 : : public - 45 : : virtual - 46 : : override(IRuleEngine) - 47 : : onlyBoundToken - 48 : : { - 49 : : // Apply on RuleEngine - 50 : 5 : RulesManagementModule._transferred(spender, from, to, value); - 51 : : } - 52 : : - 53 : : /** - 54 : : * @inheritdoc IERC3643IComplianceContract - 55 : : */ - 56 : 17 : function transferred(address from, address to, uint256 value) - 57 : : public - 58 : : virtual - 59 : : override(IERC3643IComplianceContract) - 60 : : onlyBoundToken - 61 : : { - 62 : 15 : _transferred(from, to, value); - 63 : : } - 64 : : - 65 : : /// @inheritdoc IERC3643Compliance - 66 : 4 : function created(address to, uint256 value) public virtual override(IERC3643Compliance) onlyBoundToken { - 67 : 2 : _transferred(address(0), to, value); - 68 : : } - 69 : : - 70 : : /// @inheritdoc IERC3643Compliance - 71 : 4 : function destroyed(address from, uint256 value) public virtual override(IERC3643Compliance) onlyBoundToken { - 72 : 2 : _transferred(from, address(0), value); + 39 : : /* ============ State variables ============ */ + 40 : : /** + 41 : : * @dev ERC-1404 reserves the code 0 as the "no restriction" sentinel. It is never claimed by a rule, + 42 : : * so it is answered here instead of being reported as an unknown code. + 43 : : * The message matches the one returned by CMTAT (ValidationModuleERC1404) for the same code. + 44 : : */ + 45 : : string private constant TEXT_TRANSFER_OK = "NoRestriction"; + 46 : : /// @dev Returned when no active rule claims the restriction code + 47 : : string private constant TEXT_CODE_NOT_FOUND = "Unknown restriction code"; + 48 : : + 49 : : /* ============ State functions ============ */ + 50 : : /** + 51 : : * @inheritdoc IRuleEngine + 52 : : */ + 53 : 7 : function transferred(address spender, address from, address to, uint256 value) + 54 : : public + 55 : : virtual + 56 : : override(IRuleEngine) + 57 : : onlyBoundToken + 58 : : { + 59 : : // Apply on RuleEngine + 60 : 6 : RulesManagementModule._transferred(spender, from, to, value); + 61 : : } + 62 : : + 63 : : /** + 64 : : * @inheritdoc IERC3643IComplianceContract + 65 : : */ + 66 : 19 : function transferred(address from, address to, uint256 value) + 67 : : public + 68 : : virtual + 69 : : override(IERC3643IComplianceContract) + 70 : : onlyBoundToken + 71 : : { + 72 : 16 : _transferred(from, to, value); 73 : : } 74 : : - 75 : : /* ============ View functions ============ */ - 76 : : /** - 77 : : * @notice Go through all the rule to know if a restriction exists on the transfer - 78 : : * @param from the origin address - 79 : : * @param to the destination address - 80 : : * @param value to transfer - 81 : : * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK (0) if the transfer is valid - 82 : : * - 83 : : */ - 84 : 34 : function detectTransferRestriction(address from, address to, uint256 value) - 85 : : public - 86 : : view - 87 : : virtual - 88 : : override(IERC1404) - 89 : : returns (uint8) - 90 : : { - 91 : 59 : return _detectTransferRestriction(from, to, value); - 92 : : } - 93 : : - 94 : : /** - 95 : : * @inheritdoc IERC1404Extend - 96 : : */ - 97 : 18 : function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 98 : : public - 99 : : view - 100 : : virtual - 101 : : override(IERC1404Extend) - 102 : : returns (uint8) - 103 : : { - 104 : 39 : return _detectTransferRestrictionFrom(spender, from, to, value); - 105 : : } - 106 : : - 107 : : /** - 108 : : * @inheritdoc IERC1404 - 109 : : */ - 110 : 19 : function messageForTransferRestriction(uint8 restrictionCode) - 111 : : public - 112 : : view - 113 : : virtual - 114 : : override(IERC1404) - 115 : : returns (string memory) - 116 : : { - 117 : 19 : return _messageForTransferRestriction(restrictionCode); - 118 : : } - 119 : : - 120 : : /** - 121 : : * @inheritdoc IERC3643ComplianceRead - 122 : : */ - 123 : 25 : function canTransfer(address from, address to, uint256 value) - 124 : : public - 125 : : view - 126 : : virtual - 127 : : override(IERC3643ComplianceRead) - 128 : : returns (bool) - 129 : : { - 130 : 25 : return detectTransferRestriction(from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); - 131 : : } - 132 : : - 133 : : /** - 134 : : * @inheritdoc IERC7551Compliance - 135 : : */ - 136 : 21 : function canTransferFrom(address spender, address from, address to, uint256 value) - 137 : : public - 138 : : view - 139 : : virtual - 140 : : override(IERC7551Compliance) - 141 : : returns (bool) - 142 : : { - 143 : 21 : return detectTransferRestrictionFrom(spender, from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); - 144 : : } - 145 : : - 146 : : /*////////////////////////////////////////////////////////////// - 147 : : INTERNAL/PRIVATE FUNCTIONS - 148 : : //////////////////////////////////////////////////////////////*/ - 149 : 59 : function _detectTransferRestriction(address from, address to, uint256 value) internal view virtual returns (uint8) { - 150 : 59 : uint256 rulesLength = rulesCount(); - 151 : 59 : for (uint256 i = 0; i < rulesLength; ++i) { - 152 : 59 : uint8 restriction = IRule(rule(i)).detectTransferRestriction(from, to, value); - 153 [ + ]: 59 : if (restriction > 0) { - 154 : 43 : return restriction; - 155 : : } - 156 : : } - 157 : 16 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); - 158 : : } - 159 : : - 160 : 39 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 161 : : internal - 162 : : view - 163 : : virtual - 164 : : returns (uint8) - 165 : : { - 166 : 39 : uint256 rulesLength = rulesCount(); - 167 : 39 : for (uint256 i = 0; i < rulesLength; ++i) { - 168 : 39 : uint8 restriction = IRule(rule(i)).detectTransferRestrictionFrom(spender, from, to, value); - 169 [ + ]: 39 : if (restriction > 0) { - 170 : 29 : return restriction; - 171 : : } - 172 : : } - 173 : 10 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); - 174 : : } - 175 : : - 176 : : /** - 177 : : * @dev This function returns the message from the first rule claiming the code. - 178 : : * Rule designers should keep restriction codes unique across rules. - 179 : : * If a code is shared intentionally, all rules using that code should return - 180 : : * the same message to avoid ambiguous operator feedback. - 181 : : */ - 182 : 19 : function _messageForTransferRestriction(uint8 restrictionCode) internal view virtual returns (string memory) { - 183 : 19 : uint256 rulesLength = rulesCount(); - 184 : 19 : for (uint256 i = 0; i < rulesLength; ++i) { - 185 [ + ]: 16 : if (IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)) { - 186 : 14 : return IRule(rule(i)).messageForTransferRestriction(restrictionCode); - 187 : : } - 188 : : } - 189 : 5 : return "Unknown restriction code"; - 190 : : } - 191 : : - 192 : : /** - 193 : : * @dev Override to add ERC-165 interface check for the full IRule hierarchy. - 194 : : */ - 195 : 253 : function _checkRule(address rule_) internal view virtual override { - 196 : 253 : RulesManagementModule._checkRule(rule_); - 197 [ + ]: 244 : if (!ERC165Checker.supportsInterface(rule_, RuleInterfaceId.IRULE_INTERFACE_ID)) { - 198 : 6 : revert RuleEngine_RuleInvalidInterface(); - 199 : : } - 200 : : } - 201 : : - 202 : : /** - 203 : : * @dev Shared ERC-165 checks common to all RuleEngine deployment variants. - 204 : : * Concrete deployments can extend this with access-control-specific interfaces. - 205 : : */ - 206 : 56 : function _supportsRuleEngineBaseInterface(bytes4 interfaceId) internal pure returns (bool) { - 207 : 56 : return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID - 208 : 51 : || interfaceId == ERC1404InterfaceId.IERC1404_INTERFACE_ID - 209 : 41 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID - 210 : 36 : || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID - 211 : 31 : || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_EXTENDED_INTERFACE_ID - 212 : 21 : || interfaceId == ComplianceInterfaceId.IERC7551_COMPLIANCE_INTERFACE_ID; - 213 : : } - 214 : : } + 75 : : /// @inheritdoc IERC3643Compliance + 76 : 9 : function created(address to, uint256 value) public virtual override(IERC3643Compliance) onlyBoundToken { + 77 : 6 : _transferred(address(0), to, value); + 78 : : } + 79 : : + 80 : : /// @inheritdoc IERC3643Compliance + 81 : 6 : function destroyed(address from, uint256 value) public virtual override(IERC3643Compliance) onlyBoundToken { + 82 : 3 : _transferred(from, address(0), value); + 83 : : } + 84 : : + 85 : : /* ============ View functions ============ */ + 86 : : /** + 87 : : * @notice Go through all the rule to know if a restriction exists on the transfer + 88 : : * @param from the origin address + 89 : : * @param to the destination address + 90 : : * @param value to transfer + 91 : : * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK (0) if the transfer is valid + 92 : : * + 93 : : */ + 94 : 36 : function detectTransferRestriction(address from, address to, uint256 value) + 95 : : public + 96 : : view + 97 : : virtual + 98 : : override(IERC1404) + 99 : : returns (uint8) + 100 : : { + 101 : 70 : return _detectTransferRestriction(from, to, value); + 102 : : } + 103 : : + 104 : : /** + 105 : : * @inheritdoc IERC1404Extend + 106 : : */ + 107 : 19 : function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 108 : : public + 109 : : view + 110 : : virtual + 111 : : override(IERC1404Extend) + 112 : : returns (uint8) + 113 : : { + 114 : 41 : return _detectTransferRestrictionFrom(spender, from, to, value); + 115 : : } + 116 : : + 117 : : /** + 118 : : * @inheritdoc IERC1404 + 119 : : */ + 120 : 29 : function messageForTransferRestriction(uint8 restrictionCode) + 121 : : public + 122 : : view + 123 : : virtual + 124 : : override(IERC1404) + 125 : : returns (string memory) + 126 : : { + 127 : 29 : return _messageForTransferRestriction(restrictionCode); + 128 : : } + 129 : : + 130 : : /** + 131 : : * @inheritdoc IERC3643ComplianceRead + 132 : : */ + 133 : 34 : function canTransfer(address from, address to, uint256 value) + 134 : : public + 135 : : view + 136 : : virtual + 137 : : override(IERC3643ComplianceRead) + 138 : : returns (bool) + 139 : : { + 140 : 34 : return detectTransferRestriction(from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 141 : : } + 142 : : + 143 : : /** + 144 : : * @inheritdoc IERC7551Compliance + 145 : : */ + 146 : 22 : function canTransferFrom(address spender, address from, address to, uint256 value) + 147 : : public + 148 : : view + 149 : : virtual + 150 : : override(IERC7551Compliance) + 151 : : returns (bool) + 152 : : { + 153 : 22 : return detectTransferRestrictionFrom(spender, from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 154 : : } + 155 : : + 156 : : /*////////////////////////////////////////////////////////////// + 157 : : INTERNAL/PRIVATE FUNCTIONS + 158 : : //////////////////////////////////////////////////////////////*/ + 159 : : /** + 160 : : * @notice Returns the first non-zero restriction code reported by the configured rules. + 161 : : * @param from the origin address + 162 : : * @param to the destination address + 163 : : * @param value the amount to transfer + 164 : : * @return The first non-zero ERC-1404 restriction code, or TRANSFER_OK when every rule allows it. + 165 : : */ + 166 : 70 : function _detectTransferRestriction(address from, address to, uint256 value) internal view virtual returns (uint8) { + 167 : 70 : uint256 rulesLength = rulesCount(); + 168 : 70 : for (uint256 i = 0; i < rulesLength; ++i) { + 169 : 72 : uint8 restriction = IRule(rule(i)).detectTransferRestriction(from, to, value); + 170 [ + ]: 72 : if (restriction > 0) { + 171 : 47 : return restriction; + 172 : : } + 173 : : } + 174 : 23 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 175 : : } + 176 : : + 177 : : /** + 178 : : * @notice Returns the first non-zero restriction code for a spender-initiated transfer. + 179 : : * @param spender the spender address (transferFrom) + 180 : : * @param from the origin address + 181 : : * @param to the destination address + 182 : : * @param value the amount to transfer + 183 : : * @return The first non-zero ERC-1404 restriction code, or TRANSFER_OK when every rule allows it. + 184 : : */ + 185 : 41 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 186 : : internal + 187 : : view + 188 : : virtual + 189 : : returns (uint8) + 190 : : { + 191 : 41 : uint256 rulesLength = rulesCount(); + 192 : 41 : for (uint256 i = 0; i < rulesLength; ++i) { + 193 : 43 : uint8 restriction = IRule(rule(i)).detectTransferRestrictionFrom(spender, from, to, value); + 194 [ + ]: 43 : if (restriction > 0) { + 195 : 31 : return restriction; + 196 : : } + 197 : : } + 198 : 10 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 199 : : } + 200 : : + 201 : : /** + 202 : : * @dev This function returns the message from the first rule claiming the code. + 203 : : * Rule designers should keep restriction codes unique across rules. + 204 : : * If a code is shared intentionally, all rules using that code should return + 205 : : * the same message to avoid ambiguous operator feedback. + 206 : : * The reserved code 0 (REJECTED_CODE_BASE.TRANSFER_OK) is answered before the rules are queried, + 207 : : * so that a valid transfer is never reported as an unknown restriction code. + 208 : : * @param restrictionCode The target restriction code. + 209 : : * @return The message of the first rule claiming the code, or a default message when none does. + 210 : : */ + 211 : 29 : function _messageForTransferRestriction(uint8 restrictionCode) internal view virtual returns (string memory) { + 212 [ + ]: 29 : if (restrictionCode == uint8(REJECTED_CODE_BASE.TRANSFER_OK)) { + 213 : 8 : return TEXT_TRANSFER_OK; + 214 : : } + 215 : 21 : uint256 rulesLength = rulesCount(); + 216 : 21 : for (uint256 i = 0; i < rulesLength; ++i) { + 217 [ + ]: 18 : if (IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)) { + 218 : 14 : return IRule(rule(i)).messageForTransferRestriction(restrictionCode); + 219 : : } + 220 : : } + 221 : 7 : return TEXT_CODE_NOT_FOUND; + 222 : : } + 223 : : + 224 : : /** + 225 : : * @dev Override to add ERC-165 interface check for the full IRule hierarchy. + 226 : : * @param rule_ The candidate rule address to validate. + 227 : : */ + 228 : 277 : function _checkRule(address rule_) internal view virtual override { + 229 : 277 : RulesManagementModule._checkRule(rule_); + 230 [ + ]: 268 : if (!ERC165Checker.supportsInterface(rule_, RuleInterfaceId.IRULE_INTERFACE_ID)) { + 231 : 6 : revert RuleEngine_RuleInvalidInterface(); + 232 : : } + 233 : : } + 234 : : + 235 : : /** + 236 : : * @dev Shared ERC-165 checks common to all RuleEngine deployment variants. + 237 : : * Concrete deployments can extend this with access-control-specific interfaces. + 238 : : * @param interfaceId The interface identifier to check. + 239 : : * @return True if the interface is part of the shared RuleEngine base, false otherwise. + 240 : : */ + 241 : 56 : function _supportsRuleEngineBaseInterface(bytes4 interfaceId) internal pure virtual returns (bool) { + 242 : 56 : return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + 243 : 51 : || interfaceId == ERC1404InterfaceId.IERC1404_INTERFACE_ID + 244 : 41 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + 245 : 36 : || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID + 246 : 31 : || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_EXTENDED_INTERFACE_ID + 247 : 21 : || interfaceId == ComplianceInterfaceId.IERC7551_COMPLIANCE_INTERFACE_ID; + 248 : : } + 249 : : } diff --git a/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func-sort-c.html b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func-sort-c.html index 290bad8..b25b11e 100644 --- a/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func-sort-c.html @@ -31,13 +31,13 @@ lcov.info Lines: - 15 - 15 + 16 + 16 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 6 @@ -69,28 +69,28 @@ Hit count Sort by hit count - RuleEngineOwnableShared._msgData + RuleEngineOwnableShared._msgData 2 - RuleEngineOwnableShared._checkOwnershipTransferTarget + RuleEngineOwnableShared._checkOwnershipTransferTarget 10 - RuleEngineOwnableShared.supportsInterface + RuleEngineOwnableShared.supportsInterface 35 - RuleEngineOwnableShared.constructor - 175 + RuleEngineOwnableShared.constructor + 186 - RuleEngineOwnableShared._msgSender - 249 + RuleEngineOwnableShared._msgSender + 255 - RuleEngineOwnableShared._contextSuffixLength - 251 + RuleEngineOwnableShared._contextSuffixLength + 257
diff --git a/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func.html b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func.html index 1184f16..e50dcd7 100644 --- a/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func.html +++ b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.func.html @@ -31,13 +31,13 @@ lcov.info Lines: - 15 - 15 + 16 + 16 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 6 @@ -69,27 +69,27 @@ Hit count Sort by hit count - RuleEngineOwnableShared._checkOwnershipTransferTarget + RuleEngineOwnableShared._checkOwnershipTransferTarget 10 - RuleEngineOwnableShared._contextSuffixLength - 251 + RuleEngineOwnableShared._contextSuffixLength + 257 - RuleEngineOwnableShared._msgData + RuleEngineOwnableShared._msgData 2 - RuleEngineOwnableShared._msgSender - 249 + RuleEngineOwnableShared._msgSender + 255 - RuleEngineOwnableShared.constructor - 175 + RuleEngineOwnableShared.constructor + 186 - RuleEngineOwnableShared.supportsInterface + RuleEngineOwnableShared.supportsInterface 35 diff --git a/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.gcov.html b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.gcov.html index 78dc115..95c3ed4 100644 --- a/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.gcov.html +++ b/doc/coverage/coverage/src/RuleEngineOwnableShared.sol.gcov.html @@ -31,13 +31,13 @@ lcov.info Lines: - 15 - 15 + 16 + 16 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 6 @@ -90,52 +90,68 @@ 19 : : * (`Ownable` or `Ownable2Step`) while reusing constructor, ERC-165 and ERC-2771 code. 20 : : */ 21 : : abstract contract RuleEngineOwnableShared is ERC2771ModuleStandalone, RuleEngineBase, ERC165 { - 22 : 175 : constructor(address forwarderIrrevocable, address tokenContract) ERC2771ModuleStandalone(forwarderIrrevocable) { - 23 [ + ]: 175 : if (tokenContract != address(0)) { - 24 : 1 : _bindToken(tokenContract); - 25 : : } - 26 : : } - 27 : : - 28 : : /* ============ ERC-165 ============ */ - 29 : 35 : function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { - 30 : 35 : 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 + 22 : : /** + 23 : : * @notice Sets the trusted forwarder and optionally binds an initial token. + 24 : : * @param forwarderIrrevocable Address of the trusted ERC-2771 forwarder, immutable after construction. + 25 : : * @param tokenContract Token to bind at deployment, or the zero address to bind none. + 26 : : */ + 27 : 186 : constructor(address forwarderIrrevocable, address tokenContract) ERC2771ModuleStandalone(forwarderIrrevocable) { + 28 [ + ]: 186 : if (tokenContract != address(0)) { + 29 : 1 : _bindToken(tokenContract); + 30 : : } + 31 : : // Emit the initial cap so the event log alone is enough to reconstruct maxRules. + 32 : 186 : emit SetMaxRules(_maxRules); + 33 : : } + 34 : : + 35 : : /* ============ ERC-165 ============ */ + 36 : : /** + 37 : : * @notice ERC-165 interface detection. + 38 : : * @param interfaceId The interface identifier to check. + 39 : : * @return True if the interface is supported, false otherwise. + 40 : : */ + 41 : 35 : function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + 42 : 35 : return _supportsRuleEngineBaseInterface(interfaceId) || interfaceId == OwnableInterfaceId.IERC173_INTERFACE_ID + 43 : 5 : || ERC165.supportsInterface(interfaceId); + 44 : : } + 45 : : + 46 : : /** + 47 : : * @dev Shared guard for ownership transfer targets in ownable variants. + 48 : : * @param newOwner The candidate new owner; must not be a configured rule. 49 : : */ - 50 : 249 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { - 51 : 249 : 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 + 50 : 10 : function _checkOwnershipTransferTarget(address newOwner) internal view virtual { + 51 [ + ]: 10 : if (containsRule(IRule(newOwner))) { + 52 : 2 : revert RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges(); + 53 : : } + 54 : : } + 55 : : + 56 : : /*////////////////////////////////////////////////////////////// + 57 : : ERC-2771 + 58 : : //////////////////////////////////////////////////////////////*/ + 59 : : + 60 : : /** + 61 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 62 : : * @return sender The transaction sender, unwrapped from the forwarder calldata when relayed. 63 : : */ - 64 : 251 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { - 65 : 251 : return ERC2771Context._contextSuffixLength(); + 64 : 255 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { + 65 : 255 : return ERC2771Context._msgSender(); 66 : : } - 67 : : } + 67 : : + 68 : : /** + 69 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 70 : : * @return The transaction calldata, with the appended sender stripped when relayed. + 71 : : */ + 72 : 2 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { + 73 : 2 : return ERC2771Context._msgData(); + 74 : : } + 75 : : + 76 : : /** + 77 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 78 : : * @return The length of the ERC-2771 calldata suffix. + 79 : : */ + 80 : 257 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { + 81 : 257 : return ERC2771Context._contextSuffixLength(); + 82 : : } + 83 : : } 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 ab51881..ef64590 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,13 +31,13 @@ lcov.info Lines: - 25 - 25 + 26 + 26 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 10 @@ -69,44 +69,44 @@ Hit count Sort by hit count - RuleEngine._msgData + RuleEngine._msgData 1 - RuleEngine._onlyRulesLimitManager + RuleEngine._onlyRulesLimitManager 5 - RuleEngine.supportsInterface + RuleEngine.supportsInterface 21 - RuleEngine.grantRole + RuleEngine.grantRole 39 - RuleEngine._onlyComplianceManager - 42 + RuleEngine._onlyComplianceManager + 56 - RuleEngine.constructor - 165 + RuleEngine.constructor + 184 - RuleEngine.hasRole - 173 + RuleEngine.hasRole + 192 - RuleEngine._onlyRulesManager - 196 + RuleEngine._onlyRulesManager + 217 - RuleEngine._msgSender - 535 + RuleEngine._msgSender + 614 - RuleEngine._contextSuffixLength - 536 + RuleEngine._contextSuffixLength + 615
diff --git a/doc/coverage/coverage/src/deployment/RuleEngine.sol.func.html b/doc/coverage/coverage/src/deployment/RuleEngine.sol.func.html index aff6062..064dab1 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngine.sol.func.html +++ b/doc/coverage/coverage/src/deployment/RuleEngine.sol.func.html @@ -31,13 +31,13 @@ lcov.info Lines: - 25 - 25 + 26 + 26 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 10 @@ -69,43 +69,43 @@ Hit count Sort by hit count - RuleEngine._contextSuffixLength - 536 + RuleEngine._contextSuffixLength + 615 - RuleEngine._msgData + RuleEngine._msgData 1 - RuleEngine._msgSender - 535 + RuleEngine._msgSender + 614 - RuleEngine._onlyComplianceManager - 42 + RuleEngine._onlyComplianceManager + 56 - RuleEngine._onlyRulesLimitManager + RuleEngine._onlyRulesLimitManager 5 - RuleEngine._onlyRulesManager - 196 + RuleEngine._onlyRulesManager + 217 - RuleEngine.constructor - 165 + RuleEngine.constructor + 184 - RuleEngine.grantRole + RuleEngine.grantRole 39 - RuleEngine.hasRole - 173 + RuleEngine.hasRole + 192 - RuleEngine.supportsInterface + RuleEngine.supportsInterface 21 diff --git a/doc/coverage/coverage/src/deployment/RuleEngine.sol.gcov.html b/doc/coverage/coverage/src/deployment/RuleEngine.sol.gcov.html index 60912ad..2e81e24 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngine.sol.gcov.html +++ b/doc/coverage/coverage/src/deployment/RuleEngine.sol.gcov.html @@ -31,13 +31,13 @@ lcov.info Lines: - 25 - 25 + 26 + 26 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 10 @@ -82,104 +82,140 @@ 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 : : - 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 : 165 : constructor(address admin, address forwarderIrrevocable, address tokenContract) - 28 : : ERC2771ModuleStandalone(forwarderIrrevocable) - 29 : : { - 30 [ + ]: 165 : if (admin == address(0)) { - 31 : 1 : revert RuleEngine_AdminWithAddressZeroNotAllowed(); - 32 : : } - 33 [ + ]: 164 : if (tokenContract != address(0)) { - 34 : 31 : _bindToken(tokenContract); - 35 : : } - 36 : 164 : _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 currently configured as rules. - 43 : : * Note: this check is intentionally one-directional. {addRule} does not verify - 44 : : * whether the rule address already holds a privileged role, and this function does - 45 : : * not prevent adding a privileged address as a rule afterwards. Operators are - 46 : : * responsible for keeping rule contracts and privileged accounts disjoint. - 47 : : */ - 48 : 39 : function grantRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { - 49 [ + ]: 39 : if (_rules.contains(account)) { - 50 : 3 : revert RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges(); - 51 : : } - 52 : 36 : AccessControl.grantRole(role, account); - 53 : : } - 54 : : - 55 : : /** - 56 : : * @notice Returns `true` if `account` has been granted `role`. - 57 : : * @dev The Default Admin has all roles - 58 : : */ - 59 : 173 : function hasRole(bytes32 role, address account) - 60 : : public - 61 : : view - 62 : : virtual - 63 : : override(AccessControl, IAccessControl) - 64 : : returns (bool) - 65 : : { - 66 [ + + ]: 490 : if (AccessControl.hasRole(DEFAULT_ADMIN_ROLE, account)) { - 67 : 236 : return true; - 68 : : } else { - 69 : 254 : return AccessControl.hasRole(role, account); - 70 : : } - 71 : : } - 72 : : - 73 : : /* ============ ERC-165 ============ */ - 74 : 21 : function supportsInterface(bytes4 interfaceId) - 75 : : public - 76 : : view - 77 : : virtual - 78 : : override(AccessControlEnumerable, IERC165) - 79 : : returns (bool) - 80 : : { - 81 : 21 : return _supportsRuleEngineBaseInterface(interfaceId) || AccessControlEnumerable.supportsInterface(interfaceId); - 82 : : } - 83 : : - 84 : : /*////////////////////////////////////////////////////////////// - 85 : : ERC-2771 - 86 : : //////////////////////////////////////////////////////////////*/ - 87 : 42 : function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} - 88 : 196 : function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} - 89 : 5 : function _onlyRulesLimitManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} - 90 : : + 14 : : import {ERC3643ComplianceRolesStorage} from "../modules/library/ERC3643ComplianceRolesStorage.sol"; + 15 : : import {RulesManagementModuleRolesStorage} from "../modules/library/RulesManagementModuleRolesStorage.sol"; + 16 : : /* ==== Base contract === */ + 17 : : import {RuleEngineBase} from "../RuleEngineBase.sol"; + 18 : : + 19 : : /** + 20 : : * @title Implementation of a ruleEngine as defined by the CMTAT + 21 : : */ + 22 : : contract RuleEngine is + 23 : : ERC2771ModuleStandalone, + 24 : : RuleEngineBase, + 25 : : AccessControlEnumerable, + 26 : : ERC3643ComplianceRolesStorage, + 27 : : RulesManagementModuleRolesStorage + 28 : : { + 29 : : using EnumerableSet for EnumerableSet.AddressSet; + 30 : : + 31 : : /** + 32 : : * @notice Deploys the RBAC RuleEngine. + 33 : : * @param admin Address of the contract (Access Control) + 34 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + 35 : : * @param tokenContract Token to bind at deployment, or the zero address to bind none. + 36 : : */ + 37 : 184 : constructor(address admin, address forwarderIrrevocable, address tokenContract) + 38 : : ERC2771ModuleStandalone(forwarderIrrevocable) + 39 : : { + 40 [ + ]: 184 : if (admin == address(0)) { + 41 : 1 : revert RuleEngine_AdminWithAddressZeroNotAllowed(); + 42 : : } + 43 [ + ]: 183 : if (tokenContract != address(0)) { + 44 : 32 : _bindToken(tokenContract); + 45 : : } + 46 : 183 : _grantRole(DEFAULT_ADMIN_ROLE, admin); + 47 : : // Emit the initial cap so the event log alone is enough to reconstruct maxRules. + 48 : 183 : emit SetMaxRules(_maxRules); + 49 : : } + 50 : : + 51 : : /* ============ ACCESS CONTROL ============ */ + 52 : : /** + 53 : : * @notice Grants `role` to `account`. + 54 : : * @dev Prevents granting any role to accounts currently configured as rules. + 55 : : * Note: this check is intentionally one-directional. {addRule} does not verify + 56 : : * whether the rule address already holds a privileged role, and this function does + 57 : : * not prevent adding a privileged address as a rule afterwards. Operators are + 58 : : * responsible for keeping rule contracts and privileged accounts disjoint. + 59 : : * @param role The role identifier to grant. + 60 : : * @param account The account receiving the role. + 61 : : */ + 62 : 39 : function grantRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { + 63 [ + ]: 39 : if (_rules.contains(account)) { + 64 : 3 : revert RuleEngine_RulesManagementModule_RuleAccountCannotReceivePrivileges(); + 65 : : } + 66 : 36 : AccessControl.grantRole(role, account); + 67 : : } + 68 : : + 69 : : /** + 70 : : * @notice Returns `true` if `account` has been granted `role`. + 71 : : * @dev The Default Admin has all roles + 72 : : * @param role The role identifier to check. + 73 : : * @param account The account to check. + 74 : : * @return True if the account holds the role (or is the default admin), false otherwise. + 75 : : */ + 76 : 192 : function hasRole(bytes32 role, address account) + 77 : : public + 78 : : view + 79 : : virtual + 80 : : override(AccessControl, IAccessControl) + 81 : : returns (bool) + 82 : : { + 83 [ + + ]: 544 : if (AccessControl.hasRole(DEFAULT_ADMIN_ROLE, account)) { + 84 : 270 : return true; + 85 : : } else { + 86 : 274 : return AccessControl.hasRole(role, account); + 87 : : } + 88 : : } + 89 : : + 90 : : /* ============ ERC-165 ============ */ 91 : : /** - 92 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 93 : : */ - 94 : 535 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { - 95 : 535 : return ERC2771Context._msgSender(); - 96 : : } - 97 : : - 98 : : /** - 99 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 100 : : */ - 101 : 1 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { - 102 : 1 : return ERC2771Context._msgData(); - 103 : : } - 104 : : - 105 : : /** - 106 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 107 : : */ - 108 : 536 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { - 109 : 536 : return ERC2771Context._contextSuffixLength(); - 110 : : } - 111 : : } + 92 : : * @notice ERC-165 interface detection. + 93 : : * @param interfaceId The interface identifier to check. + 94 : : * @return True if the interface is supported, false otherwise. + 95 : : */ + 96 : 21 : function supportsInterface(bytes4 interfaceId) + 97 : : public + 98 : : view + 99 : : virtual + 100 : : override(AccessControlEnumerable, IERC165) + 101 : : returns (bool) + 102 : : { + 103 : 21 : return _supportsRuleEngineBaseInterface(interfaceId) || AccessControlEnumerable.supportsInterface(interfaceId); + 104 : : } + 105 : : + 106 : : /*////////////////////////////////////////////////////////////// + 107 : : ERC-2771 + 108 : : //////////////////////////////////////////////////////////////*/ + 109 : : /** + 110 : : * @dev Access control check restricting compliance operations to COMPLIANCE_MANAGER_ROLE. + 111 : : */ + 112 : 56 : function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + 113 : : + 114 : : /** + 115 : : * @dev Access control check restricting rule management to RULES_MANAGEMENT_ROLE. + 116 : : */ + 117 : 217 : function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + 118 : : + 119 : : /** + 120 : : * @dev Access control check restricting the rule cap update to DEFAULT_ADMIN_ROLE. + 121 : : */ + 122 : 5 : function _onlyRulesLimitManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + 123 : : + 124 : : /** + 125 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 126 : : * @return sender The transaction sender, unwrapped from the forwarder calldata when relayed. + 127 : : */ + 128 : 614 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { + 129 : 614 : return ERC2771Context._msgSender(); + 130 : : } + 131 : : + 132 : : /** + 133 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 134 : : * @return The transaction calldata, with the appended sender stripped when relayed. + 135 : : */ + 136 : 1 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { + 137 : 1 : return ERC2771Context._msgData(); + 138 : : } + 139 : : + 140 : : /** + 141 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 142 : : * @return The length of the ERC-2771 calldata suffix. + 143 : : */ + 144 : 615 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { + 145 : 615 : return ERC2771Context._contextSuffixLength(); + 146 : : } + 147 : : } 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 a1087f6..59b93bf 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 @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 7 @@ -69,32 +69,32 @@ Hit count Sort by hit count - RuleEngineOwnable._msgData + RuleEngineOwnable._msgData 1 - RuleEngineOwnable._onlyRulesLimitManager + RuleEngineOwnable._onlyRulesLimitManager 2 - RuleEngineOwnable.transferOwnership + RuleEngineOwnable.transferOwnership 5 - RuleEngineOwnable._onlyComplianceManager + RuleEngineOwnable._onlyComplianceManager 46 - RuleEngineOwnable._onlyRulesManager - 65 + RuleEngineOwnable._onlyRulesManager + 69 - RuleEngineOwnable._msgSender - 181 + RuleEngineOwnable._msgSender + 185 - RuleEngineOwnable._contextSuffixLength - 182 + RuleEngineOwnable._contextSuffixLength + 186
diff --git a/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func.html index 214bcbb..6064cc1 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func.html +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 7 @@ -69,31 +69,31 @@ Hit count Sort by hit count - RuleEngineOwnable._contextSuffixLength - 182 + RuleEngineOwnable._contextSuffixLength + 186 - RuleEngineOwnable._msgData + RuleEngineOwnable._msgData 1 - RuleEngineOwnable._msgSender - 181 + RuleEngineOwnable._msgSender + 185 - RuleEngineOwnable._onlyComplianceManager + RuleEngineOwnable._onlyComplianceManager 46 - RuleEngineOwnable._onlyRulesLimitManager + RuleEngineOwnable._onlyRulesLimitManager 2 - RuleEngineOwnable._onlyRulesManager - 65 + RuleEngineOwnable._onlyRulesManager + 69 - RuleEngineOwnable.transferOwnership + 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 3a77998..5dd382d 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.gcov.html +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 7 @@ -91,48 +91,56 @@ 20 : : Ownable(owner_) 21 : : {} 22 : : - 23 : : /* ============ ACCESS CONTROL ============ */ - 24 : : /** - 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 : 46 : 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 : : } + 23 : : /** + 24 : : * @notice Transfers ownership of the contract to a new account (`newOwner`). + 25 : : * @dev Reverts when `newOwner` is already configured as a rule. + 26 : : * @param newOwner The address of the new owner. + 27 : : */ + 28 : 5 : function transferOwnership(address newOwner) public virtual override onlyOwner { + 29 : 4 : RuleEngineOwnableShared._checkOwnershipTransferTarget(newOwner); + 30 : 3 : Ownable.transferOwnership(newOwner); + 31 : : } + 32 : : + 33 : : /* ============ ACCESS CONTROL ============ */ + 34 : : /** + 35 : : * @dev Access control check using Ownable pattern + 36 : : */ + 37 : 69 : function _onlyRulesManager() internal virtual override onlyOwner {} + 38 : : + 39 : : /** + 40 : : * @dev Access control check using Ownable pattern + 41 : : */ + 42 : 2 : function _onlyRulesLimitManager() internal virtual override onlyOwner {} 43 : : 44 : : /** - 45 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 45 : : * @dev Access control check using Ownable pattern 46 : : */ - 47 : 181 : function _msgSender() internal view virtual override(RuleEngineOwnableShared, Context) returns (address sender) { - 48 : 181 : return RuleEngineOwnableShared._msgSender(); - 49 : : } - 50 : : - 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 + 47 : 46 : function _onlyComplianceManager() internal virtual override onlyOwner {} + 48 : : + 49 : : /** + 50 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 51 : : * @return sender The transaction sender, unwrapped from the forwarder calldata when relayed. + 52 : : */ + 53 : 185 : function _msgSender() internal view virtual override(RuleEngineOwnableShared, Context) returns (address sender) { + 54 : 185 : return RuleEngineOwnableShared._msgSender(); + 55 : : } + 56 : : + 57 : : /** + 58 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 59 : : * @return The transaction calldata, with the appended sender stripped when relayed. 60 : : */ - 61 : 182 : function _contextSuffixLength() internal view virtual override(RuleEngineOwnableShared, Context) returns (uint256) { - 62 : 182 : return RuleEngineOwnableShared._contextSuffixLength(); + 61 : 1 : function _msgData() internal view virtual override(RuleEngineOwnableShared, Context) returns (bytes calldata) { + 62 : 1 : return RuleEngineOwnableShared._msgData(); 63 : : } - 64 : : } + 64 : : + 65 : : /** + 66 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 67 : : * @return The length of the ERC-2771 calldata suffix. + 68 : : */ + 69 : 186 : function _contextSuffixLength() internal view virtual override(RuleEngineOwnableShared, Context) returns (uint256) { + 70 : 186 : return RuleEngineOwnableShared._contextSuffixLength(); + 71 : : } + 72 : : } 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 index b67c9fb..0a7e92c 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 8 @@ -69,36 +69,36 @@ Hit count Sort by hit count - RuleEngineOwnable2Step._msgData + RuleEngineOwnable2Step._msgData 1 - RuleEngineOwnable2Step._onlyRulesLimitManager + RuleEngineOwnable2Step._onlyRulesLimitManager 2 - RuleEngineOwnable2Step._onlyRulesManager - 5 + RuleEngineOwnable2Step.transferOwnership + 6 - RuleEngineOwnable2Step.transferOwnership - 6 + RuleEngineOwnable2Step._onlyRulesManager + 7 - RuleEngineOwnable2Step.supportsInterface + RuleEngineOwnable2Step.supportsInterface 13 - RuleEngineOwnable2Step._onlyComplianceManager + RuleEngineOwnable2Step._onlyComplianceManager 25 - RuleEngineOwnable2Step._msgSender - 68 + RuleEngineOwnable2Step._msgSender + 70 - RuleEngineOwnable2Step._contextSuffixLength - 69 + RuleEngineOwnable2Step._contextSuffixLength + 71
diff --git a/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func.html index 5228298..b7aa04b 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func.html +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 8 @@ -69,35 +69,35 @@ Hit count Sort by hit count - RuleEngineOwnable2Step._contextSuffixLength - 69 + RuleEngineOwnable2Step._contextSuffixLength + 71 - RuleEngineOwnable2Step._msgData + RuleEngineOwnable2Step._msgData 1 - RuleEngineOwnable2Step._msgSender - 68 + RuleEngineOwnable2Step._msgSender + 70 - RuleEngineOwnable2Step._onlyComplianceManager + RuleEngineOwnable2Step._onlyComplianceManager 25 - RuleEngineOwnable2Step._onlyRulesLimitManager + RuleEngineOwnable2Step._onlyRulesLimitManager 2 - RuleEngineOwnable2Step._onlyRulesManager - 5 + RuleEngineOwnable2Step._onlyRulesManager + 7 - RuleEngineOwnable2Step.supportsInterface + RuleEngineOwnable2Step.supportsInterface 13 - RuleEngineOwnable2Step.transferOwnership + RuleEngineOwnable2Step.transferOwnership 6 diff --git a/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.gcov.html index cef61d5..495a15c 100644 --- a/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.gcov.html +++ b/doc/coverage/coverage/src/deployment/RuleEngineOwnable2Step.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 8 @@ -95,54 +95,73 @@ 24 : : Ownable(owner_) 25 : : {} 26 : : - 27 : : /* ============ ACCESS CONTROL ============ */ - 28 : : /** - 29 : : * @dev Access control check using Ownable pattern - 30 : : */ - 31 : 5 : function _onlyRulesManager() internal virtual override onlyOwner {} - 32 : 2 : function _onlyRulesLimitManager() internal virtual override onlyOwner {} - 33 : : - 34 : : /** - 35 : : * @dev Access control check using Ownable pattern - 36 : : */ - 37 : 25 : function _onlyComplianceManager() internal virtual override onlyOwner {} - 38 : : - 39 : : /** - 40 : : * @notice Starts ownership transfer to `newOwner`. - 41 : : * @dev Reverts when `newOwner` is already configured as a rule. + 27 : : /** + 28 : : * @notice Starts ownership transfer to `newOwner`. + 29 : : * @dev Reverts when `newOwner` is already configured as a rule. + 30 : : * @param newOwner The address of the new owner. + 31 : : */ + 32 : 6 : function transferOwnership(address newOwner) public virtual override onlyOwner { + 33 : 6 : RuleEngineOwnableShared._checkOwnershipTransferTarget(newOwner); + 34 : 5 : Ownable2Step.transferOwnership(newOwner); + 35 : : } + 36 : : + 37 : : /* ============ ERC-165 ============ */ + 38 : : /** + 39 : : * @notice ERC-165 interface detection. + 40 : : * @param interfaceId The interface identifier to check. + 41 : : * @return True if the interface is supported, false otherwise. 42 : : */ - 43 : 6 : function transferOwnership(address newOwner) public virtual override onlyOwner { - 44 : 6 : RuleEngineOwnableShared._checkOwnershipTransferTarget(newOwner); - 45 : 5 : Ownable2Step.transferOwnership(newOwner); - 46 : : } - 47 : : - 48 : : /* ============ ERC-165 ============ */ - 49 : 13 : function supportsInterface(bytes4 interfaceId) public view virtual override(RuleEngineOwnableShared) returns (bool) { + 43 : 13 : function supportsInterface(bytes4 interfaceId) + 44 : : public + 45 : : view + 46 : : virtual + 47 : : override(RuleEngineOwnableShared) + 48 : : returns (bool) + 49 : : { 50 : 13 : return interfaceId == Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID 51 : 11 : || RuleEngineOwnableShared.supportsInterface(interfaceId); 52 : : } 53 : : - 54 : : /** - 55 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 56 : : */ - 57 : 68 : function _msgSender() internal view virtual override(RuleEngineOwnableShared, Context) returns (address sender) { - 58 : 68 : return RuleEngineOwnableShared._msgSender(); - 59 : : } - 60 : : - 61 : : /** - 62 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 63 : : */ - 64 : 1 : function _msgData() internal view virtual override(RuleEngineOwnableShared, Context) returns (bytes calldata) { - 65 : 1 : return RuleEngineOwnableShared._msgData(); - 66 : : } - 67 : : - 68 : : /** - 69 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 70 : : */ - 71 : 69 : function _contextSuffixLength() internal view virtual override(RuleEngineOwnableShared, Context) returns (uint256) { - 72 : 69 : return RuleEngineOwnableShared._contextSuffixLength(); - 73 : : } - 74 : : } + 54 : : /* ============ ACCESS CONTROL ============ */ + 55 : : /** + 56 : : * @dev Access control check using Ownable pattern + 57 : : */ + 58 : 7 : function _onlyRulesManager() internal virtual override onlyOwner {} + 59 : : + 60 : : /** + 61 : : * @dev Access control check using Ownable pattern + 62 : : */ + 63 : 2 : function _onlyRulesLimitManager() internal virtual override onlyOwner {} + 64 : : + 65 : : /** + 66 : : * @dev Access control check using Ownable pattern + 67 : : */ + 68 : 25 : function _onlyComplianceManager() internal virtual override onlyOwner {} + 69 : : + 70 : : /** + 71 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 72 : : * @return sender The transaction sender, unwrapped from the forwarder calldata when relayed. + 73 : : */ + 74 : 70 : function _msgSender() internal view virtual override(RuleEngineOwnableShared, Context) returns (address sender) { + 75 : 70 : return RuleEngineOwnableShared._msgSender(); + 76 : : } + 77 : : + 78 : : /** + 79 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 80 : : * @return The transaction calldata, with the appended sender stripped when relayed. + 81 : : */ + 82 : 1 : function _msgData() internal view virtual override(RuleEngineOwnableShared, Context) returns (bytes calldata) { + 83 : 1 : return RuleEngineOwnableShared._msgData(); + 84 : : } + 85 : : + 86 : : /** + 87 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 88 : : * @return The length of the ERC-2771 calldata suffix. + 89 : : */ + 90 : 71 : function _contextSuffixLength() internal view virtual override(RuleEngineOwnableShared, Context) returns (uint256) { + 91 : 71 : return RuleEngineOwnableShared._contextSuffixLength(); + 92 : : } + 93 : : } diff --git a/doc/coverage/coverage/src/deployment/index-sort-b.html b/doc/coverage/coverage/src/deployment/index-sort-b.html index 9786af8..f137e1a 100644 --- a/doc/coverage/coverage/src/deployment/index-sort-b.html +++ b/doc/coverage/coverage/src/deployment/index-sort-b.html @@ -31,13 +31,13 @@ lcov.info Lines: - 52 - 52 + 53 + 53 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 25 @@ -82,26 +82,26 @@ Branches Sort by branch coverage - RuleEngineOwnable.sol + RuleEngineOwnable2Step.sol
100.0%
100.0 % - 12 / 12 + 15 / 15 100.0 % - 7 / 7 + 8 / 8 - 0 / 0 - RuleEngineOwnable2Step.sol + RuleEngineOwnable.sol
100.0%
100.0 % - 15 / 15 + 12 / 12 100.0 % - 8 / 8 + 7 / 7 - 0 / 0 @@ -111,7 +111,7 @@
100.0%
100.0 % - 25 / 25 + 26 / 26 100.0 % 10 / 10 100.0 % diff --git a/doc/coverage/coverage/src/deployment/index-sort-f.html b/doc/coverage/coverage/src/deployment/index-sort-f.html index 7a8b7c2..da50b8a 100644 --- a/doc/coverage/coverage/src/deployment/index-sort-f.html +++ b/doc/coverage/coverage/src/deployment/index-sort-f.html @@ -31,13 +31,13 @@ lcov.info Lines: - 52 - 52 + 53 + 53 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 25 @@ -111,7 +111,7 @@
100.0%
100.0 % - 25 / 25 + 26 / 26 100.0 % 10 / 10 100.0 % diff --git a/doc/coverage/coverage/src/deployment/index-sort-l.html b/doc/coverage/coverage/src/deployment/index-sort-l.html index 4136abe..1742e21 100644 --- a/doc/coverage/coverage/src/deployment/index-sort-l.html +++ b/doc/coverage/coverage/src/deployment/index-sort-l.html @@ -31,13 +31,13 @@ lcov.info Lines: - 52 - 52 + 53 + 53 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 25 @@ -111,7 +111,7 @@
100.0%
100.0 % - 25 / 25 + 26 / 26 100.0 % 10 / 10 100.0 % diff --git a/doc/coverage/coverage/src/deployment/index.html b/doc/coverage/coverage/src/deployment/index.html index d85f72a..3d9fc50 100644 --- a/doc/coverage/coverage/src/deployment/index.html +++ b/doc/coverage/coverage/src/deployment/index.html @@ -31,13 +31,13 @@ lcov.info Lines: - 52 - 52 + 53 + 53 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 25 @@ -87,7 +87,7 @@
100.0%
100.0 % - 25 / 25 + 26 / 26 100.0 % 10 / 10 100.0 % diff --git a/doc/coverage/coverage/src/index-sort-b.html b/doc/coverage/coverage/src/index-sort-b.html index ab024be..cec55fe 100644 --- a/doc/coverage/coverage/src/index-sort-b.html +++ b/doc/coverage/coverage/src/index-sort-b.html @@ -31,13 +31,13 @@ lcov.info Lines: - 64 - 64 + 67 + 67 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 20 @@ -49,8 +49,8 @@ Branches: - 6 - 6 + 7 + 7 100.0 % @@ -87,7 +87,7 @@
100.0%
100.0 % - 15 / 15 + 16 / 16 100.0 % 6 / 6 100.0 % @@ -99,11 +99,11 @@
100.0%
100.0 % - 49 / 49 + 51 / 51 100.0 % 14 / 14 100.0 % - 4 / 4 + 5 / 5 diff --git a/doc/coverage/coverage/src/index-sort-f.html b/doc/coverage/coverage/src/index-sort-f.html index 4c25063..c40f388 100644 --- a/doc/coverage/coverage/src/index-sort-f.html +++ b/doc/coverage/coverage/src/index-sort-f.html @@ -31,13 +31,13 @@ lcov.info Lines: - 64 - 64 + 67 + 67 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 20 @@ -49,8 +49,8 @@ Branches: - 6 - 6 + 7 + 7 100.0 % @@ -87,7 +87,7 @@
100.0%
100.0 % - 15 / 15 + 16 / 16 100.0 % 6 / 6 100.0 % @@ -99,11 +99,11 @@
100.0%
100.0 % - 49 / 49 + 51 / 51 100.0 % 14 / 14 100.0 % - 4 / 4 + 5 / 5 diff --git a/doc/coverage/coverage/src/index-sort-l.html b/doc/coverage/coverage/src/index-sort-l.html index 8bf2ace..4318656 100644 --- a/doc/coverage/coverage/src/index-sort-l.html +++ b/doc/coverage/coverage/src/index-sort-l.html @@ -31,13 +31,13 @@ lcov.info Lines: - 64 - 64 + 67 + 67 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 20 @@ -49,8 +49,8 @@ Branches: - 6 - 6 + 7 + 7 100.0 % @@ -87,7 +87,7 @@
100.0%
100.0 % - 15 / 15 + 16 / 16 100.0 % 6 / 6 100.0 % @@ -99,11 +99,11 @@
100.0%
100.0 % - 49 / 49 + 51 / 51 100.0 % 14 / 14 100.0 % - 4 / 4 + 5 / 5 diff --git a/doc/coverage/coverage/src/index.html b/doc/coverage/coverage/src/index.html index 3a35084..a4aebf5 100644 --- a/doc/coverage/coverage/src/index.html +++ b/doc/coverage/coverage/src/index.html @@ -31,13 +31,13 @@ lcov.info Lines: - 64 - 64 + 67 + 67 100.0 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 20 @@ -49,8 +49,8 @@ Branches: - 6 - 6 + 7 + 7 100.0 % @@ -87,11 +87,11 @@
100.0%
100.0 % - 49 / 49 + 51 / 51 100.0 % 14 / 14 100.0 % - 4 / 4 + 5 / 5 RuleEngineOwnableShared.sol @@ -99,7 +99,7 @@
100.0%
100.0 % - 15 / 15 + 16 / 16 100.0 % 6 / 6 100.0 % diff --git a/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.func-sort-c.html b/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.func-sort-c.html index d6030b2..e1fde91 100644 --- a/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 7 @@ -69,32 +69,32 @@ Hit count Sort by hit count - ERC3643ComplianceExtendedModule.getTokenBounds + ERC3643ComplianceExtendedModule.getTokenBounds 4 - ERC3643ComplianceExtendedModule.isTokenSelfBindingApproved + ERC3643ComplianceExtendedModule.isTokenSelfBindingApproved 6 - ERC3643ComplianceExtendedModule.unbindTokens + ERC3643ComplianceExtendedModule.unbindTokens 9 - ERC3643ComplianceExtendedModule.setTokenSelfBindingApprovalBatch + ERC3643ComplianceExtendedModule.setTokenSelfBindingApprovalBatch 12 - ERC3643ComplianceExtendedModule.setTokenSelfBindingApproval - 14 + ERC3643ComplianceExtendedModule.bindTokens + 18 - ERC3643ComplianceExtendedModule.bindTokens - 18 + ERC3643ComplianceExtendedModule.setTokenSelfBindingApproval + 27 - ERC3643ComplianceExtendedModule._authorizeComplianceBindingChange - 72 + ERC3643ComplianceExtendedModule._authorizeComplianceBindingChange + 87
diff --git a/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.func.html b/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.func.html index a525710..9dab461 100644 --- a/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.func.html +++ b/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 7 @@ -69,31 +69,31 @@ Hit count Sort by hit count - ERC3643ComplianceExtendedModule._authorizeComplianceBindingChange - 72 + ERC3643ComplianceExtendedModule._authorizeComplianceBindingChange + 87 - ERC3643ComplianceExtendedModule.bindTokens + ERC3643ComplianceExtendedModule.bindTokens 18 - ERC3643ComplianceExtendedModule.getTokenBounds + ERC3643ComplianceExtendedModule.getTokenBounds 4 - ERC3643ComplianceExtendedModule.isTokenSelfBindingApproved + ERC3643ComplianceExtendedModule.isTokenSelfBindingApproved 6 - ERC3643ComplianceExtendedModule.setTokenSelfBindingApproval - 14 + ERC3643ComplianceExtendedModule.setTokenSelfBindingApproval + 27 - ERC3643ComplianceExtendedModule.setTokenSelfBindingApprovalBatch + ERC3643ComplianceExtendedModule.setTokenSelfBindingApprovalBatch 12 - ERC3643ComplianceExtendedModule.unbindTokens + ERC3643ComplianceExtendedModule.unbindTokens 9 diff --git a/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.gcov.html b/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.gcov.html index bfdd448..6b680ec 100644 --- a/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.gcov.html +++ b/doc/coverage/coverage/src/modules/ERC3643ComplianceExtendedModule.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 7 @@ -78,73 +78,81 @@ 7 : : import {IERC3643ComplianceExtended} from "../interfaces/IERC3643ComplianceExtended.sol"; 8 : : import {ERC3643ComplianceModule} from "./ERC3643ComplianceModule.sol"; 9 : : - 10 : : abstract contract ERC3643ComplianceExtendedModule is ERC3643ComplianceModule, IERC3643ComplianceExtended { - 11 : : using EnumerableSet for EnumerableSet.AddressSet; - 12 : : - 13 : : mapping(address token => bool approved) private _tokenSelfBindingApproval; - 14 : : - 15 : : /** - 16 : : * @inheritdoc IERC3643ComplianceExtended - 17 : : * @custom:security-note See {bindToken} for multi-tenant accounting risks. All tokens bound - 18 : : * in this batch share the same rule state. Only bind tokens that are equally trusted and - 19 : : * governed together. - 20 : : */ - 21 : 18 : function bindTokens(address[] calldata tokens) public virtual override onlyComplianceManager { - 22 : 15 : for (uint256 i = 0; i < tokens.length; ++i) { - 23 : 24 : _bindToken(tokens[i]); - 24 : : } - 25 : : } - 26 : : - 27 : : /// @inheritdoc IERC3643ComplianceExtended - 28 : 9 : function unbindTokens(address[] calldata tokens) public virtual override onlyComplianceManager { - 29 : 6 : for (uint256 i = 0; i < tokens.length; ++i) { - 30 : 12 : _unbindToken(tokens[i]); + 10 : : /** + 11 : : * @title ERC3643ComplianceExtendedModule + 12 : : * @notice Extends the core ERC-3643 compliance module with batch binding and token self-binding. + 13 : : */ + 14 : : abstract contract ERC3643ComplianceExtendedModule is ERC3643ComplianceModule, IERC3643ComplianceExtended { + 15 : : using EnumerableSet for EnumerableSet.AddressSet; + 16 : : + 17 : : /** + 18 : : * @notice Tracks which tokens are allowed to bind and unbind themselves. + 19 : : */ + 20 : : mapping(address token => bool approved) private _tokenSelfBindingApproval; + 21 : : + 22 : : /** + 23 : : * @inheritdoc IERC3643ComplianceExtended + 24 : : * @custom:security-note See {bindToken} for multi-tenant accounting risks. All tokens bound + 25 : : * in this batch share the same rule state. Only bind tokens that are equally trusted and + 26 : : * governed together. + 27 : : */ + 28 : 18 : function bindTokens(address[] calldata tokens) public virtual override onlyComplianceManager { + 29 : 15 : for (uint256 i = 0; i < tokens.length; ++i) { + 30 : 24 : _bindToken(tokens[i]); 31 : : } 32 : : } 33 : : 34 : : /// @inheritdoc IERC3643ComplianceExtended - 35 : 14 : function setTokenSelfBindingApproval(address token, bool approved) public virtual override onlyComplianceManager { - 36 [ + + ]: 11 : require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); - 37 : 8 : _tokenSelfBindingApproval[token] = approved; - 38 : 8 : emit TokenSelfBindingApprovalSet(token, approved); + 35 : 9 : function unbindTokens(address[] calldata tokens) public virtual override onlyComplianceManager { + 36 : 6 : for (uint256 i = 0; i < tokens.length; ++i) { + 37 : 12 : _unbindToken(tokens[i]); + 38 : : } 39 : : } 40 : : 41 : : /// @inheritdoc IERC3643ComplianceExtended - 42 : 12 : function setTokenSelfBindingApprovalBatch(address[] calldata tokens, bool approved) - 43 : : public - 44 : : virtual - 45 : : override - 46 : : onlyComplianceManager - 47 : : { - 48 : 9 : for (uint256 i = 0; i < tokens.length; ++i) { - 49 : 18 : address token = tokens[i]; - 50 [ + + ]: 18 : require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); - 51 : 15 : _tokenSelfBindingApproval[token] = approved; - 52 : : } - 53 : 6 : emit TokenSelfBindingApprovalBatchSet(tokens, approved); - 54 : : } - 55 : : - 56 : : /// @inheritdoc IERC3643ComplianceExtended - 57 : 6 : function isTokenSelfBindingApproved(address token) public view virtual override returns (bool) { - 58 : 6 : return _tokenSelfBindingApproval[token]; - 59 : : } - 60 : : - 61 : : /// @inheritdoc IERC3643ComplianceExtended - 62 : 4 : function getTokenBounds() public view virtual override returns (address[] memory) { - 63 : 4 : return _boundTokens.values(); - 64 : : } - 65 : : - 66 : : /** - 67 : : * @dev Authorizes bind/unbind operations. - 68 : : * Allows compliance manager, or approved token self-calls for T-REX compatibility. - 69 : : */ - 70 : 72 : function _authorizeComplianceBindingChange(address token) internal virtual override { - 71 [ + ]: 72 : if (_msgSender() == token && _tokenSelfBindingApproval[token]) { - 72 : 72 : return; - 73 : : } - 74 : 60 : _onlyComplianceManager(); - 75 : : } - 76 : : } + 42 : 27 : function setTokenSelfBindingApproval(address token, bool approved) public virtual override onlyComplianceManager { + 43 [ + + ]: 24 : require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); + 44 : 21 : _tokenSelfBindingApproval[token] = approved; + 45 : 21 : emit TokenSelfBindingApprovalSet(token, approved); + 46 : : } + 47 : : + 48 : : /// @inheritdoc IERC3643ComplianceExtended + 49 : 12 : function setTokenSelfBindingApprovalBatch(address[] calldata tokens, bool approved) + 50 : : public + 51 : : virtual + 52 : : override + 53 : : onlyComplianceManager + 54 : : { + 55 : 9 : for (uint256 i = 0; i < tokens.length; ++i) { + 56 : 18 : address token = tokens[i]; + 57 [ + + ]: 18 : require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); + 58 : 15 : _tokenSelfBindingApproval[token] = approved; + 59 : : } + 60 : 6 : emit TokenSelfBindingApprovalBatchSet(tokens, approved); + 61 : : } + 62 : : + 63 : : /// @inheritdoc IERC3643ComplianceExtended + 64 : 6 : function isTokenSelfBindingApproved(address token) public view virtual override returns (bool) { + 65 : 6 : return _tokenSelfBindingApproval[token]; + 66 : : } + 67 : : + 68 : : /// @inheritdoc IERC3643ComplianceExtended + 69 : 4 : function getTokenBounds() public view virtual override returns (address[] memory) { + 70 : 4 : return _boundTokens.values(); + 71 : : } + 72 : : + 73 : : /** + 74 : : * @dev Authorizes bind/unbind operations. + 75 : : * Allows compliance manager, or approved token self-calls for T-REX compatibility. + 76 : : * @param token The token being bound or unbound. + 77 : : */ + 78 : 87 : function _authorizeComplianceBindingChange(address token) internal virtual override { + 79 [ + ]: 87 : if (_msgSender() == token && _tokenSelfBindingApproval[token]) { + 80 : 87 : return; + 81 : : } + 82 : 61 : _onlyComplianceManager(); + 83 : : } + 84 : : } 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 f077265..a47f253 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,13 +31,13 @@ lcov.info Lines: + 26 28 - 30 - 93.3 % + 92.9 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 9 @@ -49,9 +49,9 @@ Branches: - 11 - 13 - 84.6 % + 9 + 9 + 100.0 % @@ -69,48 +69,48 @@ Hit count Sort by hit count - ERC3643ComplianceModule._authorizeComplianceBindingChange + ERC3643ComplianceModule._authorizeComplianceBindingChange 0 - ERC3643ComplianceModule._onlyComplianceManager + ERC3643ComplianceModule._onlyComplianceManager 0 - ERC3643ComplianceModule.getTokenBound - 5 + ERC3643ComplianceModule.getTokenBound + 7 - ERC3643ComplianceModule.onlyBoundToken - 6 + ERC3643ComplianceModule.onlyBoundToken + 7 - ERC3643ComplianceModule.onlyComplianceManager + ERC3643ComplianceModule.onlyComplianceManager 9 - ERC3643ComplianceModule.unbindToken - 20 + ERC3643ComplianceModule.unbindToken + 21 - ERC3643ComplianceModule._unbindToken - 25 + ERC3643ComplianceModule._unbindToken + 26 - ERC3643ComplianceModule._checkBoundToken - 31 + ERC3643ComplianceModule._checkBoundToken + 41 - ERC3643ComplianceModule.isTokenBound - 37 + ERC3643ComplianceModule.isTokenBound + 41 - ERC3643ComplianceModule.bindToken - 52 + ERC3643ComplianceModule.bindToken + 66 - ERC3643ComplianceModule._bindToken - 101 + ERC3643ComplianceModule._bindToken + 115
diff --git a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html index 98101c1..dd3cb6a 100644 --- a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html +++ b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.func.html @@ -31,13 +31,13 @@ lcov.info Lines: + 26 28 - 30 - 93.3 % + 92.9 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 9 @@ -49,9 +49,9 @@ Branches: - 11 - 13 - 84.6 % + 9 + 9 + 100.0 % @@ -69,48 +69,48 @@ Hit count Sort by hit count - ERC3643ComplianceModule._authorizeComplianceBindingChange + ERC3643ComplianceModule._authorizeComplianceBindingChange 0 - ERC3643ComplianceModule._bindToken - 101 + ERC3643ComplianceModule._bindToken + 115 - ERC3643ComplianceModule._checkBoundToken - 31 + ERC3643ComplianceModule._checkBoundToken + 41 - ERC3643ComplianceModule._onlyComplianceManager + ERC3643ComplianceModule._onlyComplianceManager 0 - ERC3643ComplianceModule._unbindToken - 25 + ERC3643ComplianceModule._unbindToken + 26 - ERC3643ComplianceModule.bindToken - 52 + ERC3643ComplianceModule.bindToken + 66 - ERC3643ComplianceModule.getTokenBound - 5 + ERC3643ComplianceModule.getTokenBound + 7 - ERC3643ComplianceModule.isTokenBound - 37 + ERC3643ComplianceModule.isTokenBound + 41 - ERC3643ComplianceModule.onlyBoundToken - 6 + ERC3643ComplianceModule.onlyBoundToken + 7 - ERC3643ComplianceModule.onlyComplianceManager + ERC3643ComplianceModule.onlyComplianceManager 9 - ERC3643ComplianceModule.unbindToken - 20 + ERC3643ComplianceModule.unbindToken + 21
diff --git a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html index d20c5d4..e60e9f2 100644 --- a/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html +++ b/doc/coverage/coverage/src/modules/ERC3643ComplianceModule.sol.gcov.html @@ -31,13 +31,13 @@ lcov.info Lines: + 26 28 - 30 - 93.3 % + 92.9 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 9 @@ -49,9 +49,9 @@ Branches: - 11 - 13 - 84.6 % + 9 + 9 + 100.0 % @@ -79,107 +79,127 @@ 8 : : /* ==== Interface and other library === */ 9 : : import {IERC3643Compliance} from "../interfaces/IERC3643Compliance.sol"; 10 : : import {ERC3643ComplianceModuleInvariantStorage} from "./library/ERC3643ComplianceModuleInvariantStorage.sol"; - 11 : : import {ERC3643ComplianceRolesStorage} from "./library/ERC3643ComplianceRolesStorage.sol"; - 12 : : - 13 : : abstract contract ERC3643ComplianceModule is - 14 : : Context, - 15 : : IERC3643Compliance, - 16 : : ERC3643ComplianceModuleInvariantStorage, - 17 : : ERC3643ComplianceRolesStorage - 18 : : { - 19 : : /* ==== Type declaration === */ - 20 : : using EnumerableSet for EnumerableSet.AddressSet; - 21 : : /* ==== State Variables === */ - 22 : : // Token binding tracking - 23 : : EnumerableSet.AddressSet internal _boundTokens; - 24 : : - 25 : : /* ==== Modifier === */ - 26 : 6 : modifier onlyBoundToken() { - 27 : 6 : _checkBoundToken(); - 28 : : _; - 29 : : } - 30 : : - 31 : 9 : modifier onlyComplianceManager() { - 32 : 9 : _onlyComplianceManager(); - 33 : : _; - 34 : : } - 35 : : - 36 : : /*////////////////////////////////////////////////////////////// - 37 : : PUBLIC/public FUNCTIONS - 38 : : //////////////////////////////////////////////////////////////*/ - 39 : : - 40 : : /* ============ State functions ============ */ - 41 : : /** - 42 : : * @inheritdoc IERC3643Compliance - 43 : : * @dev Operator warning: "multi-tenant" means one RuleEngine is shared by - 44 : : * multiple token contracts. In that setup, bind only tokens that are equally - 45 : : * trusted and governed together. - 46 : : * @custom:security-note Operation rules (stateful rules such as `RuleConditionalTransferLight` - 47 : : * or `RuleMintAllowance`) maintain per-address accounting that is shared across all bound tokens. - 48 : : * Binding tokens from different issuers to the same engine will silently cross-contaminate - 49 : : * their accounting. Only bind tokens that are equally trusted and governed together. - 50 : : */ - 51 : 52 : function bindToken(address token) public virtual override { - 52 : 52 : _authorizeComplianceBindingChange(token); - 53 : 45 : _bindToken(token); - 54 : : } - 55 : : - 56 : : /** - 57 : : * @inheritdoc IERC3643Compliance - 58 : : * @dev Operator warning: unbinding is an administrative operation and does not - 59 : : * erase any state already stored by external rule contracts in a previously - 60 : : * shared ("multi-tenant") setup. - 61 : : */ - 62 : 20 : function unbindToken(address token) public virtual override { - 63 : 20 : _authorizeComplianceBindingChange(token); - 64 : 13 : _unbindToken(token); - 65 : : } - 66 : : - 67 : : /// @inheritdoc IERC3643Compliance - 68 : 37 : function isTokenBound(address token) public view virtual override returns (bool) { - 69 : 37 : return _boundTokens.contains(token); - 70 : : } - 71 : : - 72 : : /// @inheritdoc IERC3643Compliance - 73 : 5 : function getTokenBound() public view virtual override returns (address) { - 74 [ + + ]: 5 : if (_boundTokens.length() > 0) { - 75 : : // Note that there are no guarantees on the ordering of values inside the array, - 76 : : // and it may change when more values are added or removed. - 77 : 3 : return _boundTokens.at(0); - 78 : : } else { - 79 : 2 : return address(0); - 80 : : } - 81 : : } - 82 : : - 83 : : /*////////////////////////////////////////////////////////////// - 84 : : INTERNAL/PRIVATE FUNCTIONS - 85 : : //////////////////////////////////////////////////////////////*/ - 86 : : - 87 : 25 : function _unbindToken(address token) internal { - 88 [ + + ]: 25 : require(_boundTokens.contains(token), RuleEngine_ERC3643Compliance_TokenNotBound()); - 89 : : // Should never revert because we check if the token address is already set before - 90 [ # + ]: 20 : require(_boundTokens.remove(token), RuleEngine_ERC3643Compliance_OperationNotSuccessful()); - 91 : : - 92 : 20 : emit TokenUnbound(token); - 93 : : } - 94 : : - 95 : 101 : function _bindToken(address token) internal { - 96 [ + + ]: 101 : require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); - 97 [ + + ]: 96 : require(!_boundTokens.contains(token), RuleEngine_ERC3643Compliance_TokenAlreadyBound()); - 98 : : // Should never revert because we check if the token address is already set before - 99 [ # + ]: 91 : require(_boundTokens.add(token), RuleEngine_ERC3643Compliance_OperationNotSuccessful()); - 100 : 91 : emit TokenBound(token); - 101 : : } - 102 : : - 103 : 31 : function _checkBoundToken() internal view virtual { - 104 [ + ]: 31 : if (!_boundTokens.contains(_msgSender())) { - 105 : 7 : revert RuleEngine_ERC3643Compliance_UnauthorizedCaller(); - 106 : : } - 107 : : } - 108 : : - 109 : 0 : function _authorizeComplianceBindingChange(address token) internal virtual; - 110 : 0 : function _onlyComplianceManager() internal virtual; - 111 : : } + 11 : : + 12 : : /** + 13 : : * @title ERC3643ComplianceModule + 14 : : * @notice Core ERC-3643 compliance module: tracks the tokens bound to this engine. + 15 : : */ + 16 : : abstract contract ERC3643ComplianceModule is Context, IERC3643Compliance, ERC3643ComplianceModuleInvariantStorage { + 17 : : /* ==== Type declaration === */ + 18 : : using EnumerableSet for EnumerableSet.AddressSet; + 19 : : /* ==== State Variables === */ + 20 : : // Token binding tracking + 21 : : /** + 22 : : * @notice Set of tokens allowed to call the compliance callbacks. + 23 : : */ + 24 : : EnumerableSet.AddressSet internal _boundTokens; + 25 : : + 26 : : /* ==== Modifier === */ + 27 : 7 : modifier onlyBoundToken() { + 28 : 7 : _checkBoundToken(); + 29 : : _; + 30 : : } + 31 : : + 32 : 9 : modifier onlyComplianceManager() { + 33 : 9 : _onlyComplianceManager(); + 34 : : _; + 35 : : } + 36 : : + 37 : : /*////////////////////////////////////////////////////////////// + 38 : : PUBLIC/public FUNCTIONS + 39 : : //////////////////////////////////////////////////////////////*/ + 40 : : + 41 : : /* ============ State functions ============ */ + 42 : : /** + 43 : : * @inheritdoc IERC3643Compliance + 44 : : * @dev Operator warning: "multi-tenant" means one RuleEngine is shared by + 45 : : * multiple token contracts. In that setup, bind only tokens that are equally + 46 : : * trusted and governed together. + 47 : : * @custom:security-note Operation rules (stateful rules such as `RuleConditionalTransferLightMock` + 48 : : * or `RuleMintAllowanceMock`) maintain per-address accounting that is shared across all bound tokens. + 49 : : * Binding tokens from different issuers to the same engine will silently cross-contaminate + 50 : : * their accounting. Only bind tokens that are equally trusted and governed together. + 51 : : */ + 52 : 66 : function bindToken(address token) public virtual override { + 53 : 66 : _authorizeComplianceBindingChange(token); + 54 : 58 : _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 : : */ + 63 : 21 : function unbindToken(address token) public virtual override { + 64 : 21 : _authorizeComplianceBindingChange(token); + 65 : 14 : _unbindToken(token); + 66 : : } + 67 : : + 68 : : /// @inheritdoc IERC3643Compliance + 69 : 41 : function isTokenBound(address token) public view virtual override returns (bool) { + 70 : 41 : return _boundTokens.contains(token); + 71 : : } + 72 : : + 73 : : /// @inheritdoc IERC3643Compliance + 74 : 7 : function getTokenBound() public view virtual override returns (address) { + 75 [ + + ]: 7 : if (_boundTokens.length() > 0) { + 76 : : // Note that there are no guarantees on the ordering of values inside the array, + 77 : : // and it may change when more values are added or removed. + 78 : 5 : return _boundTokens.pos(0); + 79 : : } else { + 80 : 2 : return address(0); + 81 : : } + 82 : : } + 83 : : + 84 : : /*////////////////////////////////////////////////////////////// + 85 : : INTERNAL/PRIVATE FUNCTIONS + 86 : : //////////////////////////////////////////////////////////////*/ + 87 : : + 88 : : /** + 89 : : * @dev Removes a token from the bound set. + 90 : : * @param token The token to unbind; reverts when it is not currently bound. + 91 : : */ + 92 : 26 : function _unbindToken(address token) internal virtual { + 93 : : // remove() returns false when the token was not bound, so a separate + 94 : : // contains() lookup is unnecessary. + 95 [ + + ]: 26 : require(_boundTokens.remove(token), RuleEngine_ERC3643Compliance_TokenNotBound()); + 96 : : + 97 : 21 : emit TokenUnbound(token); + 98 : : } + 99 : : + 100 : : /** + 101 : : * @dev Adds a token to the bound set. + 102 : : * @param token The token to bind; reverts on the zero address or when already bound. + 103 : : */ + 104 : 115 : function _bindToken(address token) internal virtual { + 105 [ + + ]: 115 : require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); + 106 : : // add() returns false when the token is already bound, so a separate + 107 : : // contains() lookup is unnecessary. + 108 [ + + ]: 110 : require(_boundTokens.add(token), RuleEngine_ERC3643Compliance_TokenAlreadyBound()); + 109 : 105 : emit TokenBound(token); + 110 : : } + 111 : : + 112 : : /** + 113 : : * @dev Authorization hook for bind/unbind, implemented by the deployable contracts. + 114 : : * @param token The token being bound or unbound. + 115 : : */ + 116 : 0 : function _authorizeComplianceBindingChange(address token) internal virtual; + 117 : : + 118 : : /** + 119 : : * @dev Access control hook guarding compliance management operations. + 120 : : */ + 121 : 0 : function _onlyComplianceManager() internal virtual; + 122 : : + 123 : : /** + 124 : : * @dev Reverts when the caller is not a bound token. + 125 : : */ + 126 : 41 : function _checkBoundToken() internal view virtual { + 127 [ + ]: 41 : if (!_boundTokens.contains(_msgSender())) { + 128 : 10 : revert RuleEngine_ERC3643Compliance_UnauthorizedCaller(); + 129 : : } + 130 : : } + 131 : : } 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 00e1452..1c5153f 100644 --- a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 17 @@ -69,23 +69,23 @@ Hit count Sort by hit count - RulesManagementModule._onlyRulesLimitManager + RulesManagementModule._onlyRulesLimitManager 0 - RulesManagementModule._onlyRulesManager + RulesManagementModule._onlyRulesManager 0 - RulesManagementModule._transferred.1 + RulesManagementModule.rule 5 - RulesManagementModule.rule - 5 + RulesManagementModule._transferred.1 + 6 - RulesManagementModule.maxRules + RulesManagementModule.maxRules 8 @@ -93,56 +93,56 @@ 9 - RulesManagementModule.setMaxRules + RulesManagementModule.setMaxRules 9 - RulesManagementModule._removeRule + RulesManagementModule._removeRule 13 - RulesManagementModule.rules + RulesManagementModule.rules 15 + + RulesManagementModule.removeRule + 18 + RulesManagementModule.clearRules - 16 + 19 RulesManagementModule.onlyRulesManager - 16 + 19 - RulesManagementModule.removeRule - 18 + RulesManagementModule._transferred.0 + 25 - RulesManagementModule._transferred.0 - 19 + RulesManagementModule.setRules + 53 - RulesManagementModule._clearRules - 49 + RulesManagementModule._clearRules + 54 - RulesManagementModule.setRules - 51 + RulesManagementModule.containsRule + 82 - RulesManagementModule.containsRule - 79 + RulesManagementModule.rulesCount + 191 RulesManagementModule.addRule - 181 - - - RulesManagementModule.rulesCount - 183 + 203 - RulesManagementModule._checkRule - 253 + RulesManagementModule._checkRule + 277
diff --git a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html index bd131a2..6ffd6ea 100644 --- a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html +++ b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 17 @@ -69,47 +69,47 @@ Hit count Sort by hit count - RulesManagementModule._checkRule - 253 + RulesManagementModule._checkRule + 277 - RulesManagementModule._clearRules - 49 + RulesManagementModule._clearRules + 54 - RulesManagementModule._onlyRulesLimitManager + RulesManagementModule._onlyRulesLimitManager 0 - RulesManagementModule._onlyRulesManager + RulesManagementModule._onlyRulesManager 0 - RulesManagementModule._removeRule + RulesManagementModule._removeRule 13 - RulesManagementModule._transferred.0 - 19 + RulesManagementModule._transferred.0 + 25 - RulesManagementModule._transferred.1 - 5 + RulesManagementModule._transferred.1 + 6 RulesManagementModule.addRule - 181 + 203 RulesManagementModule.clearRules - 16 + 19 - RulesManagementModule.containsRule - 79 + RulesManagementModule.containsRule + 82 - RulesManagementModule.maxRules + RulesManagementModule.maxRules 8 @@ -118,31 +118,31 @@ RulesManagementModule.onlyRulesManager - 16 + 19 - RulesManagementModule.removeRule + RulesManagementModule.removeRule 18 - RulesManagementModule.rule + RulesManagementModule.rule 5 - RulesManagementModule.rules + RulesManagementModule.rules 15 - RulesManagementModule.rulesCount - 183 + RulesManagementModule.rulesCount + 191 - RulesManagementModule.setMaxRules + RulesManagementModule.setMaxRules 9 RulesManagementModule.setRules - 51 + 53
diff --git a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html index d17ed7a..61fda73 100644 --- a/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html +++ b/doc/coverage/coverage/src/modules/RulesManagementModule.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 17 @@ -84,8 +84,8 @@ 13 : : * @title RuleEngine - part 14 : : */ 15 : : abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage, IRulesManagementModule { - 16 : 16 : modifier onlyRulesManager() { - 17 : 16 : _onlyRulesManager(); + 16 : 19 : modifier onlyRulesManager() { + 17 : 19 : _onlyRulesManager(); 18 : : _; 19 : : } 20 : : @@ -120,29 +120,29 @@ 49 : : * Security convention: rule contracts should be treated as trusted business logic, 50 : : * but should not also be granted {RULES_MANAGEMENT_ROLE}. 51 : : */ - 52 : 51 : function setRules(IRule[] calldata rules_) public virtual override(IRulesManagementModule) onlyRulesManager { - 53 [ + ]: 49 : if (rules_.length == 0) { + 52 : 53 : function setRules(IRule[] calldata rules_) public virtual override(IRulesManagementModule) onlyRulesManager { + 53 [ + ]: 51 : if (rules_.length == 0) { 54 : 6 : revert RuleEngine_RulesManagementModule_ArrayIsEmpty(); 55 : : } - 56 [ + ]: 43 : if (rules_.length > _maxRules) { + 56 [ + ]: 45 : if (rules_.length > _maxRules) { 57 : 1 : revert RuleEngine_RulesManagementModule_MaxRulesExceeded(_maxRules); 58 : : } - 59 [ + ]: 42 : if (_rules.length() > 0) { - 60 : 36 : _clearRules(); + 59 [ + ]: 44 : if (_rules.length() > 0) { + 60 : 38 : _clearRules(); 61 : : } - 62 : 42 : for (uint256 i = 0; i < rules_.length; ++i) { - 63 : 79 : _checkRule(address(rules_[i])); + 62 : 44 : for (uint256 i = 0; i < rules_.length; ++i) { + 63 : 81 : _checkRule(address(rules_[i])); 64 : : // Should never revert because we check the presence of the rule before - 65 [ # + ]: 74 : require(_rules.add(address(rules_[i])), RuleEngine_RulesManagementModule_OperationNotSuccessful()); - 66 : 74 : emit AddRule(rules_[i]); + 65 [ # + ]: 76 : require(_rules.add(address(rules_[i])), RuleEngine_RulesManagementModule_OperationNotSuccessful()); + 66 : 76 : emit AddRule(rules_[i]); 67 : : } 68 : : } 69 : : 70 : : /** 71 : : * @inheritdoc IRulesManagementModule 72 : : */ - 73 : 16 : function clearRules() public virtual override(IRulesManagementModule) onlyRulesManager { - 74 : 13 : _clearRules(); + 73 : 19 : function clearRules() public virtual override(IRulesManagementModule) onlyRulesManager { + 74 : 16 : _clearRules(); 75 : : } 76 : : 77 : : /** @@ -150,156 +150,163 @@ 79 : : * @dev Reverts when the configured maximum number of rules is already reached. 80 : : * Security convention: do not grant {RULES_MANAGEMENT_ROLE} to rule contracts. 81 : : */ - 82 : 181 : function addRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { - 83 [ + ]: 176 : if (_rules.length() >= _maxRules) { + 82 : 203 : function addRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { + 83 [ + ]: 198 : if (_rules.length() >= _maxRules) { 84 : 2 : revert RuleEngine_RulesManagementModule_MaxRulesExceeded(_maxRules); 85 : : } - 86 : 174 : _checkRule(address(rule_)); - 87 [ # + ]: 164 : require(_rules.add(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); - 88 : 164 : emit AddRule(rule_); + 86 : 196 : _checkRule(address(rule_)); + 87 [ # + ]: 186 : require(_rules.add(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); + 88 : 186 : emit AddRule(rule_); 89 : : } 90 : : 91 : : /** 92 : : * @inheritdoc IRulesManagementModule 93 : : */ - 94 : 8 : function maxRules() public view virtual override(IRulesManagementModule) returns (uint256) { - 95 : 8 : return _maxRules; - 96 : : } - 97 : : - 98 : : /** - 99 : : * @inheritdoc IRulesManagementModule - 100 : : */ - 101 : 9 : function setMaxRules(uint256 maxRules_) public virtual override(IRulesManagementModule) onlyRulesLimitManager { - 102 [ + ]: 5 : if (maxRules_ == 0) { - 103 : 1 : revert RuleEngine_RulesManagementModule_MaxRulesZeroNotAllowed(); - 104 : : } - 105 : 4 : _maxRules = maxRules_; - 106 : 4 : emit SetMaxRules(maxRules_); - 107 : : } - 108 : : - 109 : : /** - 110 : : * @inheritdoc IRulesManagementModule - 111 : : */ - 112 : 18 : function removeRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { - 113 [ + + ]: 16 : require(_rules.contains(address(rule_)), RuleEngine_RulesManagementModule_RuleDoNotMatch()); - 114 : 13 : _removeRule(rule_); - 115 : : } - 116 : : - 117 : : /* ============ View functions ============ */ - 118 : : - 119 : : /** - 120 : : * @inheritdoc IRulesManagementModule - 121 : : */ - 122 : 183 : function rulesCount() public view virtual override(IRulesManagementModule) returns (uint256) { - 123 : 300 : return _rules.length(); - 124 : : } - 125 : : - 126 : : /** - 127 : : * @inheritdoc IRulesManagementModule - 128 : : */ - 129 : 79 : function containsRule(IRule rule_) public view virtual override(IRulesManagementModule) returns (bool) { - 130 : 89 : return _rules.contains(address(rule_)); - 131 : : } - 132 : : - 133 : : /** - 134 : : * @inheritdoc IRulesManagementModule - 135 : : */ - 136 : 5 : function rule(uint256 ruleId) public view virtual override(IRulesManagementModule) returns (address) { - 137 [ + + ]: 133 : if (ruleId < _rules.length()) { - 138 : : // Note that there are no guarantees on the ordering of values inside the array, - 139 : : // and it may change when more values are added or removed. - 140 : 131 : return _rules.at(ruleId); - 141 : : } else { - 142 : 2 : return address(0); - 143 : : } - 144 : : } - 145 : : - 146 : : /** - 147 : : * @inheritdoc IRulesManagementModule - 148 : : */ - 149 : 15 : function rules() public view virtual override(IRulesManagementModule) returns (address[] memory) { - 150 : 15 : return _rules.values(); - 151 : : } - 152 : : - 153 : : /*////////////////////////////////////////////////////////////// - 154 : : INTERNAL/PRIVATE FUNCTIONS - 155 : : //////////////////////////////////////////////////////////////*/ - 156 : : /** - 157 : : * @notice Clear all the rules of the array of rules - 158 : : * - 159 : : */ - 160 : 49 : function _clearRules() internal virtual { - 161 : 49 : emit ClearRules(); - 162 : 49 : _rules.clear(); - 163 : : } - 164 : : - 165 : : /** - 166 : : * @notice Remove a rule from the array of rules - 167 : : * Revert if the rule found at the specified index does not match the rule in argument - 168 : : * @param rule_ address of the target rule + 94 : 9 : function setMaxRules(uint256 maxRules_) public virtual override(IRulesManagementModule) onlyRulesLimitManager { + 95 [ + ]: 5 : if (maxRules_ == 0) { + 96 : 1 : revert RuleEngine_RulesManagementModule_MaxRulesZeroNotAllowed(); + 97 : : } + 98 : 4 : _maxRules = maxRules_; + 99 : 4 : emit SetMaxRules(maxRules_); + 100 : : } + 101 : : + 102 : : /** + 103 : : * @inheritdoc IRulesManagementModule + 104 : : */ + 105 : 18 : function removeRule(IRule rule_) public virtual override(IRulesManagementModule) onlyRulesManager { + 106 [ + + ]: 16 : require(_rules.contains(address(rule_)), RuleEngine_RulesManagementModule_RuleDoNotMatch()); + 107 : 13 : _removeRule(rule_); + 108 : : } + 109 : : + 110 : : /* ============ View functions ============ */ + 111 : : /** + 112 : : * @inheritdoc IRulesManagementModule + 113 : : */ + 114 : 8 : function maxRules() public view virtual override(IRulesManagementModule) returns (uint256) { + 115 : 8 : return _maxRules; + 116 : : } + 117 : : + 118 : : /** + 119 : : * @inheritdoc IRulesManagementModule + 120 : : */ + 121 : 191 : function rulesCount() public view virtual override(IRulesManagementModule) returns (uint256) { + 122 : 323 : return _rules.length(); + 123 : : } + 124 : : + 125 : : /** + 126 : : * @inheritdoc IRulesManagementModule + 127 : : */ + 128 : 82 : function containsRule(IRule rule_) public view virtual override(IRulesManagementModule) returns (bool) { + 129 : 92 : return _rules.contains(address(rule_)); + 130 : : } + 131 : : + 132 : : /** + 133 : : * @inheritdoc IRulesManagementModule + 134 : : */ + 135 : 5 : function rule(uint256 ruleId) public view virtual override(IRulesManagementModule) returns (address) { + 136 [ + + ]: 152 : if (ruleId < _rules.length()) { + 137 : : // Note that there are no guarantees on the ordering of values inside the array, + 138 : : // and it may change when more values are added or removed. + 139 : 150 : return _rules.pos(ruleId); + 140 : : } else { + 141 : 2 : return address(0); + 142 : : } + 143 : : } + 144 : : + 145 : : /** + 146 : : * @inheritdoc IRulesManagementModule + 147 : : */ + 148 : 15 : function rules() public view virtual override(IRulesManagementModule) returns (address[] memory) { + 149 : 15 : return _rules.values(); + 150 : : } + 151 : : + 152 : : /*////////////////////////////////////////////////////////////// + 153 : : INTERNAL/PRIVATE FUNCTIONS + 154 : : //////////////////////////////////////////////////////////////*/ + 155 : : /** + 156 : : * @notice Clear all the rules of the array of rules + 157 : : * + 158 : : */ + 159 : 54 : function _clearRules() internal virtual { + 160 : 54 : emit ClearRules(); + 161 : 54 : _rules.clear(); + 162 : : } + 163 : : + 164 : : /** + 165 : : * @notice Remove a rule from the array of rules + 166 : : * Revert if the rule found at the specified index does not match the rule in argument + 167 : : * @param rule_ address of the target rule + 168 : : * 169 : : * - 170 : : * - 171 : : */ - 172 : 13 : function _removeRule(IRule rule_) internal virtual { - 173 : : // Should never revert because we check the presence of the rule before - 174 [ # + ]: 13 : require(_rules.remove(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); - 175 : 13 : emit RemoveRule(rule_); - 176 : : } - 177 : : - 178 : : /** - 179 : : * @dev check if a rule is valid, revert otherwise - 180 : : */ - 181 : 253 : function _checkRule(address rule_) internal view virtual { - 182 [ + ]: 253 : if (rule_ == address(0x0)) { - 183 : 3 : revert RuleEngine_RulesManagementModule_RuleAddressZeroNotAllowed(); - 184 : : } - 185 [ + ]: 250 : if (_rules.contains(rule_)) { - 186 : 6 : revert RuleEngine_RulesManagementModule_RuleAlreadyExists(); - 187 : : } - 188 : : } - 189 : : - 190 : : /* ============ Transferred functions ============ */ - 191 : : - 192 : : /** - 193 : : * @notice Go through all the rule to know if a restriction exists on the transfer - 194 : : * @dev Complexity is O(number of configured rules). Large rule sets can make - 195 : : * transfers too expensive on chains with lower block gas limits. - 196 : : * Security convention: rule contracts are expected to be trusted and must not - 197 : : * hold {RULES_MANAGEMENT_ROLE}. - 198 : : * @param from the origin address - 199 : : * @param to the destination address - 200 : : * @param value to transfer - 201 : : * - 202 : : */ - 203 : 19 : function _transferred(address from, address to, uint256 value) internal virtual { - 204 : 19 : uint256 rulesLength = _rules.length(); - 205 : 19 : for (uint256 i = 0; i < rulesLength; ++i) { - 206 : 13 : IRule(_rules.at(i)).transferred(from, to, value); - 207 : : } - 208 : : } - 209 : : - 210 : : /** - 211 : : * @notice Go through all the rule to know if a restriction exists on the transfer - 212 : : * @dev Complexity is O(number of configured rules). Large rule sets can make - 213 : : * transfers too expensive on chains with lower block gas limits. - 214 : : * Security convention: rule contracts are expected to be trusted and must not - 215 : : * hold {RULES_MANAGEMENT_ROLE}. - 216 : : * @param spender the spender address (transferFrom) - 217 : : * @param from the origin address - 218 : : * @param to the destination address - 219 : : * @param value to transfer - 220 : : * - 221 : : */ - 222 : 5 : function _transferred(address spender, address from, address to, uint256 value) internal virtual { - 223 : 5 : uint256 rulesLength = _rules.length(); - 224 : 5 : for (uint256 i = 0; i < rulesLength; ++i) { - 225 : 5 : IRule(_rules.at(i)).transferred(spender, from, to, value); - 226 : : } - 227 : : } - 228 : : - 229 : 0 : function _onlyRulesManager() internal virtual; - 230 : 0 : function _onlyRulesLimitManager() internal virtual; - 231 : : } + 170 : : */ + 171 : 13 : function _removeRule(IRule rule_) internal virtual { + 172 : : // Should never revert because we check the presence of the rule before + 173 [ # + ]: 13 : require(_rules.remove(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); + 174 : 13 : emit RemoveRule(rule_); + 175 : : } + 176 : : + 177 : : /* ============ Transferred functions ============ */ + 178 : : + 179 : : /** + 180 : : * @notice Go through all the rule to know if a restriction exists on the transfer + 181 : : * @dev Complexity is O(number of configured rules). Large rule sets can make + 182 : : * transfers too expensive on chains with lower block gas limits. + 183 : : * Security convention: rule contracts are expected to be trusted and must not + 184 : : * hold {RULES_MANAGEMENT_ROLE}. + 185 : : * @param from the origin address + 186 : : * @param to the destination address + 187 : : * @param value to transfer + 188 : : * + 189 : : */ + 190 : 25 : function _transferred(address from, address to, uint256 value) internal virtual { + 191 : 25 : uint256 rulesLength = _rules.length(); + 192 : 25 : for (uint256 i = 0; i < rulesLength; ++i) { + 193 : 19 : IRule(_rules.pos(i)).transferred(from, to, value); + 194 : : } + 195 : : } + 196 : : + 197 : : /** + 198 : : * @notice Go through all the rule to know if a restriction exists on the transfer + 199 : : * @dev Complexity is O(number of configured rules). Large rule sets can make + 200 : : * transfers too expensive on chains with lower block gas limits. + 201 : : * Security convention: rule contracts are expected to be trusted and must not + 202 : : * hold {RULES_MANAGEMENT_ROLE}. + 203 : : * @param spender the spender address (transferFrom) + 204 : : * @param from the origin address + 205 : : * @param to the destination address + 206 : : * @param value to transfer + 207 : : * + 208 : : */ + 209 : 6 : function _transferred(address spender, address from, address to, uint256 value) internal virtual { + 210 : 6 : uint256 rulesLength = _rules.length(); + 211 : 6 : for (uint256 i = 0; i < rulesLength; ++i) { + 212 : 6 : IRule(_rules.pos(i)).transferred(spender, from, to, value); + 213 : : } + 214 : : } + 215 : : + 216 : : /** + 217 : : * @dev Access control hook guarding rule management operations. + 218 : : */ + 219 : 0 : function _onlyRulesManager() internal virtual; + 220 : : + 221 : : /** + 222 : : * @dev Access control hook guarding updates to the rule cap. + 223 : : */ + 224 : 0 : function _onlyRulesLimitManager() internal virtual; + 225 : : + 226 : : /** + 227 : : * @dev check if a rule is valid, revert otherwise + 228 : : * @param rule_ The candidate rule address to validate. + 229 : : */ + 230 : 277 : function _checkRule(address rule_) internal view virtual { + 231 [ + ]: 277 : if (rule_ == address(0x0)) { + 232 : 3 : revert RuleEngine_RulesManagementModule_RuleAddressZeroNotAllowed(); + 233 : : } + 234 [ + ]: 274 : if (_rules.contains(rule_)) { + 235 : 6 : revert RuleEngine_RulesManagementModule_RuleAlreadyExists(); + 236 : : } + 237 : : } + 238 : : } 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 9945833..2924bb5 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-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 1 @@ -69,7 +69,7 @@ Hit count Sort by hit count - VersionModule.version + VersionModule.version 2 diff --git a/doc/coverage/coverage/src/modules/VersionModule.sol.func.html b/doc/coverage/coverage/src/modules/VersionModule.sol.func.html index c917463..04b7354 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-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 1 @@ -69,7 +69,7 @@ Hit count Sort by hit count - VersionModule.version + VersionModule.version 2 diff --git a/doc/coverage/coverage/src/modules/VersionModule.sol.gcov.html b/doc/coverage/coverage/src/modules/VersionModule.sol.gcov.html index 5217dc3..ca50c83 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-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 1 @@ -76,25 +76,29 @@ 5 : : /* ==== CMTAT === */ 6 : : import {IERC3643Version} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; 7 : : - 8 : : abstract contract VersionModule is IERC3643Version { - 9 : : /* ============ State Variables ============ */ - 10 : : /** - 11 : : * @dev - 12 : : * Get the current version of the smart contract - 13 : : */ - 14 : : string internal constant VERSION = "3.0.0"; - 15 : : - 16 : : /* ============ Events ============ */ - 17 : : /*////////////////////////////////////////////////////////////// - 18 : : PUBLIC/EXTERNAL FUNCTIONS - 19 : : //////////////////////////////////////////////////////////////*/ - 20 : : /** - 21 : : * @inheritdoc IERC3643Version - 22 : : */ - 23 : 2 : function version() public view virtual override(IERC3643Version) returns (string memory version_) { - 24 : 2 : return VERSION; - 25 : : } - 26 : : } + 8 : : /** + 9 : : * @title VersionModule + 10 : : * @notice Exposes the RuleEngine release version. + 11 : : */ + 12 : : abstract contract VersionModule is IERC3643Version { + 13 : : /* ============ State Variables ============ */ + 14 : : /** + 15 : : * @dev + 16 : : * Get the current version of the smart contract + 17 : : */ + 18 : : string internal constant VERSION = "3.0.0"; + 19 : : + 20 : : /* ============ Events ============ */ + 21 : : /*////////////////////////////////////////////////////////////// + 22 : : PUBLIC/EXTERNAL FUNCTIONS + 23 : : //////////////////////////////////////////////////////////////*/ + 24 : : /** + 25 : : * @inheritdoc IERC3643Version + 26 : : */ + 27 : 2 : function version() public view virtual override(IERC3643Version) returns (string memory version_) { + 28 : 2 : return VERSION; + 29 : : } + 30 : : } diff --git a/doc/coverage/coverage/src/modules/index-sort-b.html b/doc/coverage/coverage/src/modules/index-sort-b.html index a13a1af..445f53e 100644 --- a/doc/coverage/coverage/src/modules/index-sort-b.html +++ b/doc/coverage/coverage/src/modules/index-sort-b.html @@ -31,13 +31,13 @@ lcov.info Lines: - 116 - 120 - 96.7 % + 114 + 118 + 96.6 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 34 @@ -49,9 +49,9 @@ Branches: - 30 - 35 - 85.7 % + 28 + 31 + 90.3 % @@ -93,18 +93,6 @@ 82.4 % 14 / 17 - - ERC3643ComplianceModule.sol - -
93.3%93.3%
- - 93.3 % - 28 / 30 - 81.8 % - 9 / 11 - 84.6 % - 11 / 13 - VersionModule.sol @@ -129,6 +117,18 @@ 100.0 % 5 / 5 + + ERC3643ComplianceModule.sol + +
92.9%92.9%
+ + 92.9 % + 26 / 28 + 81.8 % + 9 / 11 + 100.0 % + 9 / 9 +
diff --git a/doc/coverage/coverage/src/modules/index-sort-f.html b/doc/coverage/coverage/src/modules/index-sort-f.html index 556cec3..f1d44ca 100644 --- a/doc/coverage/coverage/src/modules/index-sort-f.html +++ b/doc/coverage/coverage/src/modules/index-sort-f.html @@ -31,13 +31,13 @@ lcov.info Lines: - 116 - 120 - 96.7 % + 114 + 118 + 96.6 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 34 @@ -49,9 +49,9 @@ Branches: - 30 - 35 - 85.7 % + 28 + 31 + 90.3 % @@ -84,14 +84,14 @@ ERC3643ComplianceModule.sol -
93.3%93.3%
+
92.9%92.9%
- 93.3 % - 28 / 30 + 92.9 % + 26 / 28 81.8 % 9 / 11 - 84.6 % - 11 / 13 + 100.0 % + 9 / 9 RulesManagementModule.sol diff --git a/doc/coverage/coverage/src/modules/index-sort-l.html b/doc/coverage/coverage/src/modules/index-sort-l.html index 557fca2..9f7566d 100644 --- a/doc/coverage/coverage/src/modules/index-sort-l.html +++ b/doc/coverage/coverage/src/modules/index-sort-l.html @@ -31,13 +31,13 @@ lcov.info Lines: - 116 - 120 - 96.7 % + 114 + 118 + 96.6 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 34 @@ -49,9 +49,9 @@ Branches: - 30 - 35 - 85.7 % + 28 + 31 + 90.3 % @@ -84,14 +84,14 @@ ERC3643ComplianceModule.sol -
93.3%93.3%
+
92.9%92.9%
- 93.3 % - 28 / 30 + 92.9 % + 26 / 28 81.8 % 9 / 11 - 84.6 % - 11 / 13 + 100.0 % + 9 / 9 RulesManagementModule.sol diff --git a/doc/coverage/coverage/src/modules/index.html b/doc/coverage/coverage/src/modules/index.html index f2f0b38..b87c0a2 100644 --- a/doc/coverage/coverage/src/modules/index.html +++ b/doc/coverage/coverage/src/modules/index.html @@ -31,13 +31,13 @@ lcov.info Lines: - 116 - 120 - 96.7 % + 114 + 118 + 96.6 % Date: - 2026-05-22 15:31:49 + 2026-08-13 15:50:18 Functions: 34 @@ -49,9 +49,9 @@ Branches: - 30 - 35 - 85.7 % + 28 + 31 + 90.3 % @@ -96,14 +96,14 @@ ERC3643ComplianceModule.sol -
93.3%93.3%
+
92.9%92.9%
- 93.3 % - 28 / 30 + 92.9 % + 26 / 28 81.8 % 9 / 11 - 84.6 % - 11 / 13 + 100.0 % + 9 / 9 RulesManagementModule.sol diff --git a/doc/coverage/lcov.info b/doc/coverage/lcov.info index 77ded88..e97410e 100644 --- a/doc/coverage/lcov.info +++ b/doc/coverage/lcov.info @@ -1,218 +1,281 @@ TN: +SF:script/CMTATWithRuleEngineScript.s.sol +DA:20,1 +FN:20,CMTATWithRuleEngineScript.run +FNDA:1,CMTATWithRuleEngineScript.run +DA:22,1 +DA:23,1 +DA:24,1 +DA:25,1 +DA:27,1 +DA:28,1 +DA:29,1 +DA:30,1 +DA:37,1 +DA:38,1 +DA:39,1 +DA:40,1 +DA:42,1 +DA:43,1 +DA:45,1 +DA:46,1 +DA:47,1 +DA:48,1 +DA:50,1 +FNF:1 +FNH:1 +LF:20 +LH:20 +BRF:0 +BRH:0 +end_of_record +TN: +SF:script/RuleEngineScript.s.sol +DA:32,1 +FN:32,RuleEngineScript.run +FNDA:1,RuleEngineScript.run +DA:34,1 +DA:35,1 +DA:36,1 +DA:37,1 +DA:39,1 +DA:40,1 +DA:42,1 +DA:43,1 +DA:44,1 +DA:45,1 +DA:47,1 +DA:48,1 +DA:49,1 +DA:53,1 +DA:54,1 +FNF:1 +FNH:1 +LF:16 +LH:16 +BRF:0 +BRH:0 +end_of_record +TN: SF:src/RuleEngineBase.sol -DA:43,6 -FN:43,RuleEngineBase.transferred.0 -FNDA:6,RuleEngineBase.transferred.0 -DA:50,5 -DA:56,17 -FN:56,RuleEngineBase.transferred.1 -FNDA:17,RuleEngineBase.transferred.1 -DA:62,15 -DA:66,4 -FN:66,RuleEngineBase.created -FNDA:4,RuleEngineBase.created -DA:67,2 -DA:71,4 -FN:71,RuleEngineBase.destroyed -FNDA:4,RuleEngineBase.destroyed -DA:72,2 -DA:84,34 -FN:84,RuleEngineBase.detectTransferRestriction -FNDA:34,RuleEngineBase.detectTransferRestriction -DA:91,59 -DA:97,18 -FN:97,RuleEngineBase.detectTransferRestrictionFrom -FNDA:18,RuleEngineBase.detectTransferRestrictionFrom -DA:104,39 -DA:110,19 -FN:110,RuleEngineBase.messageForTransferRestriction -FNDA:19,RuleEngineBase.messageForTransferRestriction -DA:117,19 -DA:123,25 -FN:123,RuleEngineBase.canTransfer -FNDA:25,RuleEngineBase.canTransfer -DA:130,25 -DA:136,21 -FN:136,RuleEngineBase.canTransferFrom -FNDA:21,RuleEngineBase.canTransferFrom -DA:143,21 -DA:149,59 -FN:149,RuleEngineBase._detectTransferRestriction -FNDA:59,RuleEngineBase._detectTransferRestriction -DA:150,59 -DA:151,59 -DA:152,59 -DA:153,59 -BRDA:153,0,0,43 -DA:154,43 -DA:157,16 -DA:160,39 -FN:160,RuleEngineBase._detectTransferRestrictionFrom -FNDA:39,RuleEngineBase._detectTransferRestrictionFrom -DA:166,39 -DA:167,39 -DA:168,39 -DA:169,39 -BRDA:169,1,0,29 -DA:170,29 -DA:173,10 -DA:182,19 -FN:182,RuleEngineBase._messageForTransferRestriction -FNDA:19,RuleEngineBase._messageForTransferRestriction -DA:183,19 -DA:184,19 -DA:185,16 -BRDA:185,2,0,14 -DA:186,14 -DA:189,5 -DA:195,253 -FN:195,RuleEngineBase._checkRule -FNDA:253,RuleEngineBase._checkRule -DA:196,253 -DA:197,244 -BRDA:197,3,0,6 -DA:198,6 -DA:206,56 -FN:206,RuleEngineBase._supportsRuleEngineBaseInterface +DA:53,7 +FN:53,RuleEngineBase.transferred.0 +FNDA:7,RuleEngineBase.transferred.0 +DA:60,6 +DA:66,19 +FN:66,RuleEngineBase.transferred.1 +FNDA:19,RuleEngineBase.transferred.1 +DA:72,16 +DA:76,9 +FN:76,RuleEngineBase.created +FNDA:9,RuleEngineBase.created +DA:77,6 +DA:81,6 +FN:81,RuleEngineBase.destroyed +FNDA:6,RuleEngineBase.destroyed +DA:82,3 +DA:94,36 +FN:94,RuleEngineBase.detectTransferRestriction +FNDA:36,RuleEngineBase.detectTransferRestriction +DA:101,70 +DA:107,19 +FN:107,RuleEngineBase.detectTransferRestrictionFrom +FNDA:19,RuleEngineBase.detectTransferRestrictionFrom +DA:114,41 +DA:120,29 +FN:120,RuleEngineBase.messageForTransferRestriction +FNDA:29,RuleEngineBase.messageForTransferRestriction +DA:127,29 +DA:133,34 +FN:133,RuleEngineBase.canTransfer +FNDA:34,RuleEngineBase.canTransfer +DA:140,34 +DA:146,22 +FN:146,RuleEngineBase.canTransferFrom +FNDA:22,RuleEngineBase.canTransferFrom +DA:153,22 +DA:166,70 +FN:166,RuleEngineBase._detectTransferRestriction +FNDA:70,RuleEngineBase._detectTransferRestriction +DA:167,70 +DA:168,70 +DA:169,72 +DA:170,72 +BRDA:170,0,0,47 +DA:171,47 +DA:174,23 +DA:185,41 +FN:185,RuleEngineBase._detectTransferRestrictionFrom +FNDA:41,RuleEngineBase._detectTransferRestrictionFrom +DA:191,41 +DA:192,41 +DA:193,43 +DA:194,43 +BRDA:194,1,0,31 +DA:195,31 +DA:198,10 +DA:211,29 +FN:211,RuleEngineBase._messageForTransferRestriction +FNDA:29,RuleEngineBase._messageForTransferRestriction +DA:212,29 +BRDA:212,2,0,8 +DA:213,8 +DA:215,21 +DA:216,21 +DA:217,18 +BRDA:217,3,0,14 +DA:218,14 +DA:221,7 +DA:228,277 +FN:228,RuleEngineBase._checkRule +FNDA:277,RuleEngineBase._checkRule +DA:229,277 +DA:230,268 +BRDA:230,4,0,6 +DA:231,6 +DA:241,56 +FN:241,RuleEngineBase._supportsRuleEngineBaseInterface FNDA:56,RuleEngineBase._supportsRuleEngineBaseInterface -DA:207,56 -DA:208,51 -DA:209,41 -DA:210,36 -DA:211,31 -DA:212,21 +DA:242,56 +DA:243,51 +DA:244,41 +DA:245,36 +DA:246,31 +DA:247,21 FNF:14 FNH:14 -LF:49 -LH:49 -BRF:4 -BRH:4 +LF:51 +LH:51 +BRF:5 +BRH:5 end_of_record TN: SF:src/RuleEngineOwnableShared.sol -DA:22,175 -FN:22,RuleEngineOwnableShared.constructor -FNDA:175,RuleEngineOwnableShared.constructor -DA:23,175 -BRDA:23,0,0,1 -DA:24,1 -DA:29,35 -FN:29,RuleEngineOwnableShared.supportsInterface +DA:27,186 +FN:27,RuleEngineOwnableShared.constructor +FNDA:186,RuleEngineOwnableShared.constructor +DA:28,186 +BRDA:28,0,0,1 +DA:29,1 +DA:32,186 +DA:41,35 +FN:41,RuleEngineOwnableShared.supportsInterface FNDA:35,RuleEngineOwnableShared.supportsInterface -DA:30,35 -DA:31,5 -DA:37,10 -FN:37,RuleEngineOwnableShared._checkOwnershipTransferTarget +DA:42,35 +DA:43,5 +DA:50,10 +FN:50,RuleEngineOwnableShared._checkOwnershipTransferTarget FNDA:10,RuleEngineOwnableShared._checkOwnershipTransferTarget -DA:38,10 -BRDA:38,1,0,2 -DA:39,2 -DA:50,249 -FN:50,RuleEngineOwnableShared._msgSender -FNDA:249,RuleEngineOwnableShared._msgSender -DA:51,249 -DA:57,2 -FN:57,RuleEngineOwnableShared._msgData +DA:51,10 +BRDA:51,1,0,2 +DA:52,2 +DA:64,255 +FN:64,RuleEngineOwnableShared._msgSender +FNDA:255,RuleEngineOwnableShared._msgSender +DA:65,255 +DA:72,2 +FN:72,RuleEngineOwnableShared._msgData FNDA:2,RuleEngineOwnableShared._msgData -DA:58,2 -DA:64,251 -FN:64,RuleEngineOwnableShared._contextSuffixLength -FNDA:251,RuleEngineOwnableShared._contextSuffixLength -DA:65,251 +DA:73,2 +DA:80,257 +FN:80,RuleEngineOwnableShared._contextSuffixLength +FNDA:257,RuleEngineOwnableShared._contextSuffixLength +DA:81,257 FNF:6 FNH:6 -LF:15 -LH:15 +LF:16 +LH:16 BRF:2 BRH:2 end_of_record TN: SF:src/deployment/RuleEngine.sol -DA:27,165 -FN:27,RuleEngine.constructor -FNDA:165,RuleEngine.constructor -DA:30,165 -BRDA:30,0,0,1 -DA:31,1 -DA:33,164 -BRDA:33,1,0,31 -DA:34,31 -DA:36,164 -DA:48,39 -FN:48,RuleEngine.grantRole +DA:37,184 +FN:37,RuleEngine.constructor +FNDA:184,RuleEngine.constructor +DA:40,184 +BRDA:40,0,0,1 +DA:41,1 +DA:43,183 +BRDA:43,1,0,32 +DA:44,32 +DA:46,183 +DA:48,183 +DA:62,39 +FN:62,RuleEngine.grantRole FNDA:39,RuleEngine.grantRole -DA:49,39 -BRDA:49,2,0,3 -DA:50,3 -DA:52,36 -DA:59,173 -FN:59,RuleEngine.hasRole -FNDA:173,RuleEngine.hasRole -DA:66,490 -BRDA:66,3,0,236 -BRDA:66,3,1,254 -DA:67,236 -DA:69,254 -DA:74,21 -FN:74,RuleEngine.supportsInterface +DA:63,39 +BRDA:63,2,0,3 +DA:64,3 +DA:66,36 +DA:76,192 +FN:76,RuleEngine.hasRole +FNDA:192,RuleEngine.hasRole +DA:83,544 +BRDA:83,3,0,270 +BRDA:83,3,1,274 +DA:84,270 +DA:86,274 +DA:96,21 +FN:96,RuleEngine.supportsInterface FNDA:21,RuleEngine.supportsInterface -DA:81,21 -DA:87,42 -FN:87,RuleEngine._onlyComplianceManager -FNDA:42,RuleEngine._onlyComplianceManager -DA:88,196 -FN:88,RuleEngine._onlyRulesManager -FNDA:196,RuleEngine._onlyRulesManager -DA:89,5 -FN:89,RuleEngine._onlyRulesLimitManager +DA:103,21 +DA:112,56 +FN:112,RuleEngine._onlyComplianceManager +FNDA:56,RuleEngine._onlyComplianceManager +DA:117,217 +FN:117,RuleEngine._onlyRulesManager +FNDA:217,RuleEngine._onlyRulesManager +DA:122,5 +FN:122,RuleEngine._onlyRulesLimitManager FNDA:5,RuleEngine._onlyRulesLimitManager -DA:94,535 -FN:94,RuleEngine._msgSender -FNDA:535,RuleEngine._msgSender -DA:95,535 -DA:101,1 -FN:101,RuleEngine._msgData +DA:128,614 +FN:128,RuleEngine._msgSender +FNDA:614,RuleEngine._msgSender +DA:129,614 +DA:136,1 +FN:136,RuleEngine._msgData FNDA:1,RuleEngine._msgData -DA:102,1 -DA:108,536 -FN:108,RuleEngine._contextSuffixLength -FNDA:536,RuleEngine._contextSuffixLength -DA:109,536 +DA:137,1 +DA:144,615 +FN:144,RuleEngine._contextSuffixLength +FNDA:615,RuleEngine._contextSuffixLength +DA:145,615 FNF:10 FNH:10 -LF:25 -LH:25 +LF:26 +LH:26 BRF:5 BRH:5 end_of_record TN: SF:src/deployment/RuleEngineOwnable.sol -DA:27,65 -FN:27,RuleEngineOwnable._onlyRulesManager -FNDA:65,RuleEngineOwnable._onlyRulesManager -DA:28,2 -FN:28,RuleEngineOwnable._onlyRulesLimitManager +DA:28,5 +FN:28,RuleEngineOwnable.transferOwnership +FNDA:5,RuleEngineOwnable.transferOwnership +DA:29,4 +DA:30,3 +DA:37,69 +FN:37,RuleEngineOwnable._onlyRulesManager +FNDA:69,RuleEngineOwnable._onlyRulesManager +DA:42,2 +FN:42,RuleEngineOwnable._onlyRulesLimitManager FNDA:2,RuleEngineOwnable._onlyRulesLimitManager -DA:33,46 -FN:33,RuleEngineOwnable._onlyComplianceManager +DA:47,46 +FN:47,RuleEngineOwnable._onlyComplianceManager FNDA:46,RuleEngineOwnable._onlyComplianceManager -DA:39,5 -FN:39,RuleEngineOwnable.transferOwnership -FNDA:5,RuleEngineOwnable.transferOwnership -DA:40,4 -DA:41,3 -DA:47,181 -FN:47,RuleEngineOwnable._msgSender -FNDA:181,RuleEngineOwnable._msgSender -DA:48,181 -DA:54,1 -FN:54,RuleEngineOwnable._msgData +DA:53,185 +FN:53,RuleEngineOwnable._msgSender +FNDA:185,RuleEngineOwnable._msgSender +DA:54,185 +DA:61,1 +FN:61,RuleEngineOwnable._msgData FNDA:1,RuleEngineOwnable._msgData -DA:55,1 -DA:61,182 -FN:61,RuleEngineOwnable._contextSuffixLength -FNDA:182,RuleEngineOwnable._contextSuffixLength -DA:62,182 +DA:62,1 +DA:69,186 +FN:69,RuleEngineOwnable._contextSuffixLength +FNDA:186,RuleEngineOwnable._contextSuffixLength +DA:70,186 FNF:7 FNH:7 LF:12 @@ -222,37 +285,37 @@ BRH:0 end_of_record TN: SF:src/deployment/RuleEngineOwnable2Step.sol -DA:31,5 -FN:31,RuleEngineOwnable2Step._onlyRulesManager -FNDA:5,RuleEngineOwnable2Step._onlyRulesManager -DA:32,2 -FN:32,RuleEngineOwnable2Step._onlyRulesLimitManager -FNDA:2,RuleEngineOwnable2Step._onlyRulesLimitManager -DA:37,25 -FN:37,RuleEngineOwnable2Step._onlyComplianceManager -FNDA:25,RuleEngineOwnable2Step._onlyComplianceManager -DA:43,6 -FN:43,RuleEngineOwnable2Step.transferOwnership +DA:32,6 +FN:32,RuleEngineOwnable2Step.transferOwnership FNDA:6,RuleEngineOwnable2Step.transferOwnership -DA:44,6 -DA:45,5 -DA:49,13 -FN:49,RuleEngineOwnable2Step.supportsInterface +DA:33,6 +DA:34,5 +DA:43,13 +FN:43,RuleEngineOwnable2Step.supportsInterface FNDA:13,RuleEngineOwnable2Step.supportsInterface DA:50,13 DA:51,11 -DA:57,68 -FN:57,RuleEngineOwnable2Step._msgSender -FNDA:68,RuleEngineOwnable2Step._msgSender -DA:58,68 -DA:64,1 -FN:64,RuleEngineOwnable2Step._msgData +DA:58,7 +FN:58,RuleEngineOwnable2Step._onlyRulesManager +FNDA:7,RuleEngineOwnable2Step._onlyRulesManager +DA:63,2 +FN:63,RuleEngineOwnable2Step._onlyRulesLimitManager +FNDA:2,RuleEngineOwnable2Step._onlyRulesLimitManager +DA:68,25 +FN:68,RuleEngineOwnable2Step._onlyComplianceManager +FNDA:25,RuleEngineOwnable2Step._onlyComplianceManager +DA:74,70 +FN:74,RuleEngineOwnable2Step._msgSender +FNDA:70,RuleEngineOwnable2Step._msgSender +DA:75,70 +DA:82,1 +FN:82,RuleEngineOwnable2Step._msgData FNDA:1,RuleEngineOwnable2Step._msgData -DA:65,1 -DA:71,69 -FN:71,RuleEngineOwnable2Step._contextSuffixLength -FNDA:69,RuleEngineOwnable2Step._contextSuffixLength -DA:72,69 +DA:83,1 +DA:90,71 +FN:90,RuleEngineOwnable2Step._contextSuffixLength +FNDA:71,RuleEngineOwnable2Step._contextSuffixLength +DA:91,71 FNF:8 FNH:8 LF:15 @@ -262,49 +325,49 @@ BRH:0 end_of_record TN: SF:src/modules/ERC3643ComplianceExtendedModule.sol -DA:21,18 -FN:21,ERC3643ComplianceExtendedModule.bindTokens +DA:28,18 +FN:28,ERC3643ComplianceExtendedModule.bindTokens FNDA:18,ERC3643ComplianceExtendedModule.bindTokens -DA:22,15 -DA:23,24 -DA:28,9 -FN:28,ERC3643ComplianceExtendedModule.unbindTokens +DA:29,15 +DA:30,24 +DA:35,9 +FN:35,ERC3643ComplianceExtendedModule.unbindTokens FNDA:9,ERC3643ComplianceExtendedModule.unbindTokens -DA:29,6 -DA:30,12 -DA:35,14 -FN:35,ERC3643ComplianceExtendedModule.setTokenSelfBindingApproval -FNDA:14,ERC3643ComplianceExtendedModule.setTokenSelfBindingApproval -DA:36,11 -BRDA:36,0,0,3 -BRDA:36,0,1,8 -DA:37,8 -DA:38,8 -DA:42,12 -FN:42,ERC3643ComplianceExtendedModule.setTokenSelfBindingApprovalBatch +DA:36,6 +DA:37,12 +DA:42,27 +FN:42,ERC3643ComplianceExtendedModule.setTokenSelfBindingApproval +FNDA:27,ERC3643ComplianceExtendedModule.setTokenSelfBindingApproval +DA:43,24 +BRDA:43,0,0,3 +BRDA:43,0,1,21 +DA:44,21 +DA:45,21 +DA:49,12 +FN:49,ERC3643ComplianceExtendedModule.setTokenSelfBindingApprovalBatch FNDA:12,ERC3643ComplianceExtendedModule.setTokenSelfBindingApprovalBatch -DA:48,9 -DA:49,18 -DA:50,18 -BRDA:50,1,0,3 -BRDA:50,1,1,15 -DA:51,15 -DA:53,6 -DA:57,6 -FN:57,ERC3643ComplianceExtendedModule.isTokenSelfBindingApproved +DA:55,9 +DA:56,18 +DA:57,18 +BRDA:57,1,0,3 +BRDA:57,1,1,15 +DA:58,15 +DA:60,6 +DA:64,6 +FN:64,ERC3643ComplianceExtendedModule.isTokenSelfBindingApproved FNDA:6,ERC3643ComplianceExtendedModule.isTokenSelfBindingApproved -DA:58,6 -DA:62,4 -FN:62,ERC3643ComplianceExtendedModule.getTokenBounds +DA:65,6 +DA:69,4 +FN:69,ERC3643ComplianceExtendedModule.getTokenBounds FNDA:4,ERC3643ComplianceExtendedModule.getTokenBounds -DA:63,4 -DA:70,72 -FN:70,ERC3643ComplianceExtendedModule._authorizeComplianceBindingChange -FNDA:72,ERC3643ComplianceExtendedModule._authorizeComplianceBindingChange -DA:71,72 -BRDA:71,2,0,72 -DA:72,72 -DA:74,60 +DA:70,4 +DA:78,87 +FN:78,ERC3643ComplianceExtendedModule._authorizeComplianceBindingChange +FNDA:87,ERC3643ComplianceExtendedModule._authorizeComplianceBindingChange +DA:79,87 +BRDA:79,2,0,87 +DA:80,87 +DA:82,61 FNF:7 FNH:7 LF:24 @@ -314,199 +377,193 @@ BRH:5 end_of_record TN: SF:src/modules/ERC3643ComplianceModule.sol -DA:26,6 -FN:26,ERC3643ComplianceModule.onlyBoundToken -FNDA:6,ERC3643ComplianceModule.onlyBoundToken -DA:27,6 -DA:31,9 -FN:31,ERC3643ComplianceModule.onlyComplianceManager -FNDA:9,ERC3643ComplianceModule.onlyComplianceManager +DA:27,7 +FN:27,ERC3643ComplianceModule.onlyBoundToken +FNDA:7,ERC3643ComplianceModule.onlyBoundToken +DA:28,7 DA:32,9 -DA:51,52 -FN:51,ERC3643ComplianceModule.bindToken -FNDA:52,ERC3643ComplianceModule.bindToken -DA:52,52 -DA:53,45 -DA:62,20 -FN:62,ERC3643ComplianceModule.unbindToken -FNDA:20,ERC3643ComplianceModule.unbindToken -DA:63,20 -DA:64,13 -DA:68,37 -FN:68,ERC3643ComplianceModule.isTokenBound -FNDA:37,ERC3643ComplianceModule.isTokenBound -DA:69,37 -DA:73,5 -FN:73,ERC3643ComplianceModule.getTokenBound -FNDA:5,ERC3643ComplianceModule.getTokenBound -DA:74,5 -BRDA:74,0,0,3 -BRDA:74,0,1,2 -DA:77,3 -DA:79,2 -DA:87,25 -FN:87,ERC3643ComplianceModule._unbindToken -FNDA:25,ERC3643ComplianceModule._unbindToken -DA:88,25 -BRDA:88,1,0,5 -BRDA:88,1,1,20 -DA:90,20 -BRDA:90,2,0,- -BRDA:90,2,1,20 -DA:92,20 -DA:95,101 -FN:95,ERC3643ComplianceModule._bindToken -FNDA:101,ERC3643ComplianceModule._bindToken -DA:96,101 -BRDA:96,3,0,5 -BRDA:96,3,1,96 -DA:97,96 -BRDA:97,4,0,5 -BRDA:97,4,1,91 -DA:99,91 -BRDA:99,5,0,- -BRDA:99,5,1,91 -DA:100,91 -DA:103,31 -FN:103,ERC3643ComplianceModule._checkBoundToken -FNDA:31,ERC3643ComplianceModule._checkBoundToken -DA:104,31 -BRDA:104,6,0,7 -DA:105,7 -DA:109,0 -FN:109,ERC3643ComplianceModule._authorizeComplianceBindingChange +FN:32,ERC3643ComplianceModule.onlyComplianceManager +FNDA:9,ERC3643ComplianceModule.onlyComplianceManager +DA:33,9 +DA:52,66 +FN:52,ERC3643ComplianceModule.bindToken +FNDA:66,ERC3643ComplianceModule.bindToken +DA:53,66 +DA:54,58 +DA:63,21 +FN:63,ERC3643ComplianceModule.unbindToken +FNDA:21,ERC3643ComplianceModule.unbindToken +DA:64,21 +DA:65,14 +DA:69,41 +FN:69,ERC3643ComplianceModule.isTokenBound +FNDA:41,ERC3643ComplianceModule.isTokenBound +DA:70,41 +DA:74,7 +FN:74,ERC3643ComplianceModule.getTokenBound +FNDA:7,ERC3643ComplianceModule.getTokenBound +DA:75,7 +BRDA:75,0,0,5 +BRDA:75,0,1,2 +DA:78,5 +DA:80,2 +DA:92,26 +FN:92,ERC3643ComplianceModule._unbindToken +FNDA:26,ERC3643ComplianceModule._unbindToken +DA:95,26 +BRDA:95,1,0,5 +BRDA:95,1,1,21 +DA:97,21 +DA:104,115 +FN:104,ERC3643ComplianceModule._bindToken +FNDA:115,ERC3643ComplianceModule._bindToken +DA:105,115 +BRDA:105,2,0,5 +BRDA:105,2,1,110 +DA:108,110 +BRDA:108,3,0,5 +BRDA:108,3,1,105 +DA:109,105 +DA:116,0 +FN:116,ERC3643ComplianceModule._authorizeComplianceBindingChange FNDA:0,ERC3643ComplianceModule._authorizeComplianceBindingChange -DA:110,0 -FN:110,ERC3643ComplianceModule._onlyComplianceManager +DA:121,0 +FN:121,ERC3643ComplianceModule._onlyComplianceManager FNDA:0,ERC3643ComplianceModule._onlyComplianceManager +DA:126,41 +FN:126,ERC3643ComplianceModule._checkBoundToken +FNDA:41,ERC3643ComplianceModule._checkBoundToken +DA:127,41 +BRDA:127,4,0,10 +DA:128,10 FNF:11 FNH:9 -LF:30 -LH:28 -BRF:13 -BRH:11 +LF:28 +LH:26 +BRF:9 +BRH:9 end_of_record TN: SF:src/modules/RulesManagementModule.sol -DA:16,16 +DA:16,19 FN:16,RulesManagementModule.onlyRulesManager -FNDA:16,RulesManagementModule.onlyRulesManager -DA:17,16 +FNDA:19,RulesManagementModule.onlyRulesManager +DA:17,19 DA:21,9 FN:21,RulesManagementModule.onlyRulesLimitManager FNDA:9,RulesManagementModule.onlyRulesLimitManager DA:22,9 -DA:52,51 +DA:52,53 FN:52,RulesManagementModule.setRules -FNDA:51,RulesManagementModule.setRules -DA:53,49 +FNDA:53,RulesManagementModule.setRules +DA:53,51 BRDA:53,0,0,6 DA:54,6 -DA:56,43 +DA:56,45 BRDA:56,1,0,1 DA:57,1 -DA:59,42 -BRDA:59,2,0,36 -DA:60,36 -DA:62,42 -DA:63,79 -DA:65,74 +DA:59,44 +BRDA:59,2,0,38 +DA:60,38 +DA:62,44 +DA:63,81 +DA:65,76 BRDA:65,3,0,- -BRDA:65,3,1,74 -DA:66,74 -DA:73,16 +BRDA:65,3,1,76 +DA:66,76 +DA:73,19 FN:73,RulesManagementModule.clearRules -FNDA:16,RulesManagementModule.clearRules -DA:74,13 -DA:82,181 +FNDA:19,RulesManagementModule.clearRules +DA:74,16 +DA:82,203 FN:82,RulesManagementModule.addRule -FNDA:181,RulesManagementModule.addRule -DA:83,176 +FNDA:203,RulesManagementModule.addRule +DA:83,198 BRDA:83,4,0,2 DA:84,2 -DA:86,174 -DA:87,164 +DA:86,196 +DA:87,186 BRDA:87,5,0,- -BRDA:87,5,1,164 -DA:88,164 -DA:94,8 -FN:94,RulesManagementModule.maxRules -FNDA:8,RulesManagementModule.maxRules -DA:95,8 -DA:101,9 -FN:101,RulesManagementModule.setMaxRules +BRDA:87,5,1,186 +DA:88,186 +DA:94,9 +FN:94,RulesManagementModule.setMaxRules FNDA:9,RulesManagementModule.setMaxRules -DA:102,5 -BRDA:102,6,0,1 -DA:103,1 -DA:105,4 -DA:106,4 -DA:112,18 -FN:112,RulesManagementModule.removeRule +DA:95,5 +BRDA:95,6,0,1 +DA:96,1 +DA:98,4 +DA:99,4 +DA:105,18 +FN:105,RulesManagementModule.removeRule FNDA:18,RulesManagementModule.removeRule -DA:113,16 -BRDA:113,7,0,3 -BRDA:113,7,1,13 -DA:114,13 -DA:122,183 -FN:122,RulesManagementModule.rulesCount -FNDA:183,RulesManagementModule.rulesCount -DA:123,300 -DA:129,79 -FN:129,RulesManagementModule.containsRule -FNDA:79,RulesManagementModule.containsRule -DA:130,89 -DA:136,5 -FN:136,RulesManagementModule.rule +DA:106,16 +BRDA:106,7,0,3 +BRDA:106,7,1,13 +DA:107,13 +DA:114,8 +FN:114,RulesManagementModule.maxRules +FNDA:8,RulesManagementModule.maxRules +DA:115,8 +DA:121,191 +FN:121,RulesManagementModule.rulesCount +FNDA:191,RulesManagementModule.rulesCount +DA:122,323 +DA:128,82 +FN:128,RulesManagementModule.containsRule +FNDA:82,RulesManagementModule.containsRule +DA:129,92 +DA:135,5 +FN:135,RulesManagementModule.rule FNDA:5,RulesManagementModule.rule -DA:137,133 -BRDA:137,8,0,131 -BRDA:137,8,1,2 -DA:140,131 -DA:142,2 -DA:149,15 -FN:149,RulesManagementModule.rules +DA:136,152 +BRDA:136,8,0,150 +BRDA:136,8,1,2 +DA:139,150 +DA:141,2 +DA:148,15 +FN:148,RulesManagementModule.rules FNDA:15,RulesManagementModule.rules -DA:150,15 -DA:160,49 -FN:160,RulesManagementModule._clearRules -FNDA:49,RulesManagementModule._clearRules -DA:161,49 -DA:162,49 -DA:172,13 -FN:172,RulesManagementModule._removeRule +DA:149,15 +DA:159,54 +FN:159,RulesManagementModule._clearRules +FNDA:54,RulesManagementModule._clearRules +DA:160,54 +DA:161,54 +DA:171,13 +FN:171,RulesManagementModule._removeRule FNDA:13,RulesManagementModule._removeRule +DA:173,13 +BRDA:173,9,0,- +BRDA:173,9,1,13 DA:174,13 -BRDA:174,9,0,- -BRDA:174,9,1,13 -DA:175,13 -DA:181,253 -FN:181,RulesManagementModule._checkRule -FNDA:253,RulesManagementModule._checkRule -DA:182,253 -BRDA:182,10,0,3 -DA:183,3 -DA:185,250 -BRDA:185,11,0,6 -DA:186,6 -DA:203,19 -FN:203,RulesManagementModule._transferred.0 -FNDA:19,RulesManagementModule._transferred.0 -DA:204,19 -DA:205,19 -DA:206,13 -DA:222,5 -FN:222,RulesManagementModule._transferred.1 -FNDA:5,RulesManagementModule._transferred.1 -DA:223,5 -DA:224,5 -DA:225,5 -DA:229,0 -FN:229,RulesManagementModule._onlyRulesManager +DA:190,25 +FN:190,RulesManagementModule._transferred.0 +FNDA:25,RulesManagementModule._transferred.0 +DA:191,25 +DA:192,25 +DA:193,19 +DA:209,6 +FN:209,RulesManagementModule._transferred.1 +FNDA:6,RulesManagementModule._transferred.1 +DA:210,6 +DA:211,6 +DA:212,6 +DA:219,0 +FN:219,RulesManagementModule._onlyRulesManager FNDA:0,RulesManagementModule._onlyRulesManager -DA:230,0 -FN:230,RulesManagementModule._onlyRulesLimitManager +DA:224,0 +FN:224,RulesManagementModule._onlyRulesLimitManager FNDA:0,RulesManagementModule._onlyRulesLimitManager +DA:230,277 +FN:230,RulesManagementModule._checkRule +FNDA:277,RulesManagementModule._checkRule +DA:231,277 +BRDA:231,10,0,3 +DA:232,3 +DA:234,274 +BRDA:234,11,0,6 +DA:235,6 FNF:19 FNH:17 LF:64 @@ -516,10 +573,10 @@ BRH:14 end_of_record TN: SF:src/modules/VersionModule.sol -DA:23,2 -FN:23,VersionModule.version +DA:27,2 +FN:27,VersionModule.version FNDA:2,VersionModule.version -DA:24,2 +DA:28,2 FNF:1 FNH:1 LF:2 diff --git a/doc/schema/plantuml/ruleengine-flow-cmtat.png b/doc/schema/plantuml/ruleengine-flow-cmtat.png new file mode 100644 index 0000000..d91880f Binary files /dev/null and b/doc/schema/plantuml/ruleengine-flow-cmtat.png differ diff --git a/doc/schema/plantuml/ruleengine-flow-cmtat.puml b/doc/schema/plantuml/ruleengine-flow-cmtat.puml new file mode 100644 index 0000000..223b295 --- /dev/null +++ b/doc/schema/plantuml/ruleengine-flow-cmtat.puml @@ -0,0 +1,76 @@ +@startuml +title RuleEngine with a CMTAT token + +skinparam shadowing false +skinparam sequenceMessageAlign direction +skinparam ParticipantPadding 12 + +actor "Token holder\n/ operator" as Holder +participant "CMTAT token" as Token +participant "RuleEngine" as Engine +collections "Rule contracts\n(IRule)" as Rules + +== State-changing path == + +alt standard transfer + Holder -> Token : transfer(to, value) + activate Token + Token -> Token : no spender\n(address(0) internally) + Token -> Engine : transferred(from, to, value) +else transferFrom, mint or burn + Holder -> Token : transferFrom(from, to, value)\nmint(to, value) / burn(from, value) + Token -> Token : spender = _msgSender() + Token -> Engine : transferred(spender, from, to, value) +end + +note over Token + CMTAT picks the overload on spender != address(0) + (ValidationModuleRuleEngine._callRuleEngineTransferred). + The zero address is a branch condition only: it is never + forwarded, so the engine is never called with a zero spender. + The 4-argument overload is declared by CMTAT's IRuleEngine. + CMTAT never calls created() or destroyed(). +end note + +activate Engine +Engine -> Engine : onlyBoundToken + +alt msg.sender is not a bound token + Engine -->> Token : revert\nRuleEngine_ERC3643Compliance_UnauthorizedCaller + Token -->> Holder : transaction reverted +else caller is bound + loop for each rule in _rules (capped by maxRules, default 10) + Engine -> Rules : transferred(...)\nsame overload as the entry point + activate Rules + alt rule forbids the transfer + Rules -->> Engine : revert (no return value) + Engine -->> Token : bubble up the revert + Token -->> Holder : transaction reverted + note right of Rules + Remaining rules never run. + end note + else rule allows the transfer + Rules --> Engine : return + end + deactivate Rules + end + Engine --> Token : return + Token --> Holder : transfer executed +end +deactivate Engine +deactivate Token + +== Read-only path (ERC-1404, never reverts) == + +Holder -> Engine : detectTransferRestriction(from, to, value)\ndetectTransferRestrictionFrom(spender, from, to, value) +activate Engine +loop for each rule, until a rule returns a non-zero code + Engine -> Rules : detectTransferRestriction(...) + activate Rules + Rules --> Engine : ERC-1404 restriction code + deactivate Rules +end +Engine --> Holder : first non-zero code,\nor 0 when the transfer is allowed +deactivate Engine + +@enduml diff --git a/doc/schema/plantuml/ruleengine-flow-erc3643.png b/doc/schema/plantuml/ruleengine-flow-erc3643.png new file mode 100644 index 0000000..9167aef Binary files /dev/null and b/doc/schema/plantuml/ruleengine-flow-erc3643.png differ diff --git a/doc/schema/plantuml/ruleengine-flow-erc3643.puml b/doc/schema/plantuml/ruleengine-flow-erc3643.puml new file mode 100644 index 0000000..e6803b4 --- /dev/null +++ b/doc/schema/plantuml/ruleengine-flow-erc3643.puml @@ -0,0 +1,78 @@ +@startuml +title RuleEngine with an ERC-3643 token + +skinparam shadowing false +skinparam sequenceMessageAlign direction +skinparam ParticipantPadding 12 + +actor "Token holder\n/ agent" as Holder +participant "ERC-3643 token" as Token +participant "RuleEngine\n(compliance contract)" as Engine +collections "Rule contracts\n(IRule)" as Rules + +== State-changing path == + +alt transfer or transferFrom + Holder -> Token : transfer(to, value)\ntransferFrom(from, to, value) + activate Token + Token -> Engine : transferred(from, to, value) +else mint + Holder -> Token : mint(to, value) + Token -> Engine : created(to, value) + note right of Engine : runs _transferred(address(0), to, value) +else burn + Holder -> Token : burn(from, value) + Token -> Engine : destroyed(from, value) + note right of Engine : runs _transferred(from, address(0), value) +end + +note over Token + ERC-3643 compliance callbacks carry no spender, so an + ERC-3643 token never reaches the 4-argument + transferred(spender, from, to, value) overload - + that one is declared by CMTAT's IRuleEngine. + Mint and burn use created() / destroyed() instead. +end note + +activate Engine +Engine -> Engine : onlyBoundToken + +alt msg.sender is not a bound token + Engine -->> Token : revert\nRuleEngine_ERC3643Compliance_UnauthorizedCaller + Token -->> Holder : transaction reverted +else caller is bound + loop for each rule in _rules (capped by maxRules, default 10) + Engine -> Rules : transferred(from, to, value) + activate Rules + alt rule forbids the transfer + Rules -->> Engine : revert (no return value) + Engine -->> Token : bubble up the revert + Token -->> Holder : transaction reverted + note right of Rules + Remaining rules never run. + end note + else rule allows the transfer + Rules --> Engine : return + end + deactivate Rules + end + Engine --> Token : return + Token --> Holder : operation executed +end +deactivate Engine +deactivate Token + +== Read-only path (ERC-3643, never reverts) == + +Holder -> Engine : canTransfer(from, to, value) +activate Engine +loop for each rule, until a rule reports a restriction + Engine -> Rules : detectTransferRestriction(...) + activate Rules + Rules --> Engine : ERC-1404 restriction code + deactivate Rules +end +Engine --> Holder : true when every rule allows the transfer,\nfalse otherwise +deactivate Engine + +@enduml diff --git a/doc/schema/plantuml/ruleengine-overview.png b/doc/schema/plantuml/ruleengine-overview.png new file mode 100644 index 0000000..2d3f725 Binary files /dev/null and b/doc/schema/plantuml/ruleengine-overview.png differ diff --git a/doc/schema/plantuml/ruleengine-overview.puml b/doc/schema/plantuml/ruleengine-overview.puml new file mode 100644 index 0000000..fc1d7e4 --- /dev/null +++ b/doc/schema/plantuml/ruleengine-overview.puml @@ -0,0 +1,44 @@ +@startuml +title RuleEngine - overview + +skinparam shadowing false +skinparam componentStyle rectangle +left to right direction + +actor "Token holder" as Holder + +rectangle "Token" { + component "CMTAT" as CMTAT + component "ERC-3643 token" as T3643 +} + +component "RuleEngine" as Engine + +rectangle "Rules" { + component "Rule 0\n(e.g. whitelist rule)" as R0 + component "Rule 1" as R1 + component "..." as Rdots + component "Rule n" as Rn +} + +Holder --> CMTAT : 1. transfer(to, value) +Holder --> T3643 : 1. transfer(to, value) + +CMTAT --> Engine : 2. transferred(from, to, value) +T3643 --> Engine : 2. transferred(from, to, value) + +Engine --> R0 : 3a. transferred(...) +Engine --> R1 : 3b. transferred(...) +Engine --> Rn : 3c. transferred(...) + +note bottom of Engine + The engine runs every configured rule in order and + reverts on the first rule that forbids the transfer. + + A plain transfer carries no spender and uses the + 3-argument transferred(). Other operations differ + per token standard - see the per-token sequence + diagrams for the exact entry points. +end note + +@enduml diff --git a/doc/schema/surya/surya_graph/surya_graph_ERC3643ComplianceModule.sol.png b/doc/schema/surya/surya_graph/surya_graph_ERC3643ComplianceModule.sol.png index 4c914ce..c044050 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_ERC3643ComplianceModule.sol.png and b/doc/schema/surya/surya_graph/surya_graph_ERC3643ComplianceModule.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_ERC3643TokenMock.sol.png b/doc/schema/surya/surya_graph/surya_graph_ERC3643TokenMock.sol.png new file mode 100644 index 0000000..2631d6a Binary files /dev/null and b/doc/schema/surya/surya_graph/surya_graph_ERC3643TokenMock.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_ICompliance.sol.png b/doc/schema/surya/surya_graph/surya_graph_ICompliance.sol.png index f61d7c5..aebb90e 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_ICompliance.sol.png and b/doc/schema/surya/surya_graph/surya_graph_ICompliance.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_IERC173Subset.sol.png b/doc/schema/surya/surya_graph/surya_graph_IERC173Subset.sol.png index b40d39a..71c5b6c 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_IERC173Subset.sol.png and b/doc/schema/surya/surya_graph/surya_graph_IERC173Subset.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_IERC3643Compliance.sol.png b/doc/schema/surya/surya_graph/surya_graph_IERC3643Compliance.sol.png index 2bc3765..c2d15d2 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_IERC3643Compliance.sol.png and b/doc/schema/surya/surya_graph/surya_graph_IERC3643Compliance.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_IOwnable2StepSubset.sol.png b/doc/schema/surya/surya_graph/surya_graph_IOwnable2StepSubset.sol.png index 562ad93..c5350c5 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_IOwnable2StepSubset.sol.png and b/doc/schema/surya/surya_graph/surya_graph_IOwnable2StepSubset.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_IRuleInterfaceIdHelper.sol.png b/doc/schema/surya/surya_graph/surya_graph_IRuleInterfaceIdHelper.sol.png index 9901b49..8f4d762 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_IRuleInterfaceIdHelper.sol.png and b/doc/schema/surya/surya_graph/surya_graph_IRuleInterfaceIdHelper.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_IRulesManagementModule.sol.png b/doc/schema/surya/surya_graph/surya_graph_IRulesManagementModule.sol.png index 49f6bca..7f629fa 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_IRulesManagementModule.sol.png and b/doc/schema/surya/surya_graph/surya_graph_IRulesManagementModule.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png deleted file mode 100644 index 6b9a42e..0000000 Binary files a/doc/schema/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png and /dev/null differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleConditionalTransferLightMock.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleConditionalTransferLightMock.sol.png new file mode 100644 index 0000000..e68932b Binary files /dev/null and b/doc/schema/surya/surya_graph/surya_graph_RuleConditionalTransferLightMock.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleEngine.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleEngine.sol.png index 12bc982..144de8a 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_RuleEngine.sol.png and b/doc/schema/surya/surya_graph/surya_graph_RuleEngine.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnable.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnable.sol.png index fa8580a..a0f38c6 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnable.sol.png and b/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnable.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnable2Step.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnable2Step.sol.png index 3bebd20..ba604e6 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnable2Step.sol.png and b/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnable2Step.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnableShared.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnableShared.sol.png index a4c3db9..991844c 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnableShared.sol.png and b/doc/schema/surya/surya_graph/surya_graph_RuleEngineOwnableShared.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png deleted file mode 100644 index 5ffe822..0000000 Binary files a/doc/schema/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png and /dev/null differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleMintAllowanceMock.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleMintAllowanceMock.sol.png new file mode 100644 index 0000000..8869c66 Binary files /dev/null and b/doc/schema/surya/surya_graph/surya_graph_RuleMintAllowanceMock.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleOperationRevert.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleOperationRevert.sol.png deleted file mode 100644 index b8be722..0000000 Binary files a/doc/schema/surya/surya_graph/surya_graph_RuleOperationRevert.sol.png and /dev/null differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleOperationRevertMock.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleOperationRevertMock.sol.png new file mode 100644 index 0000000..27cfc1d Binary files /dev/null and b/doc/schema/surya/surya_graph/surya_graph_RuleOperationRevertMock.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleWhitelist.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleWhitelist.sol.png deleted file mode 100644 index 7897638..0000000 Binary files a/doc/schema/surya/surya_graph/surya_graph_RuleWhitelist.sol.png and /dev/null differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RuleWhitelistMock.sol.png b/doc/schema/surya/surya_graph/surya_graph_RuleWhitelistMock.sol.png new file mode 100644 index 0000000..a13af5d Binary files /dev/null and b/doc/schema/surya/surya_graph/surya_graph_RuleWhitelistMock.sol.png differ diff --git a/doc/schema/surya/surya_graph/surya_graph_RulesManagementModule.sol.png b/doc/schema/surya/surya_graph/surya_graph_RulesManagementModule.sol.png index e7e179a..cdd8704 100644 Binary files a/doc/schema/surya/surya_graph/surya_graph_RulesManagementModule.sol.png and b/doc/schema/surya/surya_graph/surya_graph_RulesManagementModule.sol.png differ diff --git a/doc/schema/surya/surya_inheritance/surya_inheritance_ERC3643TokenMock.sol.png b/doc/schema/surya/surya_inheritance/surya_inheritance_ERC3643TokenMock.sol.png new file mode 100644 index 0000000..76d9524 Binary files /dev/null and b/doc/schema/surya/surya_inheritance/surya_inheritance_ERC3643TokenMock.sol.png differ diff --git a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png deleted file mode 100644 index 754a650..0000000 Binary files a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png and /dev/null differ diff --git a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMock.sol.png b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMock.sol.png new file mode 100644 index 0000000..4022480 Binary files /dev/null and b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMock.sol.png differ diff --git a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleMintAllowance.sol.png b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleMintAllowance.sol.png deleted file mode 100644 index 306bddc..0000000 Binary files a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleMintAllowance.sol.png and /dev/null differ diff --git a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceMock.sol.png b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceMock.sol.png new file mode 100644 index 0000000..d7d82b4 Binary files /dev/null and b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceMock.sol.png differ diff --git a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleOperationRevert.sol.png b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleOperationRevert.sol.png deleted file mode 100644 index 6eefd3d..0000000 Binary files a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleOperationRevert.sol.png and /dev/null differ diff --git a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleOperationRevertMock.sol.png b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleOperationRevertMock.sol.png new file mode 100644 index 0000000..1ff48b4 Binary files /dev/null and b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleOperationRevertMock.sol.png differ diff --git a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleWhitelist.sol.png b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleWhitelist.sol.png deleted file mode 100644 index ae8f3b6..0000000 Binary files a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleWhitelist.sol.png and /dev/null differ diff --git a/doc/schema/surya/surya_inheritance/surya_inheritance_RuleWhitelistMock.sol.png b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleWhitelistMock.sol.png new file mode 100644 index 0000000..96885bf Binary files /dev/null and b/doc/schema/surya/surya_inheritance/surya_inheritance_RuleWhitelistMock.sol.png differ diff --git a/doc/schema/surya/surya_report/surya_report_ComplianceInterfaceId.sol.md b/doc/schema/surya/surya_report/surya_report_ComplianceInterfaceId.sol.md index 268f60e..e7cb630 100644 --- a/doc/schema/surya/surya_report/surya_report_ComplianceInterfaceId.sol.md +++ b/doc/schema/surya/surya_report/surya_report_ComplianceInterfaceId.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/library/ComplianceInterfaceId.sol | 3014106773fa069f8dd8ea535ff5fb9b38e71c5f | +| ./modules/library/ComplianceInterfaceId.sol | 11d4725317d16e41444556546ee0f3c584b4d1f2 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_ERC1404InterfaceId.sol.md b/doc/schema/surya/surya_report/surya_report_ERC1404InterfaceId.sol.md index f69f5ee..91af8aa 100644 --- a/doc/schema/surya/surya_report/surya_report_ERC1404InterfaceId.sol.md +++ b/doc/schema/surya/surya_report/surya_report_ERC1404InterfaceId.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/library/ERC1404InterfaceId.sol | caeee7c9c7d32d593e490084bcf4229fd264c100 | +| ./modules/library/ERC1404InterfaceId.sol | 40ffb6676f92b7f9941e9b5a57e9c37fa94b10ff | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_ERC2771ModuleStandalone.sol.md b/doc/schema/surya/surya_report/surya_report_ERC2771ModuleStandalone.sol.md index a649e44..c80bf07 100644 --- a/doc/schema/surya/surya_report/surya_report_ERC2771ModuleStandalone.sol.md +++ b/doc/schema/surya/surya_report/surya_report_ERC2771ModuleStandalone.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/ERC2771ModuleStandalone.sol | 8b584ce82ed0281f9192ca296956221756271055 | +| ./modules/ERC2771ModuleStandalone.sol | ca20e6f6f7f28ab2e5d90be759f773456dae36fb | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceExtendedModule.sol.md b/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceExtendedModule.sol.md index ec925e2..5c670c1 100644 --- a/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceExtendedModule.sol.md +++ b/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceExtendedModule.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/ERC3643ComplianceExtendedModule.sol | 4afe34c0d43a0b0ba2e6f2ac77cdc7af3012b23d | +| ./modules/ERC3643ComplianceExtendedModule.sol | b724099045f572c919a690c34d113af3c0855ffc | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceModule.sol.md b/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceModule.sol.md index 9bbad12..9566c30 100644 --- a/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceModule.sol.md +++ b/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceModule.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/ERC3643ComplianceModule.sol | 2a66e7dd13981ffa96eb91f3063fc67a40480646 | +| ./modules/ERC3643ComplianceModule.sol | 20481b94eee7c734ad6866811f4708824db63461 | ### Contracts Description Table @@ -22,9 +22,9 @@ | └ | getTokenBound | Public ❗️ | |NO❗️ | | └ | _unbindToken | Internal 🔒 | 🛑 | | | └ | _bindToken | Internal 🔒 | 🛑 | | -| └ | _checkBoundToken | Internal 🔒 | | | | └ | _authorizeComplianceBindingChange | Internal 🔒 | 🛑 | | | └ | _onlyComplianceManager | Internal 🔒 | 🛑 | | +| └ | _checkBoundToken | Internal 🔒 | | | ### Legend diff --git a/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceModuleInvariantStorage.sol.md b/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceModuleInvariantStorage.sol.md index 1c3987e..6a66dee 100644 --- a/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceModuleInvariantStorage.sol.md +++ b/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceModuleInvariantStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/library/ERC3643ComplianceModuleInvariantStorage.sol | 1e69f760c8d85b44b03da08c72b1c22a65baf104 | +| ./modules/library/ERC3643ComplianceModuleInvariantStorage.sol | 8ba2ee79d1f96d691db5ac2bf939b8a93b715195 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceRolesStorage.sol.md b/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceRolesStorage.sol.md index 9f422fa..24fed78 100644 --- a/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceRolesStorage.sol.md +++ b/doc/schema/surya/surya_report/surya_report_ERC3643ComplianceRolesStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/library/ERC3643ComplianceRolesStorage.sol | ec28eadaae97c5a875fda91cc2af884e4bb2f395 | +| ./modules/library/ERC3643ComplianceRolesStorage.sol | 0554655baf30dbb890e6975a632b059ee909643f | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_ERC3643TokenMock.sol.md b/doc/schema/surya/surya_report/surya_report_ERC3643TokenMock.sol.md new file mode 100644 index 0000000..f5ff9e6 --- /dev/null +++ b/doc/schema/surya/surya_report/surya_report_ERC3643TokenMock.sol.md @@ -0,0 +1,30 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./mocks/ERC3643TokenMock.sol | a3db1884accf6ce3397c3f7e65e576fc46983a0b | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **ERC3643TokenMock** | Implementation | ||| +| └ | setCompliance | Public ❗️ | 🛑 |NO❗️ | +| └ | transfer | Public ❗️ | 🛑 |NO❗️ | +| └ | mint | Public ❗️ | 🛑 |NO❗️ | +| └ | burn | Public ❗️ | 🛑 |NO❗️ | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/schema/surya/surya_report/surya_report_ICompliance.sol.md b/doc/schema/surya/surya_report/surya_report_ICompliance.sol.md index d59013e..aafef79 100644 --- a/doc/schema/surya/surya_report/surya_report_ICompliance.sol.md +++ b/doc/schema/surya/surya_report/surya_report_ICompliance.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/ICompliance.sol | ad82538f6d414b1020a82240c7bc7df561b329a1 | +| ./mocks/ICompliance.sol | d60ed00e5ca3214d76ec4a3611920400a135368e | ### Contracts Description Table @@ -18,12 +18,12 @@ | **ICompliance** | Interface | ||| | └ | bindToken | External ❗️ | 🛑 |NO❗️ | | └ | unbindToken | External ❗️ | 🛑 |NO❗️ | -| └ | isTokenBound | External ❗️ | |NO❗️ | -| └ | getTokenBound | External ❗️ | |NO❗️ | -| └ | canTransfer | External ❗️ | |NO❗️ | | └ | transferred | External ❗️ | 🛑 |NO❗️ | | └ | created | External ❗️ | 🛑 |NO❗️ | | └ | destroyed | External ❗️ | 🛑 |NO❗️ | +| └ | isTokenBound | External ❗️ | |NO❗️ | +| └ | getTokenBound | External ❗️ | |NO❗️ | +| └ | canTransfer | External ❗️ | |NO❗️ | ### Legend diff --git a/doc/schema/surya/surya_report/surya_report_IERC1404Subset.sol.md b/doc/schema/surya/surya_report/surya_report_IERC1404Subset.sol.md index 93cc3cf..d532851 100644 --- a/doc/schema/surya/surya_report/surya_report_IERC1404Subset.sol.md +++ b/doc/schema/surya/surya_report/surya_report_IERC1404Subset.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/IERC1404Subset.sol | d371148102624f43a7b0f8a2a474c26b08823964 | +| ./mocks/IERC1404Subset.sol | cc27c24b8ab0b350a1d802ce9dec1b26b4a9369d | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_IERC173Subset.sol.md b/doc/schema/surya/surya_report/surya_report_IERC173Subset.sol.md index 3ae85ec..5469620 100644 --- a/doc/schema/surya/surya_report/surya_report_IERC173Subset.sol.md +++ b/doc/schema/surya/surya_report/surya_report_IERC173Subset.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/IERC173Subset.sol | eb469793804c3dcf7a6f1d29648671dd2bfad5c9 | +| ./mocks/IERC173Subset.sol | c15447b4467ff1054817bf3296f95a26283975de | ### Contracts Description Table @@ -16,8 +16,8 @@ | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| | **IERC173Subset** | Interface | ||| -| └ | owner | External ❗️ | |NO❗️ | | └ | transferOwnership | External ❗️ | 🛑 |NO❗️ | +| └ | owner | External ❗️ | |NO❗️ | ### Legend diff --git a/doc/schema/surya/surya_report/surya_report_IERC3643Compliance.sol.md b/doc/schema/surya/surya_report/surya_report_IERC3643Compliance.sol.md index 9ea177f..078200d 100644 --- a/doc/schema/surya/surya_report/surya_report_IERC3643Compliance.sol.md +++ b/doc/schema/surya/surya_report/surya_report_IERC3643Compliance.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./interfaces/IERC3643Compliance.sol | 6a206df8531bc148ae65b3061fe56bd91d64440e | +| ./interfaces/IERC3643Compliance.sol | 10b359e1a7dacc574ca3f3b1b4924a55ca25447a | ### Contracts Description Table @@ -18,10 +18,10 @@ | **IERC3643Compliance** | Interface | IERC3643ComplianceRead, IERC3643IComplianceContract ||| | └ | bindToken | External ❗️ | 🛑 |NO❗️ | | └ | unbindToken | External ❗️ | 🛑 |NO❗️ | -| └ | isTokenBound | External ❗️ | |NO❗️ | -| └ | getTokenBound | External ❗️ | |NO❗️ | | └ | created | External ❗️ | 🛑 |NO❗️ | | └ | destroyed | External ❗️ | 🛑 |NO❗️ | +| └ | isTokenBound | External ❗️ | |NO❗️ | +| └ | getTokenBound | External ❗️ | |NO❗️ | ### Legend diff --git a/doc/schema/surya/surya_report/surya_report_IERC3643ComplianceExtended.sol.md b/doc/schema/surya/surya_report/surya_report_IERC3643ComplianceExtended.sol.md index 6b66009..4be31ca 100644 --- a/doc/schema/surya/surya_report/surya_report_IERC3643ComplianceExtended.sol.md +++ b/doc/schema/surya/surya_report/surya_report_IERC3643ComplianceExtended.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./interfaces/IERC3643ComplianceExtended.sol | 6781b238b62e20fd4e44a16393df58ac13a53d0d | +| ./interfaces/IERC3643ComplianceExtended.sol | 9965d8909904d46c4574224c634fa2526b803f3e | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_IERC3643ComplianceExtendedSubset.sol.md b/doc/schema/surya/surya_report/surya_report_IERC3643ComplianceExtendedSubset.sol.md index 3c3563f..4a0494d 100644 --- a/doc/schema/surya/surya_report/surya_report_IERC3643ComplianceExtendedSubset.sol.md +++ b/doc/schema/surya/surya_report/surya_report_IERC3643ComplianceExtendedSubset.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/IERC3643ComplianceExtendedSubset.sol | 232f0855f24c038884560c17588949cdb2772173 | +| ./mocks/IERC3643ComplianceExtendedSubset.sol | db85332ba8f3327e2dff2a0f3da509cbc270be03 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_IERC7551ComplianceSubset.sol.md b/doc/schema/surya/surya_report/surya_report_IERC7551ComplianceSubset.sol.md index aa4f744..eed90b2 100644 --- a/doc/schema/surya/surya_report/surya_report_IERC7551ComplianceSubset.sol.md +++ b/doc/schema/surya/surya_report/surya_report_IERC7551ComplianceSubset.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/IERC7551ComplianceSubset.sol | ac1d94775de7a76a4da688bd97a149df399f9a5b | +| ./mocks/IERC7551ComplianceSubset.sol | 25d02e6850beac18aa40340a7e9617ff39bbe64a | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_IOwnable2StepSubset.sol.md b/doc/schema/surya/surya_report/surya_report_IOwnable2StepSubset.sol.md index 357a4b4..3257326 100644 --- a/doc/schema/surya/surya_report/surya_report_IOwnable2StepSubset.sol.md +++ b/doc/schema/surya/surya_report/surya_report_IOwnable2StepSubset.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/IOwnable2StepSubset.sol | 7b5a95bf02e01599484430bab2d3f038e1a26c12 | +| ./mocks/IOwnable2StepSubset.sol | 2c0b4a4eccfda2aa49f81039a5f5ae8f826dbe50 | ### Contracts Description Table @@ -16,8 +16,8 @@ | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| | **IOwnable2StepSubset** | Interface | ||| -| └ | pendingOwner | External ❗️ | |NO❗️ | | └ | acceptOwnership | External ❗️ | 🛑 |NO❗️ | +| └ | pendingOwner | External ❗️ | |NO❗️ | ### Legend diff --git a/doc/schema/surya/surya_report/surya_report_IRule.sol.md b/doc/schema/surya/surya_report/surya_report_IRule.sol.md index 2172a67..cd7cc48 100644 --- a/doc/schema/surya/surya_report/surya_report_IRule.sol.md +++ b/doc/schema/surya/surya_report/surya_report_IRule.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./interfaces/IRule.sol | 300bdcb28ddc2795202d0629bc4bec231d5b301c | +| ./interfaces/IRule.sol | b4e0be299f2fe29785a03b1427992c6da26e2c5d | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_IRuleInterfaceIdHelper.sol.md b/doc/schema/surya/surya_report/surya_report_IRuleInterfaceIdHelper.sol.md index 7827964..1e7b8f2 100644 --- a/doc/schema/surya/surya_report/surya_report_IRuleInterfaceIdHelper.sol.md +++ b/doc/schema/surya/surya_report/surya_report_IRuleInterfaceIdHelper.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/IRuleInterfaceIdHelper.sol | 8362e09891e2200f2fe8aff53b6cf9005bc32fcb | +| ./mocks/IRuleInterfaceIdHelper.sol | 70fc82ab0f668a5d9ce3e3696a87de3618a5a630 | ### Contracts Description Table @@ -16,12 +16,12 @@ | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| | **IRuleAllFunctions** | Interface | ||| +| └ | transferred | External ❗️ | 🛑 |NO❗️ | +| └ | transferred | External ❗️ | 🛑 |NO❗️ | | └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ | | └ | detectTransferRestriction | External ❗️ | |NO❗️ | | └ | messageForTransferRestriction | External ❗️ | |NO❗️ | | └ | detectTransferRestrictionFrom | External ❗️ | |NO❗️ | -| └ | transferred | External ❗️ | 🛑 |NO❗️ | -| └ | transferred | External ❗️ | 🛑 |NO❗️ | | └ | canTransfer | External ❗️ | |NO❗️ | | └ | canTransferFrom | External ❗️ | |NO❗️ | | └ | supportsInterface | External ❗️ | |NO❗️ | diff --git a/doc/schema/surya/surya_report/surya_report_IRulesManagementModule.sol.md b/doc/schema/surya/surya_report/surya_report_IRulesManagementModule.sol.md index 4110545..d53bcb0 100644 --- a/doc/schema/surya/surya_report/surya_report_IRulesManagementModule.sol.md +++ b/doc/schema/surya/surya_report/surya_report_IRulesManagementModule.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./interfaces/IRulesManagementModule.sol | c51bcceecca4f78a7bbe5b394786eec2416afe84 | +| ./interfaces/IRulesManagementModule.sol | 19be4f04f6f4a4d36f36f3a7b0e4304b14c8a4b6 | ### Contracts Description Table @@ -16,15 +16,15 @@ | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| | **IRulesManagementModule** | Interface | ||| -| └ | maxRules | External ❗️ | |NO❗️ | | └ | setMaxRules | External ❗️ | 🛑 |NO❗️ | | └ | setRules | External ❗️ | 🛑 |NO❗️ | -| └ | rulesCount | External ❗️ | |NO❗️ | -| └ | rule | External ❗️ | |NO❗️ | -| └ | rules | External ❗️ | |NO❗️ | | └ | clearRules | External ❗️ | 🛑 |NO❗️ | | └ | addRule | External ❗️ | 🛑 |NO❗️ | | └ | removeRule | External ❗️ | 🛑 |NO❗️ | +| └ | maxRules | External ❗️ | |NO❗️ | +| └ | rulesCount | External ❗️ | |NO❗️ | +| └ | rule | External ❗️ | |NO❗️ | +| └ | rules | External ❗️ | |NO❗️ | | └ | containsRule | External ❗️ | |NO❗️ | diff --git a/doc/schema/surya/surya_report/surya_report_Ownable2StepInterfaceId.sol.md b/doc/schema/surya/surya_report/surya_report_Ownable2StepInterfaceId.sol.md index df2234a..cae8a41 100644 --- a/doc/schema/surya/surya_report/surya_report_Ownable2StepInterfaceId.sol.md +++ b/doc/schema/surya/surya_report/surya_report_Ownable2StepInterfaceId.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/library/Ownable2StepInterfaceId.sol | 9dc1aa65e4981b2e01c444ca69f0a56555b72fe7 | +| ./modules/library/Ownable2StepInterfaceId.sol | a716d6c074ff158d21f882a5295f9c7b0bcefd38 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_OwnableInterfaceId.sol.md b/doc/schema/surya/surya_report/surya_report_OwnableInterfaceId.sol.md index ca02a3e..84725f2 100644 --- a/doc/schema/surya/surya_report/surya_report_OwnableInterfaceId.sol.md +++ b/doc/schema/surya/surya_report/surya_report_OwnableInterfaceId.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/library/OwnableInterfaceId.sol | 9dede33864286584e89920ef99b94a0b887917e7 | +| ./modules/library/OwnableInterfaceId.sol | edfa6252f8fc5c531679d92691f96b675b9cf6d8 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleAddressList.sol.md b/doc/schema/surya/surya_report/surya_report_RuleAddressList.sol.md index 5ef8290..86267ce 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleAddressList.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleAddressList.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/validation/abstract/RuleAddressList/RuleAddressList.sol | 8255280ac9b5d4063427fc5498e5e5f0b4e9f5cb | +| ./mocks/rules/validation/abstract/RuleAddressList/RuleAddressList.sol | 5799cfc64c654417d2dc429ab3ff09a947858799 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleAddressListInternal.sol.md b/doc/schema/surya/surya_report/surya_report_RuleAddressListInternal.sol.md index e537000..2d3e16d 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleAddressListInternal.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleAddressListInternal.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/validation/abstract/RuleAddressList/RuleAddressListInternal.sol | 686d80318e40f8dd20e7240effd545440bf2ff55 | +| ./mocks/rules/validation/abstract/RuleAddressList/RuleAddressListInternal.sol | 9d54010f5ae36e448576bd24a4c6e2ed71299ae9 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleAddressListInvariantStorage.sol.md b/doc/schema/surya/surya_report/surya_report_RuleAddressListInvariantStorage.sol.md index 1e90155..afb7b8a 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleAddressListInvariantStorage.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleAddressListInvariantStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleAddressListInvariantStorage.sol | 205f8fe1abfcccf0858666642068828e086fffe0 | +| ./mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleAddressListInvariantStorage.sol | 0ea8e4e57796c8381d1f9cc530720684adcf5cbb | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleBlacklistInvariantStorage.sol.md b/doc/schema/surya/surya_report/surya_report_RuleBlacklistInvariantStorage.sol.md index ba010ba..ac35c6b 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleBlacklistInvariantStorage.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleBlacklistInvariantStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleBlacklistInvariantStorage.sol | 77276f8d004722bfbd7367c45ae5b6e29d77d857 | +| ./mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleBlacklistInvariantStorage.sol | 61187193a501b8fe26b39ef326373bfe6425c907 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleCommonInvariantStorage.sol.md b/doc/schema/surya/surya_report/surya_report_RuleCommonInvariantStorage.sol.md index 63a7037..08f4f53 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleCommonInvariantStorage.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleCommonInvariantStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/validation/abstract/RuleCommonInvariantStorage.sol | bd2bf75999a7920aa4d6be476762d747efa15515 | +| ./mocks/rules/validation/abstract/RuleCommonInvariantStorage.sol | 94ed8593f8ee0f93864661ada681b83ca0a8c7e0 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleConditionalTransferLightInvariantStorage.sol.md b/doc/schema/surya/surya_report/surya_report_RuleConditionalTransferLightInvariantStorage.sol.md index e61a169..8a5bf24 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleConditionalTransferLightInvariantStorage.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleConditionalTransferLightInvariantStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol | 5aae3234538b6a6cdf9714b12bb43f18e4834bbf | +| ./mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol | 4219a1d62f201226260a2598de2f4313e5257049 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md b/doc/schema/surya/surya_report/surya_report_RuleConditionalTransferLightMock.sol.md similarity index 84% rename from doc/schema/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md rename to doc/schema/surya/surya_report/surya_report_RuleConditionalTransferLightMock.sol.md index 29adfcb..78a2328 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleConditionalTransferLightMock.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/operation/RuleConditionalTransferLight.sol | cf43c496e088f84b15bc3061721d4bd0c2547b4e | +| ./mocks/rules/operation/RuleConditionalTransferLightMock.sol | 1d6b8e8765e6de04eaa66f46b9f753f58e9a11de | ### Contracts Description Table @@ -15,17 +15,17 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleConditionalTransferLight** | Implementation | AccessControl, RuleConditionalTransferLightInvariantStorage, IRule ||| +| **RuleConditionalTransferLightMock** | Implementation | AccessControl, RuleConditionalTransferLightInvariantStorage, IRule ||| | └ | | Public ❗️ | 🛑 |NO❗️ | -| └ | supportsInterface | Public ❗️ | |NO❗️ | +| └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ | +| └ | messageForTransferRestriction | External ❗️ | |NO❗️ | | └ | approveTransfer | Public ❗️ | 🛑 | onlyRole | -| └ | approvedCount | Public ❗️ | |NO❗️ | | └ | transferred | Public ❗️ | 🛑 |NO❗️ | | └ | transferred | Public ❗️ | 🛑 |NO❗️ | +| └ | supportsInterface | Public ❗️ | |NO❗️ | +| └ | approvedCount | Public ❗️ | |NO❗️ | | └ | detectTransferRestriction | Public ❗️ | |NO❗️ | | └ | detectTransferRestrictionFrom | Public ❗️ | |NO❗️ | -| └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ | -| └ | messageForTransferRestriction | External ❗️ | |NO❗️ | | └ | canTransfer | Public ❗️ | |NO❗️ | | └ | canTransferFrom | Public ❗️ | |NO❗️ | diff --git a/doc/schema/surya/surya_report/surya_report_RuleEngine.sol.md b/doc/schema/surya/surya_report/surya_report_RuleEngine.sol.md index 3333c22..77f6e2c 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleEngine.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleEngine.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./deployment/RuleEngine.sol | a6e71718a0950a4317d07e4b3481f26d6bc00fc9 | +| ./deployment/RuleEngine.sol | 369509e7c8c3020b8898a8620a09cbeeddbc0aed | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleEngineBase.sol.md b/doc/schema/surya/surya_report/surya_report_RuleEngineBase.sol.md index 558396c..cd5e9dd 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleEngineBase.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleEngineBase.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./RuleEngineBase.sol | 980b240b1a4a9a7b888410cf13a40f47e6bacdfe | +| ./RuleEngineBase.sol | 47009acf96c5bff2b7eac02322c70fb8b3bfa8b3 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleEngineExposed.sol.md b/doc/schema/surya/surya_report/surya_report_RuleEngineExposed.sol.md index e51ec83..312249c 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleEngineExposed.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleEngineExposed.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/RuleEngineExposed.sol | 84e4f8be4361590f336a2771b0feec449d9ac074 | +| ./mocks/RuleEngineExposed.sol | cd5640f094c401bbba4c560b21f9ac9c8644f281 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleEngineInvariantStorage.sol.md b/doc/schema/surya/surya_report/surya_report_RuleEngineInvariantStorage.sol.md index 0135054..41b1b2c 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleEngineInvariantStorage.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleEngineInvariantStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/library/RuleEngineInvariantStorage.sol | 7527e9b9ec804221911c77b3f9724e7bda7a5ab8 | +| ./modules/library/RuleEngineInvariantStorage.sol | 7763d6c1b2dbfe6a1827e4c124e35e4ff3fbc874 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleEngineOwnable.sol.md b/doc/schema/surya/surya_report/surya_report_RuleEngineOwnable.sol.md index 3277dc4..9c16e0c 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleEngineOwnable.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleEngineOwnable.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./deployment/RuleEngineOwnable.sol | 0cec773602b5703708472364b6280ead7f29906a | +| ./deployment/RuleEngineOwnable.sol | 0aefac400a5da4ebf228f8b9b788cb1d820be5c9 | ### Contracts Description Table @@ -17,10 +17,10 @@ |||||| | **RuleEngineOwnable** | Implementation | RuleEngineOwnableShared, Ownable ||| | └ | | Public ❗️ | 🛑 | RuleEngineOwnableShared Ownable | +| └ | transferOwnership | Public ❗️ | 🛑 | onlyOwner | | └ | _onlyRulesManager | Internal 🔒 | 🛑 | onlyOwner | | └ | _onlyRulesLimitManager | Internal 🔒 | 🛑 | onlyOwner | | └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyOwner | -| └ | transferOwnership | Public ❗️ | 🛑 | onlyOwner | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/schema/surya/surya_report/surya_report_RuleEngineOwnable2Step.sol.md b/doc/schema/surya/surya_report/surya_report_RuleEngineOwnable2Step.sol.md index 75047c3..5499b0a 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleEngineOwnable2Step.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleEngineOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./deployment/RuleEngineOwnable2Step.sol | bc745b539127f72509a7c603272a777879fec764 | +| ./deployment/RuleEngineOwnable2Step.sol | f4789549d0abc15201d8503c2295f13de54ff336 | ### Contracts Description Table @@ -17,11 +17,11 @@ |||||| | **RuleEngineOwnable2Step** | Implementation | RuleEngineOwnableShared, Ownable2Step ||| | └ | | Public ❗️ | 🛑 | RuleEngineOwnableShared Ownable | +| └ | transferOwnership | Public ❗️ | 🛑 | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _onlyRulesManager | Internal 🔒 | 🛑 | onlyOwner | | └ | _onlyRulesLimitManager | Internal 🔒 | 🛑 | onlyOwner | | └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyOwner | -| └ | transferOwnership | Public ❗️ | 🛑 | onlyOwner | -| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/schema/surya/surya_report/surya_report_RuleEngineOwnableShared.sol.md b/doc/schema/surya/surya_report/surya_report_RuleEngineOwnableShared.sol.md index 4782ec3..7fa2914 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleEngineOwnableShared.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleEngineOwnableShared.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./RuleEngineOwnableShared.sol | d660dcbfcc5dd26f3a81a9c267997c3dd0a6d5ae | +| ./RuleEngineOwnableShared.sol | e2711fe0ef1478ab3155ae38e940b315f66d61ae | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleInterfaceId.sol.md b/doc/schema/surya/surya_report/surya_report_RuleInterfaceId.sol.md index 45cb000..49096cf 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleInterfaceId.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleInterfaceId.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/library/RuleInterfaceId.sol | 1a357586870fbe80f05d284dffbaeaeeb2f09040 | +| ./modules/library/RuleInterfaceId.sol | 9290257eac1724325368150b93b9dabc44babafd | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleMintAllowanceInvariantStorage.sol.md b/doc/schema/surya/surya_report/surya_report_RuleMintAllowanceInvariantStorage.sol.md index 9f4c140..dab2033 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleMintAllowanceInvariantStorage.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleMintAllowanceInvariantStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol | 0b68f34198783eb76a0fa123e5bb59c5c6b63cc2 | +| ./mocks/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol | 0f5aa4e3b36ede206ea44ee8aa13158e24d5a792 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleMintAllowance.sol.md b/doc/schema/surya/surya_report/surya_report_RuleMintAllowanceMock.sol.md similarity index 86% rename from doc/schema/surya/surya_report/surya_report_RuleMintAllowance.sol.md rename to doc/schema/surya/surya_report/surya_report_RuleMintAllowanceMock.sol.md index bdb2112..4773d8d 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleMintAllowance.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleMintAllowanceMock.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/operation/RuleMintAllowance.sol | e97489d69ef915fa16584bc17a7974c146258901 | +| ./mocks/rules/operation/RuleMintAllowanceMock.sol | 8624614de65b7b571307995c38ac3911f2355e2f | ### Contracts Description Table @@ -15,18 +15,18 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleMintAllowance** | Implementation | AccessControl, RuleMintAllowanceInvariantStorage, IRule ||| +| **RuleMintAllowanceMock** | Implementation | AccessControl, RuleMintAllowanceInvariantStorage, IRule ||| | └ | | Public ❗️ | 🛑 |NO❗️ | -| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | setMintAllowance | External ❗️ | 🛑 | onlyRole | +| └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ | +| └ | messageForTransferRestriction | External ❗️ | |NO❗️ | | └ | transferred | Public ❗️ | 🛑 |NO❗️ | | └ | transferred | Public ❗️ | 🛑 |NO❗️ | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | detectTransferRestriction | Public ❗️ | |NO❗️ | | └ | detectTransferRestrictionFrom | Public ❗️ | |NO❗️ | | └ | canTransfer | Public ❗️ | |NO❗️ | | └ | canTransferFrom | Public ❗️ | |NO❗️ | -| └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ | -| └ | messageForTransferRestriction | External ❗️ | |NO❗️ | ### Legend diff --git a/doc/schema/surya/surya_report/surya_report_RuleOperationRevert.sol.md b/doc/schema/surya/surya_report/surya_report_RuleOperationRevertMock.sol.md similarity index 85% rename from doc/schema/surya/surya_report/surya_report_RuleOperationRevert.sol.md rename to doc/schema/surya/surya_report/surya_report_RuleOperationRevertMock.sol.md index 580288f..7ea25b7 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleOperationRevert.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleOperationRevertMock.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/operation/RuleOperationRevert.sol | 689fae1c2576424be3fe4e39fada0425dd0c3025 | +| ./mocks/rules/operation/RuleOperationRevertMock.sol | 9ad9b2b293ec7c02b687f2d76565a22b47c90c0d | ### Contracts Description Table @@ -15,14 +15,14 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleOperationRevert** | Implementation | AccessControl, IRule, RuleCommonInvariantStorage ||| +| **RuleOperationRevertMock** | Implementation | AccessControl, IRule, RuleCommonInvariantStorage ||| +| └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ | +| └ | messageForTransferRestriction | External ❗️ | |NO❗️ | | └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | transferred | Public ❗️ | |NO❗️ | | └ | transferred | Public ❗️ | |NO❗️ | | └ | detectTransferRestriction | Public ❗️ | |NO❗️ | | └ | detectTransferRestrictionFrom | Public ❗️ | |NO❗️ | -| └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ | -| └ | messageForTransferRestriction | External ❗️ | |NO❗️ | | └ | canTransfer | Public ❗️ | |NO❗️ | | └ | canTransferFrom | Public ❗️ | |NO❗️ | diff --git a/doc/schema/surya/surya_report/surya_report_RuleWhitelistCommon.sol.md b/doc/schema/surya/surya_report/surya_report_RuleWhitelistCommon.sol.md index 845e4ad..be16b0d 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleWhitelistCommon.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleWhitelistCommon.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/validation/abstract/RuleWhitelistCommon.sol | 8dc37d2ff4d077fb5d1033ad35a76a015018d591 | +| ./mocks/rules/validation/abstract/RuleWhitelistCommon.sol | 87e2063dfdebd9dfa686ee271223b9c42983c7f0 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleWhitelistInvariantStorage.sol.md b/doc/schema/surya/surya_report/surya_report_RuleWhitelistInvariantStorage.sol.md index 05df0ed..e63935a 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleWhitelistInvariantStorage.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleWhitelistInvariantStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleWhitelistInvariantStorage.sol | 2a0b82380f56d00a84736da408cdde871e2f328e | +| ./mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleWhitelistInvariantStorage.sol | 7a738ca2d11b8167a281c97a606db4269c64dcdb | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RuleWhitelist.sol.md b/doc/schema/surya/surya_report/surya_report_RuleWhitelistMock.sol.md similarity index 85% rename from doc/schema/surya/surya_report/surya_report_RuleWhitelist.sol.md rename to doc/schema/surya/surya_report/surya_report_RuleWhitelistMock.sol.md index d45fca7..907dc83 100644 --- a/doc/schema/surya/surya_report/surya_report_RuleWhitelist.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RuleWhitelistMock.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./mocks/rules/validation/RuleWhitelist.sol | a5f2223286e30d05a0444ab0cc7cfd0a2aef7080 | +| ./mocks/rules/validation/RuleWhitelistMock.sol | 269a711aa637cfd9640a7cbb83a5d34c963c1d17 | ### Contracts Description Table @@ -15,15 +15,15 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleWhitelist** | Implementation | RuleAddressList, RuleWhitelistCommon ||| -| └ | supportsInterface | Public ❗️ | |NO❗️ | +| **RuleWhitelistMock** | Implementation | RuleAddressList, RuleWhitelistCommon ||| | └ | | Public ❗️ | 🛑 | RuleAddressList | +| └ | transferred | Public ❗️ | 🛑 |NO❗️ | +| └ | transferred | Public ❗️ | 🛑 |NO❗️ | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | canTransfer | Public ❗️ | |NO❗️ | | └ | canTransferFrom | Public ❗️ | |NO❗️ | | └ | detectTransferRestriction | Public ❗️ | |NO❗️ | | └ | detectTransferRestrictionFrom | Public ❗️ | |NO❗️ | -| └ | transferred | Public ❗️ | 🛑 |NO❗️ | -| └ | transferred | Public ❗️ | 🛑 |NO❗️ | ### Legend diff --git a/doc/schema/surya/surya_report/surya_report_RulesManagementModule.sol.md b/doc/schema/surya/surya_report/surya_report_RulesManagementModule.sol.md index 2167950..155be60 100644 --- a/doc/schema/surya/surya_report/surya_report_RulesManagementModule.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RulesManagementModule.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/RulesManagementModule.sol | 27d1c99e25a5558da4ddf1e527e5ac9f1e8f4a32 | +| ./modules/RulesManagementModule.sol | f382772b9cd31d5d61f03a4f3e6804cd4896e9c5 | ### Contracts Description Table @@ -19,20 +19,22 @@ | └ | setRules | Public ❗️ | 🛑 | onlyRulesManager | | └ | clearRules | Public ❗️ | 🛑 | onlyRulesManager | | └ | addRule | Public ❗️ | 🛑 | onlyRulesManager | -| └ | maxRules | Public ❗️ | |NO❗️ | | └ | setMaxRules | Public ❗️ | 🛑 | onlyRulesLimitManager | | └ | removeRule | Public ❗️ | 🛑 | onlyRulesManager | +| └ | maxRules | Public ❗️ | |NO❗️ | | └ | rulesCount | Public ❗️ | |NO❗️ | | └ | containsRule | Public ❗️ | |NO❗️ | | └ | rule | Public ❗️ | |NO❗️ | | └ | rules | Public ❗️ | |NO❗️ | | └ | _clearRules | Internal 🔒 | 🛑 | | +| └ | _setMaxRules | Internal 🔒 | 🛑 | | +| └ | _addRule | Internal 🔒 | 🛑 | | | └ | _removeRule | Internal 🔒 | 🛑 | | -| └ | _checkRule | Internal 🔒 | | | | └ | _transferred | Internal 🔒 | 🛑 | | | └ | _transferred | Internal 🔒 | 🛑 | | | └ | _onlyRulesManager | Internal 🔒 | 🛑 | | | └ | _onlyRulesLimitManager | Internal 🔒 | 🛑 | | +| └ | _checkRule | Internal 🔒 | | | ### Legend diff --git a/doc/schema/surya/surya_report/surya_report_RulesManagementModuleInvariantStorage.sol.md b/doc/schema/surya/surya_report/surya_report_RulesManagementModuleInvariantStorage.sol.md index ca1591c..272be3f 100644 --- a/doc/schema/surya/surya_report/surya_report_RulesManagementModuleInvariantStorage.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RulesManagementModuleInvariantStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/library/RulesManagementModuleInvariantStorage.sol | 102ddd407f94cf19d9a53660ab83d1fe23f2def1 | +| ./modules/library/RulesManagementModuleInvariantStorage.sol | 6c314c21aaecc7e2bbbaec978f622774364d65c0 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_RulesManagementModuleRolesStorage.sol.md b/doc/schema/surya/surya_report/surya_report_RulesManagementModuleRolesStorage.sol.md index ef5e8ed..d145914 100644 --- a/doc/schema/surya/surya_report/surya_report_RulesManagementModuleRolesStorage.sol.md +++ b/doc/schema/surya/surya_report/surya_report_RulesManagementModuleRolesStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/library/RulesManagementModuleRolesStorage.sol | b446550693395e6605d6fa2585a09b5020a5f3c3 | +| ./modules/library/RulesManagementModuleRolesStorage.sol | d60ca17bf8fd97d6904966f713e4613ce4d8e3c5 | ### Contracts Description Table diff --git a/doc/schema/surya/surya_report/surya_report_VersionModule.sol.md b/doc/schema/surya/surya_report/surya_report_VersionModule.sol.md index 41983c2..c649011 100644 --- a/doc/schema/surya/surya_report/surya_report_VersionModule.sol.md +++ b/doc/schema/surya/surya_report/surya_report_VersionModule.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/VersionModule.sol | 649030ee14a1c1a8466b8de78976ada94a7d9d39 | +| ./modules/VersionModule.sol | 1929b5ea873610ac4e76786ed6339e0e601a5e64 | ### Contracts Description Table diff --git a/doc/script/convert_links_for_pdf.sh b/doc/script/convert_links_for_pdf.sh index 2f4d392..48655d7 100755 --- a/doc/script/convert_links_for_pdf.sh +++ b/doc/script/convert_links_for_pdf.sh @@ -15,7 +15,8 @@ if [ -z "$1" ]; then fi GITHUB_LINK="${1%/}" # Remove trailing slash if present -INPUT_FILE="${2:-../../README.md}" +# Defaults to the full documentation (doc/README.md), not the short root overview. +INPUT_FILE="${2:-../README.md}" OUTPUT_FILE="${3:-README_UPDATE.md}" if [ ! -f "$INPUT_FILE" ]; then diff --git a/doc/script/script_surya_graph.sh b/doc/script/script_surya_graph.sh index 31a1cef..8fdbe43 100755 --- a/doc/script/script_surya_graph.sh +++ b/doc/script/script_surya_graph.sh @@ -1,18 +1,18 @@ -#/bin/bash +#!/bin/bash +# Generate a Surya call graph (PNG) for every Solidity file under src/. +# Output: docOut/surya_graph/ +set -euo pipefail + cd '../../' DIR=$(pwd) DIR_OUT=${DIR}/docOut/surya_graph if ! [ -d "$DIR_OUT" ]; then - mkdir -p ./docOut/surya_graph + mkdir -p "$DIR_OUT" fi cd './src' -DIR=$(pwd) -for i in $(find $DIR -type f); +# -print0 / read -d '' so paths containing whitespace are handled correctly +find . -type f -name '*.sol' -print0 | while IFS= read -r -d '' i; do - #echo $i filename=${i##*/} - ext=${i##*.} - if [[ $ext == 'sol' ]]; then - npx surya graph $i | dot -Tpng > ../docOut/surya_graph/surya_graph_$filename.png; - fi -done; \ No newline at end of file + npx surya graph "$i" | dot -Tpng > "${DIR_OUT}/surya_graph_${filename}.png"; +done; diff --git a/doc/script/script_surya_inheritance.sh b/doc/script/script_surya_inheritance.sh index e4b8ca7..a043f49 100755 --- a/doc/script/script_surya_inheritance.sh +++ b/doc/script/script_surya_inheritance.sh @@ -1,17 +1,18 @@ -#/bin/bash +#!/bin/bash +# Generate a Surya inheritance graph (PNG) for every Solidity file under src/. +# Output: docOut/surya_inheritance/ +set -euo pipefail + cd '../../' DIR=$(pwd) -DIR_OUT=${DIR}/docOut/inheritance +DIR_OUT=${DIR}/docOut/surya_inheritance if ! [ -d "$DIR_OUT" ]; then - mkdir -p ./docOut/surya_inheritance + mkdir -p "$DIR_OUT" fi cd './src' -DIR=$(pwd) -for i in $(find $dir -type f); +# -print0 / read -d '' so paths containing whitespace are handled correctly +find . -type f -name '*.sol' -print0 | while IFS= read -r -d '' i; do filename=${i##*/} - ext=${i##*.} - if [[ $ext == 'sol' ]]; then - npx surya inheritance $i | dot -Tpng > ../docOut/surya_inheritance/surya_inheritance_$filename.png; - fi -done; \ No newline at end of file + npx surya inheritance "$i" | dot -Tpng > "${DIR_OUT}/surya_inheritance_${filename}.png"; +done; diff --git a/doc/script/script_surya_report.sh b/doc/script/script_surya_report.sh index 84769c9..4570c6f 100755 --- a/doc/script/script_surya_report.sh +++ b/doc/script/script_surya_report.sh @@ -1,17 +1,18 @@ -#/bin/bash +#!/bin/bash +# Generate a Surya markdown report for every Solidity file under src/. +# Output: docOut/surya_report/ +set -euo pipefail + cd '../../' DIR=$(pwd) DIR_OUT=${DIR}/docOut/surya_report if ! [ -d "$DIR_OUT" ]; then - mkdir ./docOut/surya_report + mkdir -p "$DIR_OUT" fi cd './src' -DIR=$(pwd) -for i in $(find $dir -type f); +# -print0 / read -d '' so paths containing whitespace are handled correctly +find . -type f -name '*.sol' -print0 | while IFS= read -r -d '' i; do filename=${i##*/} - ext=${i##*.} - if [[ $ext == 'sol' ]]; then - npx surya mdreport ../docOut/surya_report/surya_report_$filename.md $i; - fi -done; \ No newline at end of file + npx surya mdreport "${DIR_OUT}/surya_report_${filename}.md" "$i"; +done; diff --git a/doc/security/audits/AUDIT_OVERVIEW.md b/doc/security/audits/AUDIT_OVERVIEW.md new file mode 100644 index 0000000..ad02f28 --- /dev/null +++ b/doc/security/audits/AUDIT_OVERVIEW.md @@ -0,0 +1,90 @@ +# Audit and analysis overview + +Index of every security and quality analysis performed on the RuleEngine, with the outcome of each. + +This is an *overview of analyses*. For vulnerability reporting, see +[SECURITY.md](https://github.com/CMTA/CMTAT/blob/master/SECURITY.md) in the CMTAT main repository. + +| | | +|---|---| +| **Current version** | v3.0.0-rc5 | +| **Compiler** | solc 0.8.36, EVM Prague, optimizer on (200 runs) | +| **Audited?** | **No.** v1.0.2 was audited by ABDK in March 2022; the 3.0.0 line has not been audited. | + +## Scope + +In scope: everything under `src/` except `src/mocks/`. + +`src/mocks/` holds reference rules and test doubles — `RuleWhitelistMock`, `RuleMintAllowanceMock`, +`RuleConditionalTransferLightMock`, `RuleOperationRevertMock`, `ERC3643TokenMock` and their abstract bases. +They exist for tests, scripts and examples. Production rules are maintained separately in +[CMTA/Rules](https://github.com/CMTA/Rules). Static-analysis runs exclude them by default; findings that +concern a mock are labelled as such and do not apply to production deployments. + +## Analyses + +| Analysis | Version | Report | Assessment | +|---|---|---|---| +| Slither | v3.0.0-rc5 | [slither-report.md](./tools/v3.0.0-rc5/slither-report.md) | [feedback](./tools/v3.0.0-rc5/slither-report-feedback.md) | +| Aderyn | v3.0.0-rc5 | [aderyn-report.md](./tools/v3.0.0-rc5/aderyn-report.md) | [feedback](./tools/v3.0.0-rc5/aderyn-report-feedback.md) | +| Code-quality review | v3.0.0-rc5 | [CLAUDE_ANALYSIS.md](./tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md) | — | +| Script review | v3.0.0-rc5 | [CLAUDE_ANALYSIS_SCRIPT.md](./tools/v3.0.0-rc5/CLAUDE_ANALYSIS_SCRIPT.md) | — | +| Slither | v3.0.0-rc4 | [slither-report.md](./tools/v3.0.0-rc4/slither-report.md) | [feedback](./tools/v3.0.0-rc4/slither-report-feedback.md) | +| Aderyn | v3.0.0-rc4 | [aderyn-report.md](./tools/v3.0.0-rc4/aderyn-report.md) | [feedback](./tools/v3.0.0-rc4/aderyn-report-feedback.md) | +| Nethermind AuditAgent | v3.0.0-rc1 | [report](./tools/nethermind-audit-agent/v3.0.0-rc1/audit_agent_report_1_v3.0.0-rc1.pdf) | [feedback](./tools/nethermind-audit-agent/v3.0.0-rc1/audit_agent_report_1_v3.0.0-rc1-feedback.md) | +| ABDK (external audit) | v1.0.1 -> v1.0.2 | [ABDK report (CMTAT repo)](https://github.com/CMTA/CMTAT/blob/master/doc/audits/ABDK_CMTA_CMTATRuleEngine_v_1_0/ABDK_CMTA_CMTATRuleEngine_v_1_0.pdf) | — | + +## Static analysis results — v3.0.0-rc5 + +| Tool | High | Medium | Low | Info | Relevant to fix? | +|---|---|---|---|---|---| +| Slither 0.11.5 | 0 | 0 | 10 | 2 | **No** | +| Aderyn 0.6.5 | 0 | — | 8 (76 instances) | — | **No** | + +Every finding is by design, cosmetic, or a verified false positive. Highlights: + +- **`calls-loop` (Slither, 10)** — the engine iterating its rule set is the product. Bounded on-chain by + `maxRules` (default 10). +- **`unindexed-event-address` (Slither, 2)** — `TokenBound` / `TokenUnbound` match the ERC-3643 reference + interface, which declares them unindexed. Conformance, not an oversight. +- **`L-8 Unchecked Return` (Aderyn, 1)** — `_grantRole` in a constructor cannot return `false`. False positive. + +No change in counts from v3.0.0-rc4 for either tool. + +## Substantive findings that were fixed + +From the code-quality and script reviews of v3.0.0-rc5. None was exploitable; all were correctness, clarity or +usability defects. + +| ID | Finding | Where | +|---|---|---| +| B-1 | `contains()` + `add()` double lookup on the bound-token set (269 gas, measured) | [CLAUDE_ANALYSIS.md](./tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md) | +| C-1 | The initial `maxRules` was never emitted, so an event-only indexer could not reconstruct it | same | +| E-1 | `_bindToken` / `_unbindToken` were not `virtual`, unlike every sibling internal in the file | same | +| G-1 / H-1 | `canTransfer` fails open for spender-dependent rules; now documented at both rule and engine level | same | +| S-1 | `RuleEngineScript` never bound the token — every transfer in the resulting deployment reverted | [CLAUDE_ANALYSIS_SCRIPT.md](./tools/v3.0.0-rc5/CLAUDE_ANALYSIS_SCRIPT.md) | +| S-2 | Low-level `.call` to `CMTAT_ADDRESS` returned success against an EOA, silently misconfiguring | same | +| S-6..S-11 | Broken shebangs, undefined `$dir`, missing `mkdir -p`, no `set -euo pipefail` in the surya scripts | same | + +Known limitations that were **documented rather than changed** are listed in the technical guides: +[RuleEngine-with-CMTAT.md](../../technical/RuleEngine-with-CMTAT.md) §4 and +[RuleEngine-with-ERC3643.md](../../technical/RuleEngine-with-ERC3643.md) §4. + +## Reproducing the static analysis + +```bash +# Slither — mocks excluded +slither . --checklist --filter-paths "openzeppelin-contracts|test|CMTAT|forge-std|mocks" \ + > doc/security/audits/tools/vX.Y.Z/slither-report.md + +# Aderyn — mocks excluded. Keep --output inside the repository: +# Aderyn computes source links relative to it, so an external path bakes absolute paths into the report. +aderyn -x mocks --output doc/security/audits/tools/vX.Y.Z/aderyn-report.md +``` + +After either run, verify the scope actually held before trusting the counts: + +```bash +grep -c 'lib/\|node_modules/' # expect 0 — a filter entry that matches nothing fails open +grep -c 'src/mocks/' # expect 0 when mocks are excluded +``` diff --git a/doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md b/doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md new file mode 100644 index 0000000..e66527c --- /dev/null +++ b/doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md @@ -0,0 +1,466 @@ +# RuleEngine — Code Quality Review + +| | | +|---|---| +| **Scope** | `src/` (46 Solidity files) and `script/` (2 files) | +| **Base commit** | `50165c7` | +| **Compiler** | solc 0.8.36, `evm_version = prague`, optimizer on, 200 runs | +| **Date** | 2026-08-13 | +| **Produced with** | Claude Code | + +## This is not a security audit + +Nothing in this report is a vulnerability. No finding below lets an unauthorized party move value, bypass a +transfer restriction, or brick a contract. The one finding with real integration consequence (**H-1**) is a +divergence between a *view* and the *enforcement* path: the view is more permissive than reality, so an +integrator can be told a mint is allowed when it will revert. The enforcement path itself is correct and +still rejects — no restriction is bypassed. + +Every gas number below was measured with a benchmark harness, not derived from opcode costs. Each variant was +placed in its own contract with a single identically-named function so selector-dispatch depth could not skew +the comparison, and every measurement was taken after an identical warm-up. All benchmark files were deleted +after measurement; the test count reconciles exactly (322 before → 325 after, the three added being the C-1 +regression tests). + +## Disposition summary + +| ID | Finding | Outcome | +|----|---------|---------| +| A-1 | `addressIsListedBatch` takes `memory`, never called internally | ✅ fixed — `calldata`, 587 gas @ 10 addrs | +| A-2 | Loop increment form / `unchecked` | ⬜ left as is — already correct, see below | +| A-3 | Unbounded iteration over caller-supplied arrays | ⬜ left as is — admin-gated | +| B-1 | `contains()` then `add()`/`remove()` double lookup | ✅ fixed — 269 gas measured | +| B-2 | `_checkRule()` then `_rules.add()` double lookup | ⬜ left as is — diagnostic structure | +| C-1 | Initial `maxRules` never emitted | ✅ fixed — emitted at construction, 3 regression tests | +| C-2 | Batch self-binding approval emits input, not per-token effect | ⚠️ decide — API-visible, see below | +| D-1 | Ownable variants duplicate the ERC-2771 context trio | ⬜ left as is — compiler-mandated, proven | +| E-1 | `_bindToken` / `_unbindToken` not `virtual` | ✅ fixed | +| E-2 | `_supportsRuleEngineBaseInterface` not `virtual` | ✅ fixed | +| E-3 | 10 mock internals not `virtual` | ✅ fixed | +| F-1 | ERC-165 flattened interface-ID computation | ⬜ no finding — verified correct | +| F-2 | `RuleWhitelistMock` treats `address(0)` as a participant, blocking ERC-3643 mint | ⬜ left as is — pinned by test, operational note | +| G-1 | `canTransfer` docs omit the fail-open case | ✅ fixed — warning added | +| H-1 | View approves a mint that enforcement rejects | ✅ documented — behaviour left, see below | + +**Counted: 15 rows — 7 fixed, 6 left as is, 1 no-finding, 1 open decision.** + +## Outstanding + +| ID | Item | Why it is still open | +|----|------|----------------------| +| C-2 | `setTokenSelfBindingApprovalBatch` emits only a batch event | Fixing it changes the emitted event stream, which is API-visible to indexers. Needs a product call, not a code call. | +| H-1 | The view/enforcement divergence itself | Inherent to the ERC-1404 3-argument signature, which carries no `spender`. Documented rather than "fixed" — see the finding for why a code fix would be worse. | + +--- + +## A. Loops and iteration + +### A-1. `addressIsListedBatch` takes `memory` but is never called internally — fixed + +`RuleAddressList.sol`, `addressIsListedBatch`: + +```solidity +function addressIsListedBatch(address[] memory _targetAddresses) public view returns (bool[] memory) +``` + +A repo-wide grep found no internal caller — the only callers are external (tests). `memory` therefore forces +a needless calldata→memory copy on every call. + +**Measured**, two contracts each exposing a single `probe` function, 10 addresses, identical warm-up: + +| Variant | Gas | +|---|---| +| `memory` | 35,421 | +| `calldata` | 34,834 | +| **Delta** | **587** (~59/address) | + +**Verdict: implement.** Changed to `calldata` (and `virtual`, per E-3). Scales with array length, so the +saving grows for the batch sizes this function exists to serve. + +### A-2. Increment form and `unchecked` — no change, and deliberately so + +All 12 loops in `src/` already use `++i`, and there is **no `unchecked` block anywhere in the codebase**. + +On solc 0.8.36 this is correct and should stay. Since **0.8.22** the compiler elides the overflow check on a +bounded loop counter automatically, so wrapping `++i` in `unchecked` buys nothing on this pragma. A review +that recommended `unchecked { ++i }` here would be recommending noise. + +**Verdict: leave.** Recorded explicitly so a future reviewer does not "fix" it. + +### A-3. Unbounded iteration over caller-supplied arrays + +`bindTokens`, `unbindTokens`, `setTokenSelfBindingApprovalBatch` (`ERC3643ComplianceExtendedModule.sol`), +`addAddressesToTheList` / `removeAddressesFromTheList` (`RuleAddressListInternal.sol`) all iterate a +caller-supplied array with no length cap. + +All are gated on a privileged role (`onlyComplianceManager`, `ADDRESS_LIST_ADD_ROLE`, +`ADDRESS_LIST_REMOVE_ROLE`). The only party who can pass an oversized array is the operator, and the only +consequence is their own transaction running out of gas — no griefing surface, no state corruption. + +**Verdict: leave.** The rule set itself *is* capped (`maxRules`, default 10), which is where the bound +matters, because that loop runs on every transfer. + +--- + +## B. Storage reads + +### B-1. `contains()` immediately followed by `add()` / `remove()` — fixed + +`ERC3643ComplianceModule.sol`, before: + +```solidity +function _bindToken(address token) internal { + require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); + require(!_boundTokens.contains(token), RuleEngine_ERC3643Compliance_TokenAlreadyBound()); + // Should never revert because we check if the token address is already set before + require(_boundTokens.add(token), RuleEngine_ERC3643Compliance_OperationNotSuccessful()); + emit TokenBound(token); +} +``` + +`EnumerableSet.add` already performs the membership test internally and returns `false` when the value is +present, so `contains()` reads the same `_positions` slot twice. The source comment +("Should never revert because we check ... before") documents that the second `require` is unreachable. + +Rewritten to use the mutation's return value **while keeping the meaningful diagnostic**: + +```solidity +function _bindToken(address token) internal virtual { + require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); + // add() returns false when the token is already bound, so a separate + // contains() lookup is unnecessary. + require(_boundTokens.add(token), RuleEngine_ERC3643Compliance_TokenAlreadyBound()); + emit TokenBound(token); +} +``` + +`_unbindToken` received the same treatment with `RuleEngine_ERC3643Compliance_TokenNotBound`. + +**Measured**, two contracts each exposing a single `bind` function, identical warm-up: + +| Variant | Gas | +|---|---| +| `contains()` + `add()` | 55,198 | +| `add()` only | 54,929 | +| **Delta** | **269** | + +Note the size of that number. The `contains()` call was the **first** touch of the slot and therefore paid +the *cold* price; removing it makes `add()` the cold access. What is actually saved is the **warm** SLOAD +plus call overhead — ~269 gas, not the ~2,100 a naive cold-access assumption would predict. + +`RuleEngine_ERC3643Compliance_OperationNotSuccessful` became unreachable and was removed. It was referenced +nowhere else in `src/` or `test/`; an error that can never be raised is worse than no error, though note this +is an ABI change for anyone decoding it. + +**Verdict: implement.** Behaviour-preserving — the same two errors are raised in the same conditions. +Existing tests guard both paths (`TokenAlreadyBound` and `TokenNotBound` are asserted across all three +deployable variants in `ERC3643Compliance.t.sol`), and all 322 pre-existing tests still pass. + +### B-2. `_checkRule()` then `_rules.add()` — left as is + +`RulesManagementModule.sol` has the same shape in `addRule` / `setRules`: `_checkRule` performs a +`contains()`, then `add()` repeats it, guarded by a `require` documented as unreachable. + +This is **not** the same case. `_checkRule` is `virtual` and `RuleEngineBase` overrides it to add the ERC-165 +interface validation, so the check is an extension point, not just a lookup. Collapsing it into `add()`'s +return value would either lose the ERC-165 validation or force it to run after insertion. + +**Verdict: leave.** The upper bound on the saving is the same ~269 gas measured in B-1, per rule added — paid +only on administrative calls, never on the transfer path. Not worth dismantling an override point for. + +--- + +## C. Events + +### C-1. The initial `maxRules` is never emitted — fixed + +`RulesManagementModule.sol` initialises the cap inline: + +```solidity +uint256 internal _maxRules = DEFAULT_MAX_RULES; // 10 +``` + +`SetMaxRules` is emitted **only** by `setMaxRules`. An indexer reconstructing engine configuration purely +from events therefore sees no value at all until an admin first changes the cap — it cannot distinguish +"cap is 10" from "cap unknown", and a freshly deployed engine emits nothing. + +Fixed by emitting the initial value in both constructors (`RuleEngine`, and `RuleEngineOwnableShared` which +serves both ownable variants): + +```solidity +// Emit the initial cap so the event log alone is enough to reconstruct maxRules. +emit SetMaxRules(_maxRules); +``` + +**Verdict: implement.** Three regression tests added in `RuleEngineMaxRulesEvent.t.sol`, one per deployable +variant. Per the method: the fix was reverted and all three tests were confirmed to **fail** +(`expected an emit, but no logs were emitted afterwards`) before being restored — they are guards, not +guesses. + +### C-2. Batch self-binding approval reports its input, not its effect — open decision + +`ERC3643ComplianceExtendedModule.sol`: + +```solidity +function setTokenSelfBindingApproval(address token, bool approved) ... { + ... + emit TokenSelfBindingApprovalSet(token, approved); // per token +} + +function setTokenSelfBindingApprovalBatch(address[] calldata tokens, bool approved) ... { + for (...) { _tokenSelfBindingApproval[token] = approved; } + emit TokenSelfBindingApprovalBatchSet(tokens, approved); // echoes the input array +} +``` + +The same state change produces different events depending on which entrypoint the operator used. A consumer +subscribed to `TokenSelfBindingApprovalSet` — the obvious choice, since it is the per-token event — silently +misses every batch update. + +The inconsistency is sharpest **inside this same contract**: `bindTokens` and `unbindTokens` *do* emit a +per-token event for each element (via `_bindToken` / `_unbindToken`). Batch binding is consistent; batch +approval is not. + +The batch event also reports the input array rather than the effect: it does not distinguish tokens whose +approval actually changed from those already at the requested value. + +**Verdict: decide.** Emitting `TokenSelfBindingApprovalSet` per token would make the event stream uniform and +match the sibling batch functions, at the cost of N events' gas. That is an API-visible change to the emitted +stream, so it is a product decision rather than a mechanical fix. Left unchanged pending that call. + +--- + +## D. Duplication + +### D-1. The ownable variants duplicate the ERC-2771 context trio — left as is, and here is the proof + +`RuleEngineOwnable.sol` and `RuleEngineOwnable2Step.sol` contain byte-identical bodies for `_msgSender`, +`_msgData` and `_contextSuffixLength`, each a one-line delegation to `RuleEngineOwnableShared`. The three +access-control hooks (`_onlyRulesManager`, `_onlyRulesLimitManager`, `_onlyComplianceManager`) are likewise +identical (`onlyOwner {}`). That is 6 duplicated **code** lines per contract, excluding NatSpec. + +The obvious proposal is to hoist the trio into `RuleEngineOwnableShared`, which already defines all three. +**Before proposing that, I tested whether the duplication was avoidable** by deleting the three overrides from +`RuleEngineOwnable` and compiling: + +``` +Error (6480): Derived contract must override function "_msgSender". +Two or more base classes define function with same name and parameter types. + --> src/deployment/RuleEngineOwnable.sol:12:1 +Note: Definition in "Context": lib/openzeppelin-contracts/contracts/utils/Context.sol:21:5 +``` + +Identical errors for `_msgData` and `_contextSuffixLength`. Because `RuleEngineOwnable` inherits both +`RuleEngineOwnableShared` and `Ownable` (→ `Context`), and both branches supply these functions, C3 +linearization requires the most-derived contract to disambiguate **explicitly**. The duplication is mandated +by the compiler, not an oversight. + +The access-control hooks cannot be hoisted either: `RuleEngineOwnableShared` does not inherit `Ownable` +(that is precisely the choice it defers to its children), so it cannot apply the `onlyOwner` modifier. + +**Verdict: leave.** File restored to its original state; recorded here with the exact compiler error so this +is not re-opened. + +--- + +## E. `virtual` / override convention + +The project's own guide (`CLAUDE.md` / `AGENTS.md`, "Solidity Style") states: *"All `internal` functions must +be marked `virtual`, so inheriting contracts can override them."* This section is measured against that rule, +not an external preference. + +**Cost check, measured rather than asserted:** a plain `internal` call and a `virtual internal` call were +benchmarked in separate contracts — 10,374 vs 10,363 gas. The 11-gas delta *favours* `virtual` and is within +noise, confirming that `virtual` on an internal function is resolved statically and is free. + +### E-1. `_bindToken` / `_unbindToken` were not `virtual` — fixed + +`ERC3643ComplianceModule.sol`. These are the core compliance state mutators — every bind and unbind, batch or +single, self-binding or manager-driven, funnels through them. + +The evidence is the **inconsistency inside the same file**: every other internal there +(`_checkBoundToken`, `_authorizeComplianceBindingChange`, `_onlyComplianceManager`) is `virtual`. These two +were the outliers, so a deployment variant could override *authorization* for binding but not the binding +itself. + +**Verdict: implement.** Highest-consequence item in this section. + +### E-2. `_supportsRuleEngineBaseInterface` was not `virtual` — fixed + +`RuleEngineBase.sol`. Same inconsistency argument: `_detectTransferRestriction`, +`_detectTransferRestrictionFrom`, `_messageForTransferRestriction` and `_checkRule` in that file are all +`virtual`; this shared ERC-165 helper was not. A variant wanting to extend the base interface set had to +override the public `supportsInterface` instead of the helper built for it. + +**Verdict: implement.** + +### E-3. Ten mock internals were not `virtual` — fixed + +`RuleAddressListInternal.sol` (6: `_addAddressesToThelist`, `_removeAddressesFromThelist`, +`_addAddressToThelist`, `_removeAddressFromThelist`, `_numberListedAddress`, `_addressIsListed`) and +`RuleAddressList.sol` (the ERC-2771 context trio, which *is* `virtual` in the production engines — another +sibling inconsistency). + +These are reference implementations that integrators copy, so the convention matters more here than the +consequence does. + +**Verdict: implement.** After this pass, `grep` for `internal` functions lacking `virtual` across `src/` +returns nothing. + +--- + +## F. ERC / specification conformance + +### F-1. The flattened ERC-165 interface ID is computed correctly — no finding + +The classic ERC-165 bug is assuming `type(IFoo).interfaceId` covers inherited selectors; it covers only those +declared directly on `IFoo`. `IRule` extends `IRuleEngineERC1404`, so the naive computation would be wrong. + +The project already handles this correctly and deliberately: `IRuleInterfaceIdHelper.sol` defines a flattened +`IRuleAllFunctions` interface enumerating the whole hierarchy, `RuleInterfaceId.IRULE_INTERFACE_ID` is the +XOR over that flattened set, and `IRuleInterfaceId.t.sol` asserts the constant matches +(`testConstantMatchesAllFunctionsXOR`, plus a manual hand-XOR cross-check in `computeManualXOR`). All pass. + +**Verdict: no change.** Recorded as verified rather than omitted, so the next reviewer does not re-derive it. + +### F-2. `RuleWhitelistMock` treats `address(0)` as a participant, so an ERC-3643 token cannot mint + +**Scope: this is a mock.** `RuleWhitelistMock` lives in `src/mocks/rules/validation/` and is a reference +implementation for tests, scripts and examples — not a production rule. The production `RuleWhitelist` is +maintained separately in [CMTA/Rules](https://github.com/CMTA/Rules) and is not covered by this review. The +finding is recorded because the mock is what this repository's own scripts and integration tests deploy, and +because reference code is what integrators copy. + +Found while writing the ERC-3643 integration tests — this path had no prior test coverage. + +`RuleWhitelistMock.detectTransferRestriction` checks both endpoints without exempting the zero-address +sentinel: + +```solidity +if (!addressIsListed(from)) { + return CODE_ADDRESS_FROM_NOT_WHITELISTED; +} else if (!addressIsListed(to)) { + return CODE_ADDRESS_TO_NOT_WHITELISTED; +} +``` + +The ERC-3643 reference token pre-checks a mint as `canTransfer(address(0), _to, _amount)` +(`Token.sol:456` in `lib/ERC-3643`, tag 4.1.3). Since `address(0)` is not a listed holder, the whitelist +returns `CODE_ADDRESS_FROM_NOT_WHITELISTED` and **the mint is refused**. An issuer who whitelists only real +holders — the natural reading of "whitelist" — cannot mint at all. + +The asymmetry is what makes this notable: `detectTransferRestrictionFrom` *was* deliberately taught about the +sentinel (rc4 skipped the **spender** check for mint and burn), but the `from` / `to` checks on the 3-argument +path were not given the same treatment. + +**Verdict: leave, and pin it with a test.** Whitelisting `address(0)` is a legitimate way for an issuer to +express "minting is permitted", and changing the rule to auto-exempt the sentinel would silently widen every +existing deployment's whitelist semantics — a larger behavioural change than this review should make +unilaterally. `testMintIsBlockedWhenZeroAddressNotListed` now documents and pins the requirement, and the +integration `setUp` shows the working configuration. Note that `RuleWhitelistMock` lives in `src/mocks/` and is a +reference rule, not a production one. + +--- + +## G. Code / documentation mismatch + +### G-1. `canTransfer` documentation omitted the fail-open case — fixed + +`doc/README.md` documented `canTransfer` as: + +> Returns true if the transfer is valid, and false otherwise. +> Does not check balances or access rights (Access Control). + +The stated carve-outs are balances and access control. Nothing warned that a **spender-dependent rule cannot +be evaluated at all** on this signature — which is the case demonstrated in H-1, where `canTransfer` returns +`true` for a mint that reverts. + +**Verdict: implement.** A warning block was added to that section, and the same caveat was added to +`RuleMintAllowanceMock.detectTransferRestriction`'s NatSpec using the project's plain-word `WARNING:` marker +(no emoji, per the project's Solidity comment convention). + +--- + +## H. Weird behaviour — correct, but at odds with the purpose + +### H-1. A view approves a mint the enforcement path rejects + +This is the highest-value finding and the reason to read this report. + +`RuleMintAllowanceMock` keys its allowance by **spender** (the minter). The ERC-1404 3-argument +`detectTransferRestriction(from, to, value)` carries no spender, so the rule cannot evaluate a mint on that +path and answers "no restriction": + +```solidity +function detectTransferRestriction(address, address, uint256) public pure override returns (uint8) { + return uint8(REJECTED_CODE_BASE.TRANSFER_OK); +} +``` + +**Traced, not assumed.** `RuleEngineBase._detectTransferRestriction` aggregates rule answers and returns the +first non-zero code; a rule answering `0` contributes nothing. `canTransfer` is built on the same aggregation. +So the hardcoded "everything is fine" propagates all the way to the two view functions an integrator actually +calls on the **engine**, not just on the rule. + +Verified with a harness (since deleted), against a `RuleEngine` holding one `RuleMintAllowanceMock` with a minter +allowance of 100, querying a mint of 500: + +| Call | Result | +|---|---| +| `engine.detectTransferRestriction(address(0), to, 500)` | `0` — no restriction | +| `engine.canTransfer(address(0), to, 500)` | `true` — allowed | +| `engine.detectTransferRestrictionFrom(minter, address(0), to, 500)` | `81` — insufficient allowance | +| `engine.canTransferFrom(minter, address(0), to, 500)` | `false` — forbidden | +| `rule.transferred(minter, address(0), to, 500)` | **reverts** | + +So the 3-argument view says yes, the 4-argument view says no, and reality says no. The system **fails open on +the view path while failing closed on the enforcement path** — the opposite of the direction a compliance +engine should lean. + +**Why this is not a vulnerability:** enforcement is unaffected. The mint still reverts. The damage is to +integrators who pre-flight with `canTransfer` and get a wrong answer — a bad UX and a misleading API, not a +bypass. + +**Why not "fix" it in code.** The rule cannot invent a spender it was not given. The alternatives are all +worse: + +- Returning a restriction code unconditionally would break every legitimate non-mint transfer through this rule. +- Guessing `msg.sender` as the spender would be wrong in exactly the delegated case the field exists for. +- Removing the 3-argument implementation is not possible — `IRule` requires it. + +The real constraint is the ERC-1404 signature, which predates the operator-aware model. The honest response is +to make the limitation loud. + +**Verdict: document, keep the behaviour.** Warnings added at both levels — the rule's NatSpec (so anyone +copying this reference rule sees it) and `doc/README.md`'s `canTransfer` section (so integrators see it). The +guidance is explicit: to pre-check an operation that has an operator, use `canTransferFrom` / +`detectTransferRestrictionFrom`. + +This finding applies to `src/mocks/`, which the project documents as reference implementations rather than +production rules. That limits the blast radius but arguably raises the documentation stakes: reference code is +what integrators copy. + +--- + +## What was checked but not run + +Stated explicitly, per the "distinguish checked from assumed" rule: + +- **`script/`** was read and inventoried (2 files, both Foundry deployment scripts). It contains no loops, no + storage patterns and no events of its own — the only `memory` usages are CMTAT constructor-attribute structs + that must be `memory`. No findings, and nothing there needed benchmarking. +- **ERC-3643 / ERC-7551 semantic conformance beyond the interface-ID check (F-1)** was not exhaustively + re-derived against the specification text. F-1 covers the interface-ID computation specifically. The + project's own compatibility notes flag ERC-7551 as draft and deliberately implements a subset. +- **Storage-layout diffing** was not required: no finding in this pass moved or added a state variable. The + `virtual` additions and the `require` collapse are code-only changes. + +--- + +## Verification + +- `forge build --force` — successful +- `forge test` — **325 passed, 0 failed** (322 pre-existing + 3 new C-1 regressions) +- `forge fmt` — applied +- Project style checker — 0 violations across all 46 files in `src/` +- All temporary benchmark files deleted; test count reconciles exactly diff --git a/doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS_SCRIPT.md b/doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS_SCRIPT.md new file mode 100644 index 0000000..17ac7cb --- /dev/null +++ b/doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS_SCRIPT.md @@ -0,0 +1,361 @@ +# Script review — findings and fixes + +| | | +|---|---| +| **Scope** | `script/` (2 Foundry scripts), `doc/script/` (4 shell scripts), `package.json` npm scripts | +| **Base commit** | `50165c7` + working tree | +| **Date** | 2026-08-13 | +| **Produced with** | Claude Code | + +Findings are grouped by script family and given stable IDs (`S-n`). Each states whether it was **verified by +running it** or **reasoned from the source** — the distinction matters, and both appear below. + +One finding, **S-1**, was a functional defect: `RuleEngineScript.s.sol` produced a deployment in which no +transfer could ever succeed. It was not a security issue (it failed closed, it opened nothing), but anyone +using that script as a deployment recipe got a broken system. It is now fixed and pinned by a test. + +**All 12 findings have been fixed.** The sections below keep each finding as originally written, with the +applied fix recorded under it. + +## Disposition summary + +| ID | Script | Finding | Outcome | +|----|--------|---------|---------| +| S-1 | `RuleEngineScript.s.sol` | The CMTAT is never bound to the engine — all transfers revert | ✅ fixed — token passed to the constructor | +| S-2 | `RuleEngineScript.s.sol` | Low-level `.call` + bare `require(success)` succeeds against an EOA | ✅ fixed — typed call | +| S-3 | `RuleEngineScript.s.sol` | Ships an empty whitelist, so nothing can transfer or mint even once bound | ✅ fixed — deployer + `address(0)` listed | +| S-4 | `test/script/RuleEngineScript.t.sol` | Asserts only that `run()` does not revert | ✅ fixed — 5 assertions + a live mint | +| S-5 | `CMTATWithRuleEngineScript.s.sol` | Correct — the reference for how S-1/S-2 should look | ⬜ no change needed | +| S-6 | 3 of 4 shell scripts | Broken shebang `#/bin/bash` (missing `!`), files are executable | ✅ fixed — `#!/bin/bash` in all three | +| S-7 | inheritance + report | `find $dir` — undefined variable; works only by GNU-find accident | ✅ fixed — quoted `"$DIR"` | +| S-8 | `script_surya_report.sh` | `mkdir` without `-p` creates a hidden ordering dependency | ✅ fixed — `mkdir -p`, verified standalone | +| S-9 | `script_surya_inheritance.sh` | Guard checks a directory that is never created | ✅ fixed — guard and target aligned | +| S-10 | 3 surya scripts | No `set -euo pipefail`; a surya failure silently yields a 0-byte PNG | ✅ fixed — added to all three | +| S-11 | 3 surya scripts | `for i in $(find …)` word-splits on whitespace in paths | ✅ fixed — `-print0` / `read -d ''` | +| S-12 | `convert_links_for_pdf.sh` | Well written — the in-repo example of a correct shell script | ⬜ no change needed | +| S-13 | npm `surya:*`, `uml:*` | Write output into the repository root, and it is not gitignored | ✅ fixed — output moved to `docOut/` | +| S-14 | `convert_links_for_pdf.sh` | Default input still points at the root README after the README split | ✅ fixed — defaults to `doc/README.md` | + +**14 entries — 12 findings, all fixed; 2 positive references requiring no change.** + +### What was verified after the fixes + +| Check | Result | +|---|---| +| `forge build` | 0 errors | +| `forge test` | **336 passed**, 0 failed | +| S-1 regression test | Fails when the binding is reverted (`the token must be bound to the engine`), passes when restored | +| `./script_surya_graph.sh` | Runs via `./` (shebang fixed) — 47 files, 0 empty | +| `./script_surya_inheritance.sh` | 47 files, 0 empty | +| `./script_surya_report.sh` | 47 files, 0 empty — **and now runs standalone with no `docOut/` present** | +| `npm run surya:report` | Writes into `docOut/`; repository root stays clean | +| `package.json` | Valid JSON | +| Solidity style checker | 0 violations across 47 files | + +--- + +## A. Foundry scripts (`script/`) + +### S-1. `RuleEngineScript` leaves the CMTAT unbound, so every transfer reverts + +**Verified by running the script in a test.** + +```solidity +RuleEngine ruleEngine = new RuleEngine(admin, address(0), address(0)); +// ^^^^^^^^^^ tokenContract +ruleEngine.addRule(ruleWhitelist); +(bool success,) = + address(cmtatAddress).call(abi.encodeCall(ValidationModuleRuleEngine.setRuleEngine, ruleEngine)); +``` + +The third constructor argument is `tokenContract`. Passing `address(0)` binds nothing, and the script never +calls `bindToken` afterwards. The token is pointed at the engine, but the engine does not recognise the token. + +Because `transferred`, `created` and `destroyed` are all guarded by `onlyBoundToken`, **every** transfer, mint +and burn then reverts with `RuleEngine_ERC3643Compliance_UnauthorizedCaller`. + +Running the script and inspecting the result: + +``` +engine set on token : 0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264 +token bound to engine?: false +getTokenBound : 0x0000000000000000000000000000000000000000 +``` + +and a subsequent `cmtat.mint(...)` reverts. + +**Fixed** — the token is now passed to the constructor, exactly as the sibling script already did: + +```solidity +RuleEngine ruleEngine = new RuleEngine(admin, address(0), cmtatAddress); +``` + +The equivalent alternative — keeping the constructor as-is and calling `ruleEngine.bindToken(cmtatAddress)` +before `setRuleEngine` — was rejected because the constructor form matches the sibling script and cannot be +forgotten as a separate step. + +The NatSpec now states why the binding matters, so a future edit that drops it has to ignore an explicit +warning. `testRun` pins the behaviour: reverting the constructor argument makes it fail with +`the token must be bound to the engine`. + +### S-2. The low-level call silently succeeds when `CMTAT_ADDRESS` is not a contract + +**Verified by running the script against an EOA.** + +```solidity +(bool success,) = + address(cmtatAddress).call(abi.encodeCall(ValidationModuleRuleEngine.setRuleEngine, ruleEngine)); +require(success); +``` + +Two problems, both consequences of using a low-level call where a typed one would do: + +1. **A call to an address with no code returns `success = true`.** A typo'd or stale `CMTAT_ADDRESS` + environment variable pointing at an EOA makes the script complete normally while configuring nothing. A + test that sets `CMTAT_ADDRESS` to a fresh `makeAddr(...)` runs the script to completion without reverting. +2. **`require(success)` discards the revert reason.** The realistic failure — the deployer lacking + `DEFAULT_ADMIN_ROLE` on the token — surfaces as a bare `require` failure with no diagnostic, on a script + that may have already broadcast several deployment transactions. + +**Fixed** — it now goes through the typed interface, as `CMTATWithRuleEngineScript` does: + +```solidity +ValidationModuleRuleEngine(cmtatAddress).setRuleEngine(IRuleEngine(address(ruleEngine))); +``` + +This reverts with the real reason and cannot silently succeed against an EOA, because a typed call to a +codeless address fails on the return-data decode. + +### S-3. The deployed whitelist is empty + +**Reasoned from the source, consistent with the rule's tested behaviour.** + +The script deploys `RuleWhitelistMock` and adds it to the engine without listing any address. Once S-1 is +fixed, the resulting deployment still rejects every transfer, because no participant is whitelisted — and +rejects every mint, because `address(0)` is not listed either (see finding `F-2` in +[CLAUDE_ANALYSIS.md](./CLAUDE_ANALYSIS.md)). + +This may well have been deliberate for a demo whose point is "deploy the wiring, configure it yourself". But +the header did not say so, and the natural reading of an example deployment script is that its output works. + +**Fixed** — the script now seeds the list so the demo deployment is usable as-is: + +```solidity +address[] memory listed = new address[](2); +listed[0] = admin; +listed[1] = address(0); // required for mint and burn; the rule treats it as an ordinary participant +ruleWhitelist.addAddressesToTheList(listed); +``` + +The NatSpec states both that this is demo seeding to be replaced with the real address list, and why +`address(0)` has to be present. `testRun` now performs a real mint against the deployed set-up, so the +combination of S-1 and S-3 is covered end to end rather than assumed. + +### S-4. The script test asserts nothing about the result + +`test/script/RuleEngineScript.t.sol` ends with: + +```solidity +RuleEngineScript deployScript = new RuleEngineScript(); +deployScript.run(); +``` + +There is no assertion. The test passes as long as `run()` does not revert, which is why **S-1 has been +invisible**: the script does exactly what it says and produces a non-functional deployment, and the test is +satisfied. + +**Fixed** — the test now asserts the post-conditions the script exists to establish: + +```solidity +assertEq(address(cmtat.ruleEngine()), address(engine)); +assertTrue(engine.isTokenBound(address(cmtat))); +assertEq(engine.rulesCount(), 1); +``` + +The first two would have failed before an S-1 fix and pass after, which is the definition of a useful +regression test. + +### S-5. `CMTATWithRuleEngineScript` is correct — use it as the reference + +Recorded deliberately, because the contrast is the strongest evidence that S-1 and S-2 are defects rather than +design choices. The sibling script, in the same directory, does both things properly: + +```solidity +RuleEngine ruleEngine = new RuleEngine(admin, trustedForwarder, address(cmtatContract)); // binds +cmtatContract.setRuleEngine(ruleEngine); // typed call +``` + +Same author, same purpose, same directory — one binds the token and uses a typed call, the other does neither. + +--- + +## B. Shell scripts (`doc/script/`) + +### S-6. Broken shebang in three of the four scripts + +**Verified by inspecting the raw bytes.** + +| Script | First line | +|---|---| +| `convert_links_for_pdf.sh` | `#!/bin/bash` — correct | +| `script_surya_graph.sh` | `#/bin/bash` — **missing `!`** | +| `script_surya_inheritance.sh` | `#/bin/bash` — **missing `!`** | +| `script_surya_report.sh` | `#/bin/bash` — **missing `!`** | + +`#/bin/bash` is an ordinary comment, not an interpreter directive. All four files carry the executable bit +(`-rwxrwxr-x`), so they are meant to be run as `./script_surya_graph.sh` — and in that form the kernel finds no +interpreter and the behaviour falls back to the caller's shell. It happens to work when that is bash; it is +undefined otherwise. Invoking them as `bash script_surya_graph.sh` masks the problem entirely, which is +presumably why it has survived. + +**Fixed** — the missing `!` was added in all three. Each script was then executed as `./script_….sh` (not `bash script_….sh`) to confirm the shebang is honoured; all three completed with exit code 0. + +### S-7. `find $dir` uses an undefined variable + +**Verified by reproducing the expansion.** + +`script_surya_inheritance.sh:10` and `script_surya_report.sh:10`: + +```bash +DIR=$(pwd) # sets DIR (uppercase) +for i in $(find $dir -type f); # reads dir (lowercase) — never set +``` + +`$dir` expands to nothing, so the command becomes `find -type f`. GNU find accepts that and defaults to `.`, +which is why these scripts appear to work. Reproduced here: `find -type f` with an empty variable returns the +same file list as `find . -type f`, exit code 0. + +Two consequences: + +- **Portability** — BSD/macOS `find` requires a path operand and errors out. These two scripts are therefore + expected to fail on macOS. *(Reasoned, not tested — no BSD environment available here.)* +- The third script, `script_surya_graph.sh:12`, correctly uses `$DIR`. Another same-directory inconsistency. + +**Fixed** — all three scripts now use `find "$DIR" -type f -name '*.sol'`, quoted, which is also portable to BSD/macOS find. + +### S-8. `script_surya_report.sh` uses `mkdir` without `-p` + +```bash +DIR_OUT=${DIR}/docOut/surya_report +if ! [ -d "$DIR_OUT" ]; then + mkdir ./docOut/surya_report # graph.sh uses `mkdir -p` +fi +``` + +Without `-p`, this fails when `docOut/` does not yet exist — which is the case on a clean checkout, since +`docOut/` is gitignored. The script only works if `script_surya_graph.sh` (which uses `mkdir -p`) has run +first. That ordering requirement is real but undocumented in the scripts themselves. + +**Fixed** — now `mkdir -p`, matching the graph script. Verified by moving `docOut/` aside to reproduce the clean-checkout condition and running `./script_surya_report.sh` alone: it completed with exit code 0 and produced 47 reports. The ordering dependency is gone. + +### S-9. The output guard in `script_surya_inheritance.sh` checks the wrong path + +```bash +DIR_OUT=${DIR}/docOut/inheritance # checked +... + mkdir -p ./docOut/surya_inheritance # created +``` + +The guard tests `docOut/inheritance`, which nothing ever creates, so the condition is always true and `mkdir +-p` runs on every invocation. Harmless because of `-p`, but the guard is dead code and the mismatch suggests a +copy-paste that was only half-updated. + +**Fixed** — the guard and the created directory are now the same path, and both scripts write to `"${DIR_OUT}"` rather than a second hard-coded relative path, so the two cannot drift apart again. + +### S-10. No `set -euo pipefail`, so a failing surya call produces a silent empty PNG + +None of the three surya scripts set any shell options. Combined with the pipeline + +```bash +npx surya inheritance $i | dot -Tpng > ../docOut/surya_inheritance/surya_inheritance_$filename.png; +``` + +a failure in `npx surya` still leaves `dot` running on empty input, which writes a **0-byte or near-empty PNG** +and returns 0. The loop continues, the script exits successfully, and the failure is only discoverable by +noticing the image is blank later. + +This is not hypothetical: the known surya crash on contracts calling `super.()` manifests exactly this way. + +**Fixed** — `set -euo pipefail` added to all three, so a broken render stops the run and names the +file. `convert_links_for_pdf.sh` already sets `-e`. + +### S-11. `for i in $(find …)` word-splits on paths containing whitespace + +All three surya scripts iterate command substitution output unquoted. No current path under `src/` contains a +space, so this is latent rather than live, but it is one directory rename away from producing confusing +errors. + +**Fixed** — all three now use `find … -print0` piped into `while IFS= read -r -d ''`, and every path expansion is quoted. + +### S-12. `convert_links_for_pdf.sh` is the example to copy + +Correct shebang, `set -e`, a usage message with examples, input-file validation, `mktemp` for scratch state, +and a placeholder token to sidestep `sed` escaping in URLs. The surya scripts are well below the standard this +one sets, in the same directory. + +--- + +## C. npm scripts (`package.json`) + +### S-13. Surya and UML scripts write into the repository root + +```json +"surya:report": "npx surya mdreport surya_report_ruleEngine.md src/deployment/RuleEngine.sol", +"surya:graph": "npx surya graph src/deployment/RuleEngine.sol | dot -Tpng > surya_graph_RuleEngine.png" +``` + +Both write to the repo root. `.gitignore` covers `docOut/` but neither `surya_report_ruleEngine.md` nor +`surya_graph_RuleEngine.png`, so running either leaves untracked files in the root that can be committed by +accident. The shell scripts in `doc/script/` write into `docOut/` and are then moved under `doc/schema/surya/` +— two different conventions for the same tool. + +**Fixed** — all five scripts now `mkdir -p` and write beneath `docOut/` (already gitignored). Verified with `npm run surya:report`: the file lands in `docOut/surya_report/` and the repository root stays clean. Superseded guidance follows: + +Original suggestion — write into `docOut/` (already gitignored) or directly under `doc/schema/`, and/or add the +two filenames to `.gitignore`. + +The `uml:*` scripts have the same characteristic via `sol2uml`'s default output location. + +### S-14. `convert_links_for_pdf.sh` now defaults to the short README + +```bash +INPUT_FILE="${2:-../../README.md}" +``` + +From `doc/script/`, that resolves to the root `README.md`. Since the README was split, the root file is a +122-line overview while the full documentation — the thing a PDF is presumably wanted for — is +`doc/README.md` (1780 lines). The default therefore now converts the wrong document. + +This is a consequence of the README split, not an original defect in the script. + +**Fixed** — the default is now `../README.md` (i.e. `doc/README.md`), with a comment stating why. Superseded guidance follows: + +Original suggestion — change the default to `../README.md`, or leave it and document that +the input file must be passed explicitly. + +--- + +## Verification notes + +- **S-1 and S-2 were verified by executing `RuleEngineScript.run()`** inside a temporary Foundry test: once + against a real CMTAT (showing `isTokenBound == false` and a reverting mint), once against an EOA (showing the + script completes). Both probe files were deleted afterwards; the suite is back to **336 tests, 0 failures**. +- **S-6, S-7, S-8, S-9** were verified by reading the script sources and, for S-7, reproducing the empty-variable + `find` expansion in a scratch directory. +- **The BSD/macOS half of S-7 is reasoned, not tested** — no BSD environment was available. +- **S-3, S-11, S-13, S-14** were reasoned from source and file inspection at discovery time. + +After the fixes were applied, the following were additionally executed: + +- All three surya scripts were run **as `./script_….sh`**, which exercises the corrected shebang rather than + bypassing it with `bash script_….sh`. Each produced 47 outputs with no empty files. +- `script_surya_report.sh` was run with `docOut/` moved aside, reproducing the clean-checkout condition that + previously broke it (S-8). It completed with exit code 0. +- `npm run surya:report` was run to confirm S-13: the output lands in `docOut/surya_report/` and the repository + root stays clean. +- The S-1 fix was reverted and `testRun` confirmed to **fail** (`the token must be bound to the engine`) before + being restored, so the new assertions are a real guard rather than a guess. +- **The BSD/macOS portability improvement in S-7 remains reasoned, not tested** — no BSD environment was + available. The quoted `find "$DIR"` form is correct on both, but only GNU find was exercised here. + +`docOut/` holds regenerated scratch output from these runs. It is gitignored and can be deleted. diff --git a/doc/security/audits/tools/v3.0.0-rc5/aderyn-report-feedback.md b/doc/security/audits/tools/v3.0.0-rc5/aderyn-report-feedback.md new file mode 100644 index 0000000..f2fb2a5 --- /dev/null +++ b/doc/security/audits/tools/v3.0.0-rc5/aderyn-report-feedback.md @@ -0,0 +1,151 @@ +# Aderyn Report — Assessment Feedback + +**Tool:** [Aderyn](https://github.com/Cyfrin/aderyn) 0.6.5 +**Report file:** `doc/security/audits/tools/v3.0.0-rc5/aderyn-report.md` +**Assessment date:** 2026-08-13 +**Scope:** `src/`, **mocks excluded** (`-x mocks`), 24 files analysed, solc 0.8.36 / EVM Prague + +```bash +aderyn -x mocks --output doc/security/audits/tools/v3.0.0-rc5/aderyn-report.md +``` + +## Summary + +| ID | Finding | Tool Impact | Instances | Assessment | Decision | +|----|---------|-------------|-----------|------------|----------| +| L-1 | Centralization Risk | Low | 14 | The engine is by definition an admin-operated compliance controller | Accepted by design | +| L-2 | Unspecific Solidity Pragma | Low | 19 | `^0.8.20` is the supported range for integrators; the build pins 0.8.36 | Accepted by design | +| L-3 | PUSH0 Opcode | Low | 24 | EVM target is Prague, which includes PUSH0 | Accepted by design | +| L-4 | Modifier Invoked Only Once | Low | 1 | `onlyRulesLimitManager`, kept for symmetry with the hook pattern | Cosmetic, kept | +| L-5 | Empty Block | Low | 9 | The three access-control hooks × three variants; the modifier does the work | Accepted by design | +| L-6 | Loop Contains `require`/`revert` | Low | 4 | Admin batch operations where fail-fast is the wanted behaviour | Accepted by design | +| L-7 | Costly operations inside loop | Low | 4 | Admin-gated batch writes; inherent to a batch operation | Accepted by design | +| L-8 | Unchecked Return | Low | 1 | `_grantRole` cannot return `false` in a constructor | False positive | + +**0 High · 8 Low (76 instances). Nothing to fix.** + +## Scope verification + +| Check | Result | +|---|---| +| `grep -c 'lib/\|node_modules/' aderyn-report.md` | **0** — no vendored dependency in scope | +| `grep -c 'src/mocks/' aderyn-report.md` | **0** — `-x mocks` applied correctly | +| `grep -c '/home/' aderyn-report.md` | **0** — no absolute paths committed | + +The last check is worth keeping in the routine. Aderyn computes its source links relative to the `--output` +path, so writing the report anywhere outside the repository produces links like +`../../../../../home//…/src/…` — machine-specific paths baked into a committed document. The first +attempt at this run wrote to a scratch directory and produced exactly that in 76 links; it was re-run with the +output inside the repository, which yields the correct relative `../../../../../src/…` form used by rc4. + +## Changes since v3.0.0-rc4 + +**No change:** the same 8 detectors with identical instance counts (14 / 19 / 24 / 1 / 9 / 4 / 1 — in report +order 14, 19, 24, 1, 9, 4, 4, 1). + +Stability is expected. The rc5 production changes were the `contains()`/`add()` collapse, `virtual` on internal +functions, the `SetMaxRules` constructor emission, NatSpec additions and the mock-rule rename. None of those +create or remove an empty block, a loop, a pragma or a centralization vector, and the rename is out of scope +because mocks are excluded. + +## Detailed triage + +### L-1: Centralization Risk (14 instances) + +Flags the `onlyRole` / `onlyOwner` privileged functions across the three deployable variants. + +The RuleEngine is a compliance controller: an operator must be able to add and remove rules and bind tokens, +or the contract has no purpose. The project deliberately ships three access-control shapes — RBAC +(`RuleEngine`), single-owner (`RuleEngineOwnable`) and two-step handover (`RuleEngineOwnable2Step`) — so the +issuer can pick the centralization profile that matches its governance. ERC-3643 itself specifies ERC-173 +ownership for the compliance contract. + +**Decision: accepted by design.** This is a property of the product, not a defect. + +### L-2: Unspecific Solidity Pragma (19 instances) + +Every `src/` file declares `pragma solidity ^0.8.20;`. + +The caret is intentional: these contracts are consumed as a library by integrators who compile against their +own toolchain, and pinning an exact version would force their whole project onto it. The *build* is +deterministic regardless — `foundry.toml` pins `solc = "0.8.36"`, so the artefacts this project ships are +produced by a single known compiler. + +**Decision: accepted by design.** + +### L-3: PUSH0 Opcode (24 instances) + +`foundry.toml` sets `evm_version = 'prague'`. PUSH0 has been available since Shanghai, so its presence is +expected and correct for the declared target. + +The finding is a genuine deployment-target consideration rather than a code defect: a chain that has not +adopted Shanghai cannot execute this bytecode. Any such deployment must recompile with a lower `evm_version`, +which is a build-configuration decision, not a source change. + +**Decision: accepted by design**, with the deployment caveat recorded here. + +### L-4: Modifier Invoked Only Once (1 instance) + +`onlyRulesLimitManager` at `RulesManagementModule.sol:21` guards `setMaxRules` and nothing else. + +Inlining it would break the project's documented pattern, in which every protected operation goes through a +virtual `_onlyX` hook wrapped in an `onlyX` modifier, so each deployable variant can override the +authorization independently. Collapsing the single-use case would make the rule-cap guard the odd one out. + +**Decision: cosmetic, kept deliberately.** + +### L-5: Empty Block (9 instances) + +All nine are the same three functions across the three deployable contracts: + +```solidity +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) {} +``` + +The body is empty because the modifier performs the check. This is the core access-control pattern described +in `CLAUDE.md`, and an empty body is the correct expression of it. + +**Decision: accepted by design.** + +### L-6: Loop Contains `require`/`revert` (4 instances) + +`ERC3643ComplianceExtendedModule` lines 29, 36 and 55 (`bindTokens`, `unbindTokens`, +`setTokenSelfBindingApprovalBatch`) and `RulesManagementModule.setRules` line 62. + +These are administrative batch operations. Reverting the whole batch on a bad element is the wanted semantics: +a partially-applied compliance change is worse than a rejected one. + +**Decision: accepted by design.** + +### L-7: Costly operations inside loop (4 instances) + +The same four loops, flagged for storage writes inside the iteration. A batch operation cannot avoid writing +once per element. All four are gated on a privileged role, so the only party who can pass an oversized array +is the operator, and the only consequence is their own transaction running out of gas. + +Recorded as `A-3` in `CLAUDE_ANALYSIS.md`, where the same conclusion was reached independently. + +**Decision: accepted by design.** + +### L-8: Unchecked Return (1 instance) — false positive + +`RuleEngine.sol:46`: + +```solidity +_grantRole(DEFAULT_ADMIN_ROLE, admin); +``` + +OpenZeppelin's `_grantRole` returns `true` when the role was newly granted and `false` when the account +already held it. This call is in the constructor, on a contract whose role storage is necessarily empty, so +the account cannot already hold the role and the return value is invariably `true`. Checking it would add a +branch that can never be taken. + +**Decision: false positive.** + +## Conclusion + +**No actionable security fixes are required from this Aderyn run.** Seven findings are by-design consequences +of what the RuleEngine is — an admin-operated, rule-iterating compliance controller distributed as a library — +one is a deliberately kept cosmetic, and one (`L-8`) is a verified false positive. No finding is exploitable. diff --git a/doc/security/audits/tools/v3.0.0-rc5/aderyn-report.md b/doc/security/audits/tools/v3.0.0-rc5/aderyn-report.md new file mode 100644 index 0000000..c4f368a --- /dev/null +++ b/doc/security/audits/tools/v3.0.0-rc5/aderyn-report.md @@ -0,0 +1,651 @@ +# Aderyn report — v3.0.0-rc5 + +| | | +|---|---| +| **Tool** | Aderyn 0.6.5 | +| **Scope** | `src/`, **mocks excluded** (`-x mocks`) | +| **Compiler** | solc 0.8.36, EVM Prague | +| **Files analysed** | 24 | +| **Date** | 2026-08-13 | + +```bash +aderyn -x mocks --output doc/security/audits/tools/v3.0.0-rc5/aderyn-report.md +``` + +**Result: 0 High · 8 Low (76 instances) — nothing to fix.** + +| ID | Finding | Severity | Instances | Assessment | +|----|---------|----------|-----------|------------| +| L-1 | Centralization Risk | Low | 14 | **By design** — the engine is an admin-operated compliance controller | +| L-2 | Unspecific Solidity Pragma | Low | 19 | **By design** — `^0.8.20` is the supported range; the build pins 0.8.36 | +| L-3 | PUSH0 Opcode | Low | 24 | **By design** — EVM target is Prague, which has PUSH0 | +| L-4 | Modifier Invoked Only Once | Low | 1 | **Cosmetic** — `onlyRulesLimitManager`, kept for symmetry with the hook pattern | +| L-5 | Empty Block | Low | 9 | **By design** — access-control hooks are intentionally empty bodies | +| L-6 | Loop Contains `require`/`revert` | Low | 4 | **By design** — admin batch operations, fail-fast is wanted | +| L-7 | Costly operations inside loop | Low | 4 | **By design** — admin-gated batch writes | +| L-8 | Unchecked Return | Low | 1 | **False positive** — `_grantRole` cannot return `false` in a constructor | + +**Scope verified:** `grep -c 'lib/\|node_modules/'` = **0** and `grep -c 'src/mocks/'` = **0** in this report. + +**Delta from v3.0.0-rc4: no change** — same 8 detectors, identical instance counts (14/19/24/1/9/4/4/1). + +Triage and per-finding reasoning: [aderyn-report-feedback.md](./aderyn-report-feedback.md). +Overview of all analyses: [AUDIT_OVERVIEW.md](../../AUDIT_OVERVIEW.md). + +--- + +# 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: 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 + +## Files Summary + +| Key | Value | +| --- | --- | +| .sol Files | 24 | +| Total nSLOC | 635 | + + +## Files Details + +| Filepath | nSLOC | +| --- | --- | +| src/RuleEngineBase.sol | 145 | +| src/RuleEngineOwnableShared.sol | 34 | +| src/deployment/RuleEngine.sol | 72 | +| src/deployment/RuleEngineOwnable.sol | 26 | +| src/deployment/RuleEngineOwnable2Step.sol | 38 | +| src/interfaces/IERC3643Compliance.sol | 12 | +| src/interfaces/IERC3643ComplianceExtended.sol | 12 | +| src/interfaces/IRule.sol | 5 | +| src/interfaces/IRulesManagementModule.sol | 14 | +| src/modules/ERC2771ModuleStandalone.sol | 6 | +| src/modules/ERC3643ComplianceExtendedModule.sol | 48 | +| src/modules/ERC3643ComplianceModule.sol | 51 | +| src/modules/RulesManagementModule.sol | 105 | +| src/modules/VersionModule.sol | 8 | +| src/modules/library/ComplianceInterfaceId.sol | 6 | +| src/modules/library/ERC1404InterfaceId.sol | 4 | +| src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol | 7 | +| src/modules/library/ERC3643ComplianceRolesStorage.sol | 4 | +| src/modules/library/Ownable2StepInterfaceId.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 | 17 | +| src/modules/library/RulesManagementModuleRolesStorage.sol | 4 | +| **Total** | **635** | + + +## Issue Summary + +| Category | No. of Issues | +| --- | --- | +| High | 0 | +| Low | 8 | + + +# 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. + +
14 Found Instances + + +- Found in src/deployment/RuleEngine.sol [Line: 25](../../../../../src/deployment/RuleEngine.sol#L25) + + ```solidity + AccessControlEnumerable, + ``` + +- Found in src/deployment/RuleEngine.sol [Line: 112](../../../../../src/deployment/RuleEngine.sol#L112) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/deployment/RuleEngine.sol [Line: 117](../../../../../src/deployment/RuleEngine.sol#L117) + + ```solidity + function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + ``` + +- Found in src/deployment/RuleEngine.sol [Line: 122](../../../../../src/deployment/RuleEngine.sol#L122) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- 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: 28](../../../../../src/deployment/RuleEngineOwnable.sol#L28) + + ```solidity + function transferOwnership(address newOwner) public virtual override onlyOwner { + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 37](../../../../../src/deployment/RuleEngineOwnable.sol#L37) + + ```solidity + function _onlyRulesManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 42](../../../../../src/deployment/RuleEngineOwnable.sol#L42) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 47](../../../../../src/deployment/RuleEngineOwnable.sol#L47) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 15](../../../../../src/deployment/RuleEngineOwnable2Step.sol#L15) + + ```solidity + contract RuleEngineOwnable2Step is RuleEngineOwnableShared, Ownable2Step { + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 32](../../../../../src/deployment/RuleEngineOwnable2Step.sol#L32) + + ```solidity + function transferOwnership(address newOwner) public virtual override onlyOwner { + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 58](../../../../../src/deployment/RuleEngineOwnable2Step.sol#L58) + + ```solidity + function _onlyRulesManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 63](../../../../../src/deployment/RuleEngineOwnable2Step.sol#L63) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 68](../../../../../src/deployment/RuleEngineOwnable2Step.sol#L68) + + ```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;` + +
19 Found Instances + + +- Found in src/RuleEngineBase.sol [Line: 3](../../../../../src/RuleEngineBase.sol#L3) + + ```solidity + 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 + 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/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 + pragma solidity ^0.8.20; + ``` + +- Found in src/interfaces/IERC3643ComplianceExtended.sol [Line: 3](../../../../../src/interfaces/IERC3643ComplianceExtended.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/ERC3643ComplianceExtendedModule.sol [Line: 3](../../../../../src/modules/ERC3643ComplianceExtendedModule.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/ERC3643ComplianceModuleInvariantStorage.sol [Line: 3](../../../../../src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/library/ERC3643ComplianceRolesStorage.sol [Line: 3](../../../../../src/modules/library/ERC3643ComplianceRolesStorage.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; + ``` + +- Found in src/modules/library/RulesManagementModuleRolesStorage.sol [Line: 3](../../../../../src/modules/library/RulesManagementModuleRolesStorage.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. + +
24 Found Instances + + +- Found in src/RuleEngineBase.sol [Line: 3](../../../../../src/RuleEngineBase.sol#L3) + + ```solidity + 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 + 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/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 + pragma solidity ^0.8.20; + ``` + +- Found in src/interfaces/IERC3643ComplianceExtended.sol [Line: 3](../../../../../src/interfaces/IERC3643ComplianceExtended.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/ERC3643ComplianceExtendedModule.sol [Line: 3](../../../../../src/modules/ERC3643ComplianceExtendedModule.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/ERC1404InterfaceId.sol [Line: 3](../../../../../src/modules/library/ERC1404InterfaceId.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol [Line: 3](../../../../../src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/library/ERC3643ComplianceRolesStorage.sol [Line: 3](../../../../../src/modules/library/ERC3643ComplianceRolesStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/library/Ownable2StepInterfaceId.sol [Line: 3](../../../../../src/modules/library/Ownable2StepInterfaceId.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 + 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; + ``` + +- Found in src/modules/library/RulesManagementModuleRolesStorage.sol [Line: 3](../../../../../src/modules/library/RulesManagementModuleRolesStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +
+ + + +## 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. + +
9 Found Instances + + +- Found in src/deployment/RuleEngine.sol [Line: 112](../../../../../src/deployment/RuleEngine.sol#L112) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/deployment/RuleEngine.sol [Line: 117](../../../../../src/deployment/RuleEngine.sol#L117) + + ```solidity + function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + ``` + +- Found in src/deployment/RuleEngine.sol [Line: 122](../../../../../src/deployment/RuleEngine.sol#L122) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 37](../../../../../src/deployment/RuleEngineOwnable.sol#L37) + + ```solidity + function _onlyRulesManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 42](../../../../../src/deployment/RuleEngineOwnable.sol#L42) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable.sol [Line: 47](../../../../../src/deployment/RuleEngineOwnable.sol#L47) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 58](../../../../../src/deployment/RuleEngineOwnable2Step.sol#L58) + + ```solidity + function _onlyRulesManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 63](../../../../../src/deployment/RuleEngineOwnable2Step.sol#L63) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyOwner {} + ``` + +- Found in src/deployment/RuleEngineOwnable2Step.sol [Line: 68](../../../../../src/deployment/RuleEngineOwnable2Step.sol#L68) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +
+ + + +## 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 + +
4 Found Instances + + +- Found in src/modules/ERC3643ComplianceExtendedModule.sol [Line: 29](../../../../../src/modules/ERC3643ComplianceExtendedModule.sol#L29) + + ```solidity + for (uint256 i = 0; i < tokens.length; ++i) { + ``` + +- Found in src/modules/ERC3643ComplianceExtendedModule.sol [Line: 36](../../../../../src/modules/ERC3643ComplianceExtendedModule.sol#L36) + + ```solidity + for (uint256 i = 0; i < tokens.length; ++i) { + ``` + +- Found in src/modules/ERC3643ComplianceExtendedModule.sol [Line: 55](../../../../../src/modules/ERC3643ComplianceExtendedModule.sol#L55) + + ```solidity + for (uint256 i = 0; i < tokens.length; ++i) { + ``` + +- Found in src/modules/RulesManagementModule.sol [Line: 62](../../../../../src/modules/RulesManagementModule.sol#L62) + + ```solidity + for (uint256 i = 0; i < rules_.length; ++i) { + ``` + +
+ + + +## L-7: Costly operations inside loop + +Invoking `SSTORE` operations in loops may waste gas. Use a local variable to hold the loop computation result. + +
4 Found Instances + + +- Found in src/modules/ERC3643ComplianceExtendedModule.sol [Line: 29](../../../../../src/modules/ERC3643ComplianceExtendedModule.sol#L29) + + ```solidity + for (uint256 i = 0; i < tokens.length; ++i) { + ``` + +- Found in src/modules/ERC3643ComplianceExtendedModule.sol [Line: 36](../../../../../src/modules/ERC3643ComplianceExtendedModule.sol#L36) + + ```solidity + for (uint256 i = 0; i < tokens.length; ++i) { + ``` + +- Found in src/modules/ERC3643ComplianceExtendedModule.sol [Line: 55](../../../../../src/modules/ERC3643ComplianceExtendedModule.sol#L55) + + ```solidity + for (uint256 i = 0; i < tokens.length; ++i) { + ``` + +- Found in src/modules/RulesManagementModule.sol [Line: 62](../../../../../src/modules/RulesManagementModule.sol#L62) + + ```solidity + for (uint256 i = 0; i < rules_.length; ++i) { + ``` + +
+ + + +## 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: 46](../../../../../src/deployment/RuleEngine.sol#L46) + + ```solidity + _grantRole(DEFAULT_ADMIN_ROLE, admin); + ``` + +
+ + + diff --git a/doc/security/audits/tools/v3.0.0-rc5/slither-report-feedback.md b/doc/security/audits/tools/v3.0.0-rc5/slither-report-feedback.md new file mode 100644 index 0000000..5a74724 --- /dev/null +++ b/doc/security/audits/tools/v3.0.0-rc5/slither-report-feedback.md @@ -0,0 +1,103 @@ +# Slither Report — Assessment Feedback + +**Tool:** [Slither](https://github.com/crytic/slither) 0.11.5 +**Report file:** `doc/security/audits/tools/v3.0.0-rc5/slither-report.md` +**Assessment date:** 2026-08-13 +**Scope:** `src/`, **mocks excluded**, 103 contracts analysed, solc 0.8.36 / EVM Prague + +```bash +slither . --checklist --filter-paths "openzeppelin-contracts|test|CMTAT|forge-std|mocks" \ + > doc/security/audits/tools/v3.0.0-rc5/slither-report.md +``` + +## Summary + +| IDs | Detector | Tool Impact | Assessment | Decision | +|-----|----------|-------------|------------|----------| +| 0-9 | `calls-loop` | Low | Inherent to pluggable rule-engine dispatch; bounded by `maxRules` | Accepted by design | +| 10-11 | `unindexed-event-address` | Informational | The ERC-3643 reference declares these events unindexed — matching it is conformance | Accepted by design | + +**0 High · 0 Medium · 10 Low · 2 Informational. Nothing to fix.** + +## Scope verification + +Confirmed before triage, because a `--filter-paths` entry that matches nothing fails open and silently pulls +the whole vendored tree into scope: + +| Check | Result | +|---|---| +| `grep -c 'lib/\|node_modules/' slither-report.md` | **0** — no vendored dependency in scope | +| `grep -c 'src/mocks/' slither-report.md` | **0** — mocks correctly excluded | + +The filter list names dependencies individually (`openzeppelin-contracts`, `CMTAT`, `forge-std`) rather than +filtering `lib` wholesale. That still resolves correctly here — each entry matches its `lib/` path as a +substring, and `lib/CMTATv3.0.0` and `lib/openzeppelin-contracts-upgradeable` are caught by the `CMTAT` and +`openzeppelin-contracts` entries respectively. `lib/ERC-3643`, added this release, matches no entry, but it is +imported by nothing under `src/` so the compiler never pulls it in — confirmed by the zero `lib/` count above. +Worth revisiting if an `src/` contract ever imports from it. + +## Changes since v3.0.0-rc4 + +**No change in counts:** 10 `calls-loop` + 2 `unindexed-event-address`, identical to rc4. + +This is the expected outcome. The rc5 changes that touched production code — collapsing the +`contains()`/`add()` double lookup, adding `virtual` to internal functions, emitting `SetMaxRules` at +construction, NatSpec, and the rename of the mock rules — do not affect either detector. The mock rename is +invisible here because mocks are excluded from scope. + +One triage change, not a count change: the `unindexed-event-address` disposition moves from **Deferred** to +**Accepted by design** (see below). + +## Detailed triage + +### IDs 0-9: `calls-loop` + +Ten instances across `RulesManagementModule._transferred` (both overloads), +`RuleEngineBase._detectTransferRestriction`, `_detectTransferRestrictionFrom` and +`_messageForTransferRestriction`. + +The engine exists to call a configurable list of rule contracts, so an external call inside a loop is the +product, not a defect. The risk the detector points at — unbounded iteration — is bounded on-chain by +`maxRules` (default **10**, `DEFAULT_MAX_RULES`), and the cap is now emitted at deployment so it is visible +from the event log alone. + +Rules are trusted business logic by convention: the engine refuses to grant a role to an address currently +configured as a rule, and the documentation states that rule contracts must not hold +`RULES_MANAGEMENT_ROLE`. + +**Decision: accepted by design.** Documented in `doc/technical/RuleEngine-with-CMTAT.md` §4.1 and +`RuleEngine-with-ERC3643.md` §4.4, and recorded as `A-3` in `CLAUDE_ANALYSIS.md`. + +### IDs 10-11: `unindexed-event-address` + +`TokenBound(address token)` and `TokenUnbound(address token)` in `IERC3643Compliance.sol` (lines 18 and 24) +declare their address parameter without `indexed`. + +rc4 deferred this as "valid optimization, but ABI-breaking to change now". That reasoning was incomplete. The +ERC-3643 reference implementation declares the same two events **unindexed**: + +```solidity +// lib/ERC-3643/contracts/compliance/modular/IModularCompliance.sol:82,89 +event TokenBound(address _token); +event TokenUnbound(address _token); +``` + +(identically in `compliance/legacy/ICompliance.sol:85,92`.) + +So the current declaration is **conformance with the standard**, not an oversight. Adding `indexed` would move +the parameter from the data section into a topic, changing the event's topic layout and breaking any indexer +written against the ERC-3643 interface. The correct place to change this is the standard, not this +implementation. + +Note the contrast with `TokenSelfBindingApprovalSet`, which *is* indexed: that event is specific to this +project's extended module and carries no conformance obligation, so indexing it was free. + +**Decision: accepted by design (spec conformance).** Upgraded from rc4's "deferred", since the reason is now +positive rather than merely cautious. + +## Conclusion + +**No actionable security fixes are required from this Slither run.** Both detectors are architectural +by-design outcomes, and neither is exploitable: `calls-loop` describes the engine's core dispatch, bounded by +an on-chain cap, and `unindexed-event-address` reflects deliberate conformance with the ERC-3643 event +signatures. diff --git a/doc/security/audits/tools/v3.0.0-rc5/slither-report.md b/doc/security/audits/tools/v3.0.0-rc5/slither-report.md new file mode 100644 index 0000000..a4f849f --- /dev/null +++ b/doc/security/audits/tools/v3.0.0-rc5/slither-report.md @@ -0,0 +1,136 @@ +# Slither report — v3.0.0-rc5 + +| | | +|---|---| +| **Tool** | Slither 0.11.5 | +| **Scope** | `src/`, **mocks excluded** | +| **Compiler** | solc 0.8.36, EVM Prague | +| **Contracts analysed** | 103 | +| **Date** | 2026-08-13 | + +```bash +slither . --checklist --filter-paths "openzeppelin-contracts|test|CMTAT|forge-std|mocks" \ + > doc/security/audits/tools/v3.0.0-rc5/slither-report.md +``` + +**Result: 0 High · 0 Medium · 10 Low · 2 Informational — nothing to fix.** + +| Detector | Severity | Instances | Assessment | +|---|---|---|---| +| `calls-loop` | Low | 10 | **By design** — the engine iterates its rule set; bounded by `maxRules` (default 10) | +| `unindexed-event-address` | Informational | 2 | **By design (spec conformance)** — the ERC-3643 reference declares these events unindexed | + +**Scope verified:** `grep -c 'lib/\|node_modules/'` = **0** and `grep -c 'src/mocks/'` = **0** in this report, so +no vendored dependency or mock contract entered the analysis. + +**Delta from v3.0.0-rc4: no change** — same two detectors, same 10 + 2 instances. + +Triage and per-finding reasoning: [slither-report-feedback.md](./slither-report-feedback.md). +Overview of all analyses: [AUDIT_OVERVIEW.md](../../AUDIT_OVERVIEW.md). + +--- + +**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 +[RulesManagementModule._transferred(address,address,uint256)](src/modules/RulesManagementModule.sol#L190-L195) has external calls inside a loop: [IRule(_rules.pos(i)).transferred(from,to,value)](src/modules/RulesManagementModule.sol#L193) + Calls stack containing the loop: + RuleEngineBase.destroyed(address,uint256) + +src/modules/RulesManagementModule.sol#L190-L195 + + + - [ ] ID-1 +[RuleEngineBase._messageForTransferRestriction(uint8)](src/RuleEngineBase.sol#L211-L222) has external calls inside a loop: [IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)](src/RuleEngineBase.sol#L217) + Calls stack containing the loop: + RuleEngineBase.messageForTransferRestriction(uint8) + +src/RuleEngineBase.sol#L211-L222 + + + - [ ] ID-2 +[RuleEngineBase._detectTransferRestriction(address,address,uint256)](src/RuleEngineBase.sol#L166-L175) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestriction(from,to,value)](src/RuleEngineBase.sol#L169) + Calls stack containing the loop: + RuleEngineBase.canTransfer(address,address,uint256) + RuleEngineBase.detectTransferRestriction(address,address,uint256) + +src/RuleEngineBase.sol#L166-L175 + + + - [ ] ID-3 +[RuleEngineBase._messageForTransferRestriction(uint8)](src/RuleEngineBase.sol#L211-L222) has external calls inside a loop: [IRule(rule(i)).messageForTransferRestriction(restrictionCode)](src/RuleEngineBase.sol#L218) + Calls stack containing the loop: + RuleEngineBase.messageForTransferRestriction(uint8) + +src/RuleEngineBase.sol#L211-L222 + + + - [ ] ID-4 +[RulesManagementModule._transferred(address,address,uint256)](src/modules/RulesManagementModule.sol#L190-L195) has external calls inside a loop: [IRule(_rules.pos(i)).transferred(from,to,value)](src/modules/RulesManagementModule.sol#L193) + Calls stack containing the loop: + RuleEngineBase.created(address,uint256) + +src/modules/RulesManagementModule.sol#L190-L195 + + + - [ ] ID-5 +[RuleEngineBase._detectTransferRestrictionFrom(address,address,address,uint256)](src/RuleEngineBase.sol#L185-L199) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestrictionFrom(spender,from,to,value)](src/RuleEngineBase.sol#L193) + Calls stack containing the loop: + RuleEngineBase.canTransferFrom(address,address,address,uint256) + RuleEngineBase.detectTransferRestrictionFrom(address,address,address,uint256) + +src/RuleEngineBase.sol#L185-L199 + + + - [ ] ID-6 +[RuleEngineBase._detectTransferRestrictionFrom(address,address,address,uint256)](src/RuleEngineBase.sol#L185-L199) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestrictionFrom(spender,from,to,value)](src/RuleEngineBase.sol#L193) + Calls stack containing the loop: + RuleEngineBase.detectTransferRestrictionFrom(address,address,address,uint256) + +src/RuleEngineBase.sol#L185-L199 + + + - [ ] ID-7 +[RulesManagementModule._transferred(address,address,uint256)](src/modules/RulesManagementModule.sol#L190-L195) has external calls inside a loop: [IRule(_rules.pos(i)).transferred(from,to,value)](src/modules/RulesManagementModule.sol#L193) + Calls stack containing the loop: + RuleEngineBase.transferred(address,address,uint256) + +src/modules/RulesManagementModule.sol#L190-L195 + + + - [ ] ID-8 +[RuleEngineBase._detectTransferRestriction(address,address,uint256)](src/RuleEngineBase.sol#L166-L175) has external calls inside a loop: [restriction = IRule(rule(i)).detectTransferRestriction(from,to,value)](src/RuleEngineBase.sol#L169) + Calls stack containing the loop: + RuleEngineBase.detectTransferRestriction(address,address,uint256) + +src/RuleEngineBase.sol#L166-L175 + + + - [ ] ID-9 +[RulesManagementModule._transferred(address,address,address,uint256)](src/modules/RulesManagementModule.sol#L209-L214) has external calls inside a loop: [IRule(_rules.pos(i)).transferred(spender,from,to,value)](src/modules/RulesManagementModule.sol#L212) + Calls stack containing the loop: + RuleEngineBase.transferred(address,address,address,uint256) + +src/modules/RulesManagementModule.sol#L209-L214 + + +## unindexed-event-address +Impact: Informational +Confidence: High + - [ ] ID-10 +Event [IERC3643Compliance.TokenBound(address)](src/interfaces/IERC3643Compliance.sol#L18) has address parameters but no indexed parameters + +src/interfaces/IERC3643Compliance.sol#L18 + + + - [ ] ID-11 +Event [IERC3643Compliance.TokenUnbound(address)](src/interfaces/IERC3643Compliance.sol#L24) has address parameters but no indexed parameters + +src/interfaces/IERC3643Compliance.sol#L24 + + diff --git a/doc/technical/RuleEngine-with-CMTAT.md b/doc/technical/RuleEngine-with-CMTAT.md new file mode 100644 index 0000000..0fdb1fb --- /dev/null +++ b/doc/technical/RuleEngine-with-CMTAT.md @@ -0,0 +1,203 @@ +# Using the RuleEngine with a CMTAT token + +How to attach a RuleEngine to a [CMTAT](https://github.com/CMTA/CMTAT) token, which entry points CMTAT +actually calls, how to configure both sides, and the limitations to know about before deploying. + +For the ERC-3643 equivalent, see [RuleEngine-with-ERC3643.md](./RuleEngine-with-ERC3643.md). The two token +standards drive **disjoint entry points** on the engine, so a rule or an integration written against one does +not automatically hold for the other. + +## 1. Flow + +![RuleEngine flow with a CMTAT token](../schema/plantuml/ruleengine-flow-cmtat.png) + +_Diagram source: [doc/schema/plantuml/ruleengine-flow-cmtat.puml](../schema/plantuml/ruleengine-flow-cmtat.puml)._ + +## 2. Which entry points CMTAT uses + +CMTAT calls **`transferred`** for every state-changing operation, and picks between two overloads according to whether the operation has a spender. + +The choice is made in +`ValidationModuleRuleEngine._callRuleEngineTransferred`: + +```solidity +if (spender != address(0)) { + ruleEngine_.transferred(spender, from, to, value); // 4-argument +} else { + ruleEngine_.transferred(from, to, value); // 3-argument +} +``` + +The `spender` value comes from `CMTATBaseCommon`: + +| CMTAT operation | spender passed | Overload called | +|---|---|---| +| `transfer(to, value)` | `address(0)` | **3-argument** `transferred(from, to, value)` | +| `transferFrom(from, to, value)` | `_msgSender()` | 4-argument `transferred(spender, from, to, value)` | +| `mint(to, value)` | `_msgSender()` | 4-argument, with `from == address(0)` | +| `burn(from, value)` | `_msgSender()` | 4-argument, with `to == address(0)` | + +Two consequences follow: + +- A **plain transfer never carries a spender**. The zero address is a branch condition inside CMTAT and is + never forwarded, so the engine is never called with a zero spender. +- Since CMTAT v3.3.0, **mint and burn go through the 4-argument overload** with the operator as `spender`. + A rule that rejects unknown spenders must skip or adapt that check when `from == address(0)` (mint) or `to == address(0)` (burn), or it will block issuance and redemption. + +**CMTAT never calls `created()` or `destroyed()`.** Those belong to `IERC3643Compliance` and are used only by ERC-3643 tokens. + +The 4-argument `transferred` is conversely declared by CMTAT's `IRuleEngine` and is never +reached by an ERC-3643 token. + +### Read-only path + +CMTAT exposes the ERC-1404 view path, which the engine answers by iterating the same rules and returning the **first non-zero** restriction code: + +- `detectTransferRestriction(from, to, value)` / `canTransfer(from, to, value)` +- `detectTransferRestrictionFrom(spender, from, to, value)` / `canTransferFrom(spender, from, to, value)` +- `messageForTransferRestriction(code)` — resolves the code against the rule that claims it + +## 3. Configuration + +### 3.1 Deploy the RuleEngine + +Three deployable variants share identical core logic and differ only in access control. Pick one: + +| Contract | Access control | Use case | +|---|---|---| +| `RuleEngine` | Role-based (`AccessControlEnumerable`) | Multiple operators, granular permissions | +| `RuleEngineOwnable` | ERC-173 `Ownable` | Single owner | +| `RuleEngineOwnable2Step` | ERC-173 `Ownable2Step` | Single owner, safer handover | + +```solidity +// admin, trusted ERC-2771 forwarder (address(0) to disable gasless), token to bind (may be address(0)) +RuleEngine engine = new RuleEngine(admin, forwarder, address(0)); +``` + +The forwarder is **immutable** — it is fixed at construction and cannot be changed afterwards. + +### 3.2 Bind the token to the engine + +Only bound tokens may call `transferred`. Binding is a privileged operation on the engine: + +```solidity +engine.bindToken(address(cmtat)); // COMPLIANCE_MANAGER_ROLE, or owner on the ownable variants +``` + +You can also pass the token as the third constructor argument to bind it at deployment. + +CMTAT does **not** self-bind — unlike ERC-3643's `setCompliance`, `setRuleEngine` does not call back into the engine. Binding is therefore always an explicit operator action, and the self-binding approval mechanism (`setTokenSelfBindingApproval`) is not needed for CMTAT. + +### 3.3 Point the token at the engine + +Either at construction: + +```solidity +ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(address(engine))); +``` + +or afterwards, from `ValidationModuleRuleEngine`: + +```solidity +cmtat.setRuleEngine(IRuleEngine(address(engine))); // caller needs DEFAULT_ADMIN_ROLE on the token +``` + +### 3.4 Add rules + +```solidity +engine.addRule(IRule(address(rule))); // RULES_MANAGEMENT_ROLE, or owner +engine.setRules(rulesArray); // replaces the whole set atomically +``` + +A rule must implement `IRule` and advertise `RuleInterfaceId.IRULE_INTERFACE_ID` (`0x2497d6cb`) through ERC-165; the engine validates this on `addRule` and rejects anything else. Rules run **in declaration order** and the first one to revert aborts the whole transaction. + +### 3.5 Roles reference (`RuleEngine` variant) + +| Role | Grants | +|---|---| +| `DEFAULT_ADMIN_ROLE` | Everything (the `hasRole` override makes the admin hold all roles), plus `setMaxRules` | +| `RULES_MANAGEMENT_ROLE` | `addRule`, `removeRule`, `setRules`, `clearRules` | +| `COMPLIANCE_MANAGER_ROLE` | `bindToken`, `unbindToken`, and the batch variants | + +On `RuleEngineOwnable` / `RuleEngineOwnable2Step` all three collapse to `onlyOwner`. + +## 4. Warnings and limitations + +### 4.1 The rule set is iterated on every transfer + +Every configured rule is called on every transfer, mint, burn **and** on every view call. Cost is O(number of rules). + +An on-chain cap (`maxRules`, default **10**) bounds this; raising it re-exposes unbounded gas cost for +administrative operations such as `clearRules`. + +A single gas-heavy or misconfigured rule can make all transfers fail. + +### 4.2 The 3-argument view path fails open for spender-dependent rules + +`detectTransferRestriction` and `canTransfer` carry no `spender`. A rule keyed by spender — a per-minter mint allowance, a spender whitelist — cannot evaluate the operation on that path and must answer "no restriction". +The engine aggregates that answer, so **these two views can report an operation as allowed that the state-changing path will revert**. + +Use the 4-argument `detectTransferRestrictionFrom` / `canTransferFrom` to pre-check anything with an operator. Recorded as finding `H-1` in [CLAUDE_ANALYSIS.md](../security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md). + +### 4.3 Address-list rules treat `address(0)` as a participant + +**This concerns `RuleWhitelistMock`, a reference rule in `src/mocks/`, not a production rule.** Production rules live in [CMTA/Rules](https://github.com/CMTA/Rules) and may handle the sentinel differently — check the +rule you actually deploy. + +`RuleWhitelistMock` checks both endpoints without exempting the zero-address sentinel. Because CMTAT routes mint through the 4-argument overload with `from == address(0)`, and the whitelist's `from`/`to` checks still apply, **the zero address must be whitelisted for minting to be permitted**. + +Whitelisting only real holders silently blocks issuance. Recorded as finding `F-2`. + +### 4.4 One engine shared by several tokens is not neutral + +An engine can be bound to multiple tokens, but the ERC-3643 callbacks **do not pass the token address to the rules**. + +Any stateful rule that keeps per-address accounting therefore mixes state across every bound token. + +Only bind tokens that are equally trusted and governed together. Unbinding does not retroactively separate state already accumulated. + +### 4.5 Restriction codes must be unique across the rule set + +The engine returns the first non-zero code, and `messageForTransferRestriction` resolves a code against the first rule claiming it. + +If two rules share a code they must return the same message, or operators get inconsistent feedback. Keep the CMTAT-reserved ranges free. + +### 4.6 Rules are trusted code + +Rule contracts are called on every transfer and can revert or consume arbitrary gas. Treat them as trusted business logic. + +Do not grant `RULES_MANAGEMENT_ROLE` to a rule contract — the engine blocks granting any role +to an address currently configured as a rule, but the check is one-directional and does not stop an already privileged address from later being added as a rule. + +## 5. What is tested + +| Area | File | Tests | +|---|---|---| +| Engine + CMTAT (current) | `RuleEngine/RulesManagementModuleTest/CMTATIntegration.t.sol` | 3 | +| Engine + CMTAT v3.0.0 | `RuleEngine/RulesManagementModuleTest/CMTATIntegrationV3.t.sol` | 3 | +| Whitelist rule + CMTAT (current) | `RuleWhitelist/CMTATIntegration.t.sol` | 11 | +| Whitelist rule + CMTAT v3.0.0 | `RuleWhitelist/CMTATIntegrationV3.t.sol` | 11 | +| Reverting rule propagation | `RuleEngine/RulesManagementModuleTest/RuleEngineOperationRevert.t.sol` | 1 | +| Reverting rule, CMTAT v3.0.0 | `RuleEngine/RulesManagementModuleTest/RuleEngineOperationRevertV3.t.sol` | 1 | +| Deployment script | `script/CMTATWithRuleEngineScript.t.sol` | 1 | + +**31 CMTAT-specific tests.** Coverage includes: a real `CMTATStandardStandalone` bound to the engine, transfers accepted and rejected through the whitelist, restriction-code propagation back to the token, and a rule that reverts mid-transfer. + +**Backward compatibility is tested against two CMTAT versions.** The `…V3` suites deploy a real CMTAT **v3.0.0** token (submodule `lib/CMTATv3.0.0`, remapped `CMTATv3.0.0/`) alongside the current **v3.3.0-rc3** token (`lib/CMTAT`). + +Both are bound to the same engine implementation. + +### Version compatibility + +| RuleEngine | CMTAT | +|---|---| +| v3.0.0-rc5 | ≥ v3.0.0, target v3.3.0-rc3 | +| v3.0.0-rc4 | ≥ v3.0.0, target v3.3.0-rc1 | +| v1.0.2.1 | v2.3.0 (audited) | + +## 6. Related documents + +- [RuleEngine-with-ERC3643.md](./RuleEngine-with-ERC3643.md) — the ERC-3643 counterpart +- [../README.md](../README.md) — full interface and API reference +- [CLAUDE_ANALYSIS.md](../security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md) — code-quality review, findings `H-1` and `F-2` +- Production rules: [github.com/CMTA/Rules](https://github.com/CMTA/Rules) diff --git a/doc/technical/RuleEngine-with-ERC3643.md b/doc/technical/RuleEngine-with-ERC3643.md new file mode 100644 index 0000000..3342de4 --- /dev/null +++ b/doc/technical/RuleEngine-with-ERC3643.md @@ -0,0 +1,217 @@ +# Using the RuleEngine with an ERC-3643 token + +How to attach a RuleEngine to an [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) (T-REX) token as its compliance contract, which entry points the token actually calls, how to configure the self-binding handshake, and the limitations to know about before deploying. + +For the CMTAT equivalent, see [RuleEngine-with-CMTAT.md](./RuleEngine-with-CMTAT.md). The two standards drive +**disjoint entry points** on the engine, so a rule or an integration written against one does not +automatically hold for the other. + +## 1. Flow + +![RuleEngine flow with an ERC-3643 token](../schema/plantuml/ruleengine-flow-erc3643.png) + +_Diagram source: [doc/schema/plantuml/ruleengine-flow-erc3643.puml](../schema/plantuml/ruleengine-flow-erc3643.puml)._ + +## 2. Which entry points an ERC-3643 token uses + +In ERC-3643 the RuleEngine plays the role of the **compliance contract**. Taken from `Token.sol` in the reference implementation (submodule `lib/ERC-3643`, tag 4.1.3): + +| Token operation | Pre-check | State-changing callback | +|---|---|---| +| `transfer(to, amount)` | `canTransfer(msg.sender, to, amount)` | `transferred(msg.sender, to, amount)` | +| `transferFrom(from, to, amount)` | `canTransfer(from, to, amount)` | `transferred(from, to, amount)` | +| `forcedTransfer(from, to, amount)` | — | `transferred(from, to, amount)` | +| `mint(to, amount)` | `canTransfer(address(0), to, amount)` | `created(to, amount)` | +| `burn(from, amount)` | — | `destroyed(from, amount)` | +| `setCompliance(newCompliance)` | — | `unbindToken(this)` then `bindToken(this)` | + +Two consequences follow: + +- **ERC-3643 compliance callbacks carry no spender.** An ERC-3643 token therefore *never* reaches the + 4-argument `transferred(spender, from, to, value)` overload — that one is declared by CMTAT's `IRuleEngine`, + not by ERC-3643. Everything goes through the 3-argument form. +- **Mint and burn use dedicated entry points**, `created` and `destroyed`, rather than `transferred`. Inside + the engine both run the same 3-argument rule loop: + `created(to, value)` → `_transferred(address(0), to, value)`, and + `destroyed(from, value)` → `_transferred(from, address(0), value)`. + +Note the mint pre-check passes `address(0)` as the origin. That single detail drives two of the limitations in +section 4. + +## 3. Configuration + +### 3.1 Deploy the RuleEngine + +Same three variants as for CMTAT. ERC-3643's own specification points at ERC-173 for ownership: + +> The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of setting the Compliance parameters and binding the Compliance to a Token contract. + +so `RuleEngineOwnable` or `RuleEngineOwnable2Step` is the closest match to the spec, though `RuleEngine` (role-based) works identically and is preferable with multiple operators. + +```solidity +RuleEngineOwnable engine = new RuleEngineOwnable(owner, forwarder, address(0)); +``` + +### 3.2 Grant self-binding approval — required before `setCompliance` + +`Token.setCompliance` makes the **token** call `bindToken` and `unbindToken` on the compliance contract, not +the operator: + +```solidity +function setCompliance(address _compliance) public override onlyOwner { + if (address(_tokenCompliance) != address(0)) { + _tokenCompliance.unbindToken(address(this)); + } + _tokenCompliance = IModularCompliance(_compliance); + _tokenCompliance.bindToken(address(this)); + emit ComplianceAdded(_compliance); +} +``` + +To support that without letting arbitrary contracts bind themselves, the engine gates self-binding behind an explicit approval: + +```solidity +engine.setTokenSelfBindingApproval(address(token), true); // COMPLIANCE_MANAGER_ROLE, or owner +engine.isTokenSelfBindingApproved(address(token)); // -> true +``` + +**Recommended operational sequence:** + +1. On the target engine, grant self-binding approval for the token. +2. Call `token.setCompliance(address(engine))`. +3. Optionally revoke the approval afterwards if no further migration is expected. + +Without step 1, `setCompliance` reverts. This is exercised by +`testSetComplianceRevertsWithoutSelfBindingApproval`. + +An operator can equally bind the token directly with `engine.bindToken(address(token))` and skip the approval entirely — self-binding approval exists specifically to support the T-REX `setCompliance` pattern. + +### 3.3 Migrating between engines + +`setCompliance` unbinds from the old compliance and binds to the new one in a single transaction. The **new** engine must have granted self-binding approval for the token before the call; the old one does not need it still granted for the unbind to succeed. + +Verified by `testSetComplianceUnbindsThePreviousEngine`. + +### 3.4 Add rules + +Identical to the CMTAT case: + +```solidity +engine.addRule(IRule(address(rule))); +``` + +Rules must implement `IRule` and advertise `RuleInterfaceId.IRULE_INTERFACE_ID` via ERC-165. They run in declaration order; the first to revert aborts the transaction. + +### 3.5 Batch helpers + +The extended compliance module adds operator-side batch operations: + +```solidity +engine.bindTokens(tokens); // bind several tokens +engine.unbindTokens(tokens); +engine.setTokenSelfBindingApprovalBatch(tokens, true); +engine.getTokenBounds(); // every bound token +``` + +## 4. Warnings and limitations + +### 4.1 The mint pre-check fails open for spender-dependent rules + +`Token.mint` pre-checks with `canTransfer(address(0), _to, _amount)`. That signature carries **no spender**, so a rule keyed by spender — a per-minter allowance, for instance — cannot evaluate the mint and must answer "no restriction". + +The engine aggregates that answer, so the pre-check reports the mint as allowed even when a spender-aware rule would reject it. + +The state-changing path is unaffected and still enforces correctly; the damage is to anything that trusts the pre-check. Pinned by `testMintPreCheckFailsOpenForSpenderKeyedRule`, and recorded as finding `H-1` in [CLAUDE_ANALYSIS.md](../security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md). + +### 4.2 `address(0)` must be whitelisted for minting to work + +**This concerns `RuleWhitelistMock`, a reference rule in `src/mocks/`, not a production rule.** Production rules live in [CMTA/Rules](https://github.com/CMTA/Rules) and may handle the sentinel differently — check the +rule you actually deploy. + +The behavior is described here because the mock is what the examples, scripts and tests in this repository use, and because integrators copy it. + +`RuleWhitelistMock` treats the zero address as an ordinary participant: + +```solidity +if (!addressIsListed(from)) { return CODE_ADDRESS_FROM_NOT_WHITELISTED; } +``` + +Because the mint pre-check passes `address(0)` as `from`, **an issuer who whitelists only real holders cannot mint at all** — every mint is refused with `CODE_ADDRESS_FROM_NOT_WHITELISTED`. Whitelist `address(0)` explicitly to permit issuance. + +Pinned by `testMintIsBlockedWhenZeroAddressNotListed`, recorded as finding `F-2`. + +### 4.3 One engine shared by several tokens is not neutral + +The ERC-3643 callbacks **do not pass the token address to the rules**, so a stateful rule that keeps +per-address accounting mixes state across every bound token. + +- Only bind tokens that are equally trusted and governed together. +- Unbinding does not retroactively separate state already accumulated in a rule. + +This matters more here than for CMTAT, because `bindTokens` makes multi-token binding a one-call operation. + +### 4.4 The rule set is iterated on every operation + +O(number of rules) on every transfer, mint, burn and view call, capped by `maxRules` (default **10**). + +A gas-heavy rule affects every operation on every bound token. + +### 4.5 Only bound tokens may call the callbacks + +`transferred`, `created` and `destroyed` all revert with +`RuleEngine_ERC3643Compliance_UnauthorizedCaller` for any caller that is not a bound token. Verified by +`testUnboundCallerCannotCallTransferred` and `testUnboundCallerCannotCallCreatedOrDestroyed`. + +### 4.6 Restriction codes must be unique across the rule set + +As with CMTAT: the engine returns the first non-zero code, so overlapping codes across rules produce inconsistent operator feedback unless they share the same message. + +### 4.7 ERC-7551 support is a draft subset + +`IERC7551Compliance` comes from `draft-IERC7551` and is not final. This project implements a subset focused on +`canTransferFrom`. Do not assume full ERC-7551 conformance. + +### 4.8 The reference T-REX token cannot be compiled into this test suite + +Relevant if you intend to add tests against the vendored implementation in `lib/ERC-3643`. Three independent blockers: + +- it pins `pragma solidity 0.8.17` (exact), while this project compiles with **0.8.36**; +- it requires **OpenZeppelin 4.8.x**, while this project uses **5.7.0**; +- it imports the `onchain-id/solidity` package, which is not installed here. + +Attempting it fails at resolution: + +``` +Error: Encountered invalid solc version in lib/ERC-3643/contracts/token/Token.sol: +No solc version exists that matches the version requirement: =0.8.17 +``` + +The integration tests therefore use `ERC3643TokenMock`, whose compliance interaction is copied from the reference `Token.sol` (the table in section 2 is taken from it directly). Compiling the real token would require unpinning the project compiler and vendoring a second OpenZeppelin major alongside the current one. + +## 5. What is tested + +| Area | File | Tests | +|---|---|---| +| Compliance module, RBAC variant | `RuleEngine/ERC3643Compliance.t.sol` | 30 | +| Compliance module, ownable variant | `RuleEngineOwnable/ERC3643Compliance.t.sol` | 29 | +| End-to-end with an ERC-3643 style token | `RuleEngine/ERC3643TokenIntegration.t.sol` | 11 | + +**70 ERC-3643-specific tests.** The `RuleEngineOwnable2Step` suite exercises the same compliance surface again +through its own variant. + +The end-to-end suite covers, using a token that drives the engine exactly as `Token.sol` does: + +- `setCompliance` binding the token, and unbinding a previous engine on migration +- `setCompliance` reverting without self-binding approval +- transfers accepted and rejected through the whitelist rule, via `transferred` +- mint through `created` and burn through `destroyed` +- `transferred` / `created` / `destroyed` rejecting unbound callers +- the two documented limitations above (`H-1` fail-open, `F-2` zero-address whitelist), pinned so a change in + behaviour is noticed + +## 6. Related documents + +- [RuleEngine-with-CMTAT.md](./RuleEngine-with-CMTAT.md) — the CMTAT counterpart +- [../README.md](../README.md) — full interface and API reference +- [CLAUDE_ANALYSIS.md](../security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md) — code-quality review, findings `H-1` and `F-2` +- Production rules: [github.com/CMTA/Rules](https://github.com/CMTA/Rules) diff --git a/foundry.toml b/foundry.toml index 6c528c9..6aff270 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -solc = "0.8.34" +solc = "0.8.36" src = 'src' out = 'out' libs = ['lib'] diff --git a/hardhat.config.js b/hardhat.config.js index 38cd154..302db33 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -3,7 +3,7 @@ require("@nomicfoundation/hardhat-toolbox"); require("@nomicfoundation/hardhat-foundry"); module.exports = { solidity: { - version: "0.8.34", + version: "0.8.36", settings: { optimizer: { enabled: true, diff --git a/lib/CMTAT b/lib/CMTAT index 580d477..658672f 160000 --- a/lib/CMTAT +++ b/lib/CMTAT @@ -1 +1 @@ -Subproject commit 580d4776e4cbb857b2da7d83fd79144ae7e47557 +Subproject commit 658672f190d56d3f61663a7d6d51962b8980df70 diff --git a/lib/ERC-3643 b/lib/ERC-3643 new file mode 160000 index 0000000..dab1660 --- /dev/null +++ b/lib/ERC-3643 @@ -0,0 +1 @@ +Subproject commit dab1660fe594e17e83d691137ba67272534732ac diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 5fd1781..cab1993 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 5fd1781b1454fd1ef8e722282f86f9293cacf256 +Subproject commit cab19933c33c2ad1d4c7a84864a3601dddfd16f3 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index 7bf4727..14f52c5 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 7bf4727aacdbfaa0f36cbd664654d0c9e1dc52bf +Subproject commit 14f52c54d3a1eefbda3d4071efba24d3c1e07e8a diff --git a/package-lock.json b/package-lock.json index 1ecc425..850ed90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "RuleEngineNew", "devDependencies": { "@nomicfoundation/hardhat-foundry": "^1.2.1", "@nomicfoundation/hardhat-toolbox": "^6.1.2", @@ -12,9 +13,9 @@ } }, "node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "dev": true, "license": "MIT", "peer": true @@ -563,7 +564,6 @@ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -582,7 +582,6 @@ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -596,7 +595,6 @@ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -609,8 +607,7 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", @@ -618,7 +615,6 @@ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -637,7 +633,6 @@ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^6.2.2" }, @@ -654,7 +649,6 @@ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1284,7 +1278,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=14" } @@ -1848,6 +1841,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "peer": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1862,6 +1856,7 @@ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8.6" }, @@ -1972,15 +1967,16 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, @@ -2006,6 +2002,7 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "peer": true, "engines": { "node": ">=8" }, @@ -2063,9 +2060,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -2077,6 +2074,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "peer": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -2407,6 +2405,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, + "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -2680,7 +2679,6 @@ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2696,7 +2694,6 @@ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -2831,6 +2828,7 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, + "peer": true, "engines": { "node": ">=0.3.1" } @@ -2883,8 +2881,7 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/elliptic": { "version": "6.6.1", @@ -3165,9 +3162,9 @@ } }, "node_modules/ethers": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", - "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz", + "integrity": "sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==", "dev": true, "funding": [ { @@ -3182,13 +3179,13 @@ "license": "MIT", "peer": true, "dependencies": { - "@adraffy/ens-normalize": "1.10.1", + "@adraffy/ens-normalize": "1.11.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", "tslib": "2.7.0", - "ws": "8.17.1" + "ws": "8.21.0" }, "engines": { "node": ">=14.0.0" @@ -3250,9 +3247,9 @@ "peer": true }, "node_modules/ethers/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "peer": true, @@ -3351,9 +3348,9 @@ "peer": true }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, "funding": [ { @@ -3401,6 +3398,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "peer": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3448,9 +3446,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -3458,6 +3456,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -3490,7 +3489,6 @@ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" @@ -3503,9 +3501,9 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "peer": true, @@ -3513,8 +3511,8 @@ "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -3544,7 +3542,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -3556,6 +3555,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -3738,6 +3738,7 @@ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3757,6 +3758,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "peer": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -3816,9 +3818,9 @@ } }, "node_modules/globby/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "peer": true, @@ -3920,9 +3922,9 @@ } }, "node_modules/hardhat": { - "version": "2.28.6", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.28.6.tgz", - "integrity": "sha512-zQze7qe+8ltwHvhX5NQ8sN1N37WWZGw8L63y+2XcPxGwAjc/SMF829z3NS6o1krX0sryhAsVBK/xrwUqlsot4Q==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.29.0.tgz", + "integrity": "sha512-tsj5mCSjDCFOhGfBl4vwqDEcwdlES9VUzRWfdrwvEVhus6D8W6u+WfUKRLLwFhKGS/8lKPoXGsjYWPXl3CCpOg==", "dev": true, "license": "MIT", "dependencies": { @@ -3951,7 +3953,7 @@ "lodash": "^4.17.11", "micro-eth-signer": "^0.14.0", "mnemonist": "^0.38.0", - "mocha": "^10.0.0", + "mocha": "^11.1.0", "p-map": "^4.0.0", "picocolors": "^1.1.0", "raw-body": "^2.4.1", @@ -4131,6 +4133,158 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/hardhat/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/hardhat/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/hardhat/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/hardhat/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/hardhat/node_modules/mocha": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.8.0.tgz", + "integrity": "sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/hardhat/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/hardhat/node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/hardhat/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/hardhat/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4290,9 +4444,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "peer": true, @@ -4400,9 +4554,9 @@ } }, "node_modules/immutable": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", - "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz", + "integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==", "dev": true, "license": "MIT" }, @@ -4421,6 +4575,7 @@ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, + "peer": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -4465,6 +4620,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "peer": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -4491,6 +4647,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4509,6 +4666,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "peer": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -4533,10 +4691,21 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "peer": true, "engines": { "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -4600,8 +4769,7 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/isows": { "version": "1.0.7", @@ -4626,7 +4794,6 @@ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -4644,10 +4811,21 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -4859,8 +5037,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/make-error": { "version": "1.3.6", @@ -5053,6 +5230,7 @@ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -5077,7 +5255,6 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -5110,6 +5287,7 @@ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, + "peer": true, "dependencies": { "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", @@ -5145,6 +5323,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -5170,6 +5349,7 @@ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8.6" }, @@ -5182,6 +5362,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "peer": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -5194,6 +5375,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -5297,6 +5479,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5336,6 +5519,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "peer": true, "dependencies": { "wrappy": "1" } @@ -5377,9 +5561,9 @@ } }, "node_modules/ox": { - "version": "0.14.15", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.15.tgz", - "integrity": "sha512-3TubCmbKen/cuZQzX0qDbOS5lojjdSZ90lqKxWIDWd5siuJ0IJBaTXMYs8eMPLcraqnOwGZazz3apHPGiRCkGQ==", + "version": "0.14.33", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.33.tgz", + "integrity": "sha512-rooA/4o7bBof4Ge2VH/eovfNPb/AEEYyrNj03wggc55g5HZD8Pjs/OeWhttgjic3dDcqn0r29bDuvQEdTiUemQ==", "dev": true, "funding": [ { @@ -5408,14 +5592,6 @@ } } }, - "node_modules/ox/node_modules/@adraffy/ens-normalize": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", - "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/ox/node_modules/@noble/curves": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", @@ -5539,8 +5715,7 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, - "license": "BlueOak-1.0.0", - "peer": true + "license": "BlueOak-1.0.0" }, "node_modules/path-exists": { "version": "4.0.0", @@ -5568,7 +5743,6 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -5585,7 +5759,6 @@ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -5841,9 +6014,9 @@ } }, "node_modules/recursive-readdir/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "peer": true, @@ -6051,9 +6224,9 @@ } }, "node_modules/sc-istanbul/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "peer": true, @@ -6093,9 +6266,9 @@ } }, "node_modules/sc-istanbul/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, "license": "MIT", "peer": true, @@ -6167,9 +6340,9 @@ "peer": true }, "node_modules/secp256k1": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.4.tgz", - "integrity": "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.5.tgz", + "integrity": "sha512-SQZi5+/uiJIFPYbeRrVuu77Sr3bFOTq0oCQs67CqYwdmg0lhnqi/8djSWhzNO3GKGOqxBYCdx8zJJv0zUwDDvw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6297,7 +6470,6 @@ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -6311,7 +6483,6 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -6336,9 +6507,9 @@ } }, "node_modules/shelljs/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "peer": true, @@ -6390,7 +6561,6 @@ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=14" }, @@ -6730,7 +6900,6 @@ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -6759,7 +6928,6 @@ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6991,6 +7159,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "peer": true, "dependencies": { "is-number": "^7.0.0" }, @@ -7176,9 +7345,9 @@ } }, "node_modules/typechain/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "peer": true, @@ -7365,9 +7534,9 @@ "peer": true }, "node_modules/viem": { - "version": "2.47.16", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.47.16.tgz", - "integrity": "sha512-IHkMi65tXLFSz81V2wtOXfRmznSvU0ANOPMRMgBcTwTPWMC+TcUSFFLvcl0UKpj+omFe+54lIcyAwK0I5jcvSw==", + "version": "2.55.15", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.55.15.tgz", + "integrity": "sha512-ka9SfSJ3ZfhuUEzTGmufwALRvEPVKM068tF0AwfdRZagqA3yuFa/QoXIvALzr9y47m4Wiisl3yEoYWuFQso6Ng==", "dev": true, "funding": [ { @@ -7384,8 +7553,8 @@ "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", - "ox": "0.14.15", - "ws": "8.18.3" + "ox": "0.14.33", + "ws": "8.21.0" }, "peerDependencies": { "typescript": ">=5.0.4" @@ -7470,9 +7639,9 @@ } }, "node_modules/viem/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "peer": true, @@ -7714,7 +7883,8 @@ "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/wrap-ansi": { "version": "7.0.0", @@ -7740,7 +7910,6 @@ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -7757,13 +7926,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.3.0" }, @@ -7794,6 +7965,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, + "peer": true, "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -7812,6 +7984,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "peer": true, "engines": { "node": ">=10" } diff --git a/package.json b/package.json index ee734de..302bb1d 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "scripts": { "test:hardhat": "npx hardhat test test/hardhat/RuleEngine.smoke.js", - "uml": "npx sol2uml class src", - "uml:ruleEngine": "npx sol2uml class src/deployment/RuleEngine.sol", - "uml:test": "npx sol2uml class test", - "surya:report": "npx surya mdreport surya_report_ruleEngine.md src/deployment/RuleEngine.sol", - "surya:graph": "npx surya graph src/deployment/RuleEngine.sol | dot -Tpng > surya_graph_RuleEngine.png" + "uml": "mkdir -p docOut/uml && npx sol2uml class src -o docOut/uml/uml_src.svg", + "uml:ruleEngine": "mkdir -p docOut/uml && npx sol2uml class src/deployment/RuleEngine.sol -o docOut/uml/uml_RuleEngine.svg", + "uml:test": "mkdir -p docOut/uml && npx sol2uml class test -o docOut/uml/uml_test.svg", + "surya:report": "mkdir -p docOut/npm && npx surya mdreport docOut/npm/surya_report_ruleEngine.md src/deployment/RuleEngine.sol", + "surya:graph": "mkdir -p docOut/npm && npx surya graph src/deployment/RuleEngine.sol | dot -Tpng > docOut/npm/surya_graph_RuleEngine.png" }, "devDependencies": { "@nomicfoundation/hardhat-foundry": "^1.2.1", diff --git a/script/CMTATWithRuleEngineScript.s.sol b/script/CMTATWithRuleEngineScript.s.sol index 377a982..782fdd1 100644 --- a/script/CMTATWithRuleEngineScript.s.sol +++ b/script/CMTATWithRuleEngineScript.s.sol @@ -9,10 +9,10 @@ import {ICMTATConstructor, CMTATStandardStandalone} from "CMTAT/deployment/CMTAT import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; import {RuleEngine} from "src/deployment/RuleEngine.sol"; -import {RuleWhitelist} from "src/mocks/rules/validation/RuleWhitelist.sol"; +import {RuleWhitelistMock} from "src/mocks/rules/validation/RuleWhitelistMock.sol"; /** - * @title Example deployment of a CMTAT, a mock RuleWhitelist and a RuleEngine + * @title Example deployment of a CMTAT, a mock RuleWhitelistMock and a RuleEngine * @dev This script deploys a reference/mock rule from `src/mocks/` for demo and testing flows. * It is not a production deployment recipe for rule contracts. */ @@ -39,7 +39,7 @@ contract CMTATWithRuleEngineScript is Script { new CMTATStandardStandalone(trustedForwarder, admin, erc20Attributes, extraInformationAttributes, engines); console.log("CMTAT cmtatContract : ", address(cmtatContract)); // whitelist - RuleWhitelist ruleWhitelist = new RuleWhitelist(admin, trustedForwarder); + RuleWhitelistMock ruleWhitelist = new RuleWhitelistMock(admin, trustedForwarder); console.log("whitelist: ", address(ruleWhitelist)); // ruleEngine RuleEngine ruleEngine = new RuleEngine(admin, trustedForwarder, address(cmtatContract)); diff --git a/script/RuleEngineScript.s.sol b/script/RuleEngineScript.s.sol index 30ddb41..9ea7086 100644 --- a/script/RuleEngineScript.s.sol +++ b/script/RuleEngineScript.s.sol @@ -6,15 +6,27 @@ pragma solidity ^0.8.20; import {Script, console} from "forge-std/Script.sol"; import {RuleEngine} from "src/deployment/RuleEngine.sol"; -import {RuleWhitelist} from "src/mocks/rules/validation/RuleWhitelist.sol"; +import {RuleWhitelistMock} from "src/mocks/rules/validation/RuleWhitelistMock.sol"; +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; import { ValidationModuleRuleEngine } from "CMTAT/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol"; /** - * @title Example deployment of a mock RuleWhitelist and a RuleEngine + * @title Example deployment of a mock RuleWhitelistMock and a RuleEngine * @dev This script deploys a reference/mock rule from `src/mocks/` for demo and testing flows. * It is not a production deployment recipe for rule contracts. + * + * Expects an already-deployed CMTAT at `CMTAT_ADDRESS`. The deployer must hold `DEFAULT_ADMIN_ROLE` + * on that token, otherwise {setRuleEngine} reverts. + * + * The token is bound to the engine through the constructor: without it, every transfer, mint and burn + * reverts with `RuleEngine_ERC3643Compliance_UnauthorizedCaller`, because the compliance callbacks are + * guarded by `onlyBoundToken`. + * + * The deployer and the zero address are added to the whitelist so the resulting deployment is usable + * as-is: the zero address is required for mint and burn, since the rule treats it as an ordinary + * participant. Replace this with the real address list for anything beyond a demo. */ contract RuleEngineScript is Script { function run() external { @@ -24,16 +36,21 @@ contract RuleEngineScript is Script { address cmtatAddress = vm.envAddress("CMTAT_ADDRESS"); vm.startBroadcast(deployerPrivateKey); //whitelist - RuleWhitelist ruleWhitelist = new RuleWhitelist(admin, address(0)); + RuleWhitelistMock ruleWhitelist = new RuleWhitelistMock(admin, address(0)); console.log("whitelist: ", address(ruleWhitelist)); - // ruleEngine - RuleEngine ruleEngine = new RuleEngine(admin, address(0), address(0)); + // Seed the list so the demo deployment can actually transfer, mint and burn. + address[] memory listed = new address[](2); + listed[0] = admin; + listed[1] = address(0); + ruleWhitelist.addAddressesToTheList(listed); + // ruleEngine, bound to the CMTAT token + RuleEngine ruleEngine = new RuleEngine(admin, address(0), cmtatAddress); console.log("RuleEngine: ", address(ruleEngine)); ruleEngine.addRule(ruleWhitelist); - // Configure the new ruleEngine for CMTAT - (bool success,) = - address(cmtatAddress).call(abi.encodeCall(ValidationModuleRuleEngine.setRuleEngine, ruleEngine)); - require(success); + // Configure the new ruleEngine for CMTAT. + // A typed call is used deliberately: a low-level `.call` would return success even when + // `cmtatAddress` holds no code, silently producing an unconfigured deployment. + ValidationModuleRuleEngine(cmtatAddress).setRuleEngine(IRuleEngine(address(ruleEngine))); vm.stopBroadcast(); } } diff --git a/src/RuleEngineBase.sol b/src/RuleEngineBase.sol index 08d8219..bbf9be8 100644 --- a/src/RuleEngineBase.sol +++ b/src/RuleEngineBase.sol @@ -36,8 +36,18 @@ abstract contract RuleEngineBase is RuleEngineInvariantStorage, IRuleEngineERC1404 { + /* ============ State variables ============ */ + /** + * @dev ERC-1404 reserves the code 0 as the "no restriction" sentinel. It is never claimed by a rule, + * so it is answered here instead of being reported as an unknown code. + * The message matches the one returned by CMTAT (ValidationModuleERC1404) for the same code. + */ + string private constant TEXT_TRANSFER_OK = "NoRestriction"; + /// @dev Returned when no active rule claims the restriction code + string private constant TEXT_CODE_NOT_FOUND = "Unknown restriction code"; + /* ============ State functions ============ */ - /* + /** * @inheritdoc IRuleEngine */ function transferred(address spender, address from, address to, uint256 value) @@ -146,6 +156,13 @@ abstract contract RuleEngineBase is /*////////////////////////////////////////////////////////////// INTERNAL/PRIVATE FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the first non-zero restriction code reported by the configured rules. + * @param from the origin address + * @param to the destination address + * @param value the amount to transfer + * @return The first non-zero ERC-1404 restriction code, or TRANSFER_OK when every rule allows it. + */ function _detectTransferRestriction(address from, address to, uint256 value) internal view virtual returns (uint8) { uint256 rulesLength = rulesCount(); for (uint256 i = 0; i < rulesLength; ++i) { @@ -157,6 +174,14 @@ abstract contract RuleEngineBase is return uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Returns the first non-zero restriction code for a spender-initiated transfer. + * @param spender the spender address (transferFrom) + * @param from the origin address + * @param to the destination address + * @param value the amount to transfer + * @return The first non-zero ERC-1404 restriction code, or TRANSFER_OK when every rule allows it. + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -178,19 +203,27 @@ abstract contract RuleEngineBase is * Rule designers should keep restriction codes unique across rules. * If a code is shared intentionally, all rules using that code should return * the same message to avoid ambiguous operator feedback. + * The reserved code 0 (REJECTED_CODE_BASE.TRANSFER_OK) is answered before the rules are queried, + * so that a valid transfer is never reported as an unknown restriction code. + * @param restrictionCode The target restriction code. + * @return The message of the first rule claiming the code, or a default message when none does. */ function _messageForTransferRestriction(uint8 restrictionCode) internal view virtual returns (string memory) { + if (restrictionCode == uint8(REJECTED_CODE_BASE.TRANSFER_OK)) { + return TEXT_TRANSFER_OK; + } uint256 rulesLength = rulesCount(); for (uint256 i = 0; i < rulesLength; ++i) { if (IRule(rule(i)).canReturnTransferRestrictionCode(restrictionCode)) { return IRule(rule(i)).messageForTransferRestriction(restrictionCode); } } - return "Unknown restriction code"; + return TEXT_CODE_NOT_FOUND; } /** * @dev Override to add ERC-165 interface check for the full IRule hierarchy. + * @param rule_ The candidate rule address to validate. */ function _checkRule(address rule_) internal view virtual override { RulesManagementModule._checkRule(rule_); @@ -202,8 +235,10 @@ abstract contract RuleEngineBase is /** * @dev Shared ERC-165 checks common to all RuleEngine deployment variants. * Concrete deployments can extend this with access-control-specific interfaces. + * @param interfaceId The interface identifier to check. + * @return True if the interface is part of the shared RuleEngine base, false otherwise. */ - function _supportsRuleEngineBaseInterface(bytes4 interfaceId) internal pure returns (bool) { + function _supportsRuleEngineBaseInterface(bytes4 interfaceId) internal pure virtual returns (bool) { return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID || interfaceId == ERC1404InterfaceId.IERC1404_INTERFACE_ID || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID diff --git a/src/RuleEngineOwnableShared.sol b/src/RuleEngineOwnableShared.sol index e4d2ab5..c29f35b 100644 --- a/src/RuleEngineOwnableShared.sol +++ b/src/RuleEngineOwnableShared.sol @@ -19,13 +19,25 @@ import {IRule} from "./interfaces/IRule.sol"; * (`Ownable` or `Ownable2Step`) while reusing constructor, ERC-165 and ERC-2771 code. */ abstract contract RuleEngineOwnableShared is ERC2771ModuleStandalone, RuleEngineBase, ERC165 { + /** + * @notice Sets the trusted forwarder and optionally binds an initial token. + * @param forwarderIrrevocable Address of the trusted ERC-2771 forwarder, immutable after construction. + * @param tokenContract Token to bind at deployment, or the zero address to bind none. + */ constructor(address forwarderIrrevocable, address tokenContract) ERC2771ModuleStandalone(forwarderIrrevocable) { if (tokenContract != address(0)) { _bindToken(tokenContract); } + // Emit the initial cap so the event log alone is enough to reconstruct maxRules. + _setMaxRules(DEFAULT_MAX_RULES); } /* ============ ERC-165 ============ */ + /** + * @notice ERC-165 interface detection. + * @param interfaceId The interface identifier to check. + * @return True if the interface is supported, false otherwise. + */ function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { return _supportsRuleEngineBaseInterface(interfaceId) || interfaceId == OwnableInterfaceId.IERC173_INTERFACE_ID || ERC165.supportsInterface(interfaceId); @@ -33,6 +45,7 @@ abstract contract RuleEngineOwnableShared is ERC2771ModuleStandalone, RuleEngine /** * @dev Shared guard for ownership transfer targets in ownable variants. + * @param newOwner The candidate new owner; must not be a configured rule. */ function _checkOwnershipTransferTarget(address newOwner) internal view virtual { if (containsRule(IRule(newOwner))) { @@ -46,6 +59,7 @@ abstract contract RuleEngineOwnableShared is ERC2771ModuleStandalone, RuleEngine /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return sender The transaction sender, unwrapped from the forwarder calldata when relayed. */ function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { return ERC2771Context._msgSender(); @@ -53,6 +67,7 @@ abstract contract RuleEngineOwnableShared is ERC2771ModuleStandalone, RuleEngine /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The transaction calldata, with the appended sender stripped when relayed. */ function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { return ERC2771Context._msgData(); @@ -60,6 +75,7 @@ abstract contract RuleEngineOwnableShared is ERC2771ModuleStandalone, RuleEngine /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The length of the ERC-2771 calldata suffix. */ function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); diff --git a/src/deployment/RuleEngine.sol b/src/deployment/RuleEngine.sol index 8cd4fff..bcaf7d5 100644 --- a/src/deployment/RuleEngine.sol +++ b/src/deployment/RuleEngine.sol @@ -29,8 +29,10 @@ contract RuleEngine is using EnumerableSet for EnumerableSet.AddressSet; /** + * @notice Deploys the RBAC RuleEngine. * @param admin Address of the contract (Access Control) * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + * @param tokenContract Token to bind at deployment, or the zero address to bind none. */ constructor(address admin, address forwarderIrrevocable, address tokenContract) ERC2771ModuleStandalone(forwarderIrrevocable) @@ -42,6 +44,8 @@ contract RuleEngine is _bindToken(tokenContract); } _grantRole(DEFAULT_ADMIN_ROLE, admin); + // Emit the initial cap so the event log alone is enough to reconstruct maxRules. + _setMaxRules(DEFAULT_MAX_RULES); } /* ============ ACCESS CONTROL ============ */ @@ -52,6 +56,8 @@ contract RuleEngine is * whether the rule address already holds a privileged role, and this function does * not prevent adding a privileged address as a rule afterwards. Operators are * responsible for keeping rule contracts and privileged accounts disjoint. + * @param role The role identifier to grant. + * @param account The account receiving the role. */ function grantRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { if (_rules.contains(account)) { @@ -63,6 +69,9 @@ contract RuleEngine is /** * @notice Returns `true` if `account` has been granted `role`. * @dev The Default Admin has all roles + * @param role The role identifier to check. + * @param account The account to check. + * @return True if the account holds the role (or is the default admin), false otherwise. */ function hasRole(bytes32 role, address account) public @@ -79,6 +88,11 @@ contract RuleEngine is } /* ============ ERC-165 ============ */ + /** + * @notice ERC-165 interface detection. + * @param interfaceId The interface identifier to check. + * @return True if the interface is supported, false otherwise. + */ function supportsInterface(bytes4 interfaceId) public view @@ -92,12 +106,24 @@ contract RuleEngine is /*////////////////////////////////////////////////////////////// ERC-2771 //////////////////////////////////////////////////////////////*/ + /** + * @dev Access control check restricting compliance operations to COMPLIANCE_MANAGER_ROLE. + */ function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + + /** + * @dev Access control check restricting rule management to RULES_MANAGEMENT_ROLE. + */ function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + + /** + * @dev Access control check restricting the rule cap update to DEFAULT_ADMIN_ROLE. + */ function _onlyRulesLimitManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return sender The transaction sender, unwrapped from the forwarder calldata when relayed. */ function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { return ERC2771Context._msgSender(); @@ -105,6 +131,7 @@ contract RuleEngine is /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The transaction calldata, with the appended sender stripped when relayed. */ function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { return ERC2771Context._msgData(); @@ -112,6 +139,7 @@ contract RuleEngine is /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The length of the ERC-2771 calldata suffix. */ function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); diff --git a/src/deployment/RuleEngineOwnable.sol b/src/deployment/RuleEngineOwnable.sol index 391c066..e81ac60 100644 --- a/src/deployment/RuleEngineOwnable.sol +++ b/src/deployment/RuleEngineOwnable.sol @@ -20,29 +20,35 @@ contract RuleEngineOwnable is RuleEngineOwnableShared, Ownable { Ownable(owner_) {} + /** + * @notice Transfers ownership of the contract to a new account (`newOwner`). + * @dev Reverts when `newOwner` is already configured as a rule. + * @param newOwner The address of the new owner. + */ + function transferOwnership(address newOwner) public virtual override onlyOwner { + RuleEngineOwnableShared._checkOwnershipTransferTarget(newOwner); + Ownable.transferOwnership(newOwner); + } + /* ============ ACCESS CONTROL ============ */ /** * @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 {} + function _onlyRulesLimitManager() 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. + * @dev Access control check using Ownable pattern */ - function transferOwnership(address newOwner) public virtual override onlyOwner { - RuleEngineOwnableShared._checkOwnershipTransferTarget(newOwner); - Ownable.transferOwnership(newOwner); - } + function _onlyComplianceManager() internal virtual override onlyOwner {} /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return sender The transaction sender, unwrapped from the forwarder calldata when relayed. */ function _msgSender() internal view virtual override(RuleEngineOwnableShared, Context) returns (address sender) { return RuleEngineOwnableShared._msgSender(); @@ -50,6 +56,7 @@ contract RuleEngineOwnable is RuleEngineOwnableShared, Ownable { /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The transaction calldata, with the appended sender stripped when relayed. */ function _msgData() internal view virtual override(RuleEngineOwnableShared, Context) returns (bytes calldata) { return RuleEngineOwnableShared._msgData(); @@ -57,6 +64,7 @@ contract RuleEngineOwnable is RuleEngineOwnableShared, Ownable { /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The length of the ERC-2771 calldata suffix. */ 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 29f0ff4..d334ec2 100644 --- a/src/deployment/RuleEngineOwnable2Step.sol +++ b/src/deployment/RuleEngineOwnable2Step.sol @@ -24,21 +24,10 @@ contract RuleEngineOwnable2Step is RuleEngineOwnableShared, Ownable2Step { Ownable(owner_) {} - /* ============ ACCESS CONTROL ============ */ - /** - * @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. + * @param newOwner The address of the new owner. */ function transferOwnership(address newOwner) public virtual override onlyOwner { RuleEngineOwnableShared._checkOwnershipTransferTarget(newOwner); @@ -46,13 +35,41 @@ contract RuleEngineOwnable2Step is RuleEngineOwnableShared, Ownable2Step { } /* ============ ERC-165 ============ */ - function supportsInterface(bytes4 interfaceId) public view virtual override(RuleEngineOwnableShared) returns (bool) { + /** + * @notice ERC-165 interface detection. + * @param interfaceId The interface identifier to check. + * @return True if the interface is supported, false otherwise. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleEngineOwnableShared) + returns (bool) + { return interfaceId == Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID || RuleEngineOwnableShared.supportsInterface(interfaceId); } + /* ============ ACCESS CONTROL ============ */ + /** + * @dev Access control check using Ownable pattern + */ + function _onlyRulesManager() internal virtual override onlyOwner {} + + /** + * @dev Access control check using Ownable pattern + */ + function _onlyRulesLimitManager() internal virtual override onlyOwner {} + + /** + * @dev Access control check using Ownable pattern + */ + function _onlyComplianceManager() internal virtual override onlyOwner {} + /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return sender The transaction sender, unwrapped from the forwarder calldata when relayed. */ function _msgSender() internal view virtual override(RuleEngineOwnableShared, Context) returns (address sender) { return RuleEngineOwnableShared._msgSender(); @@ -60,6 +77,7 @@ contract RuleEngineOwnable2Step is RuleEngineOwnableShared, Ownable2Step { /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The transaction calldata, with the appended sender stripped when relayed. */ function _msgData() internal view virtual override(RuleEngineOwnableShared, Context) returns (bytes calldata) { return RuleEngineOwnableShared._msgData(); @@ -67,6 +85,7 @@ contract RuleEngineOwnable2Step is RuleEngineOwnableShared, Ownable2Step { /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The length of the ERC-2771 calldata suffix. */ function _contextSuffixLength() internal view virtual override(RuleEngineOwnableShared, Context) returns (uint256) { return RuleEngineOwnableShared._contextSuffixLength(); diff --git a/src/interfaces/IERC3643Compliance.sol b/src/interfaces/IERC3643Compliance.sol index cc7c350..6e98898 100644 --- a/src/interfaces/IERC3643Compliance.sol +++ b/src/interfaces/IERC3643Compliance.sol @@ -5,6 +5,10 @@ pragma solidity ^0.8.20; /* ==== CMTAT === */ import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; +/** + * @title IERC3643Compliance + * @notice Compliance interface implemented by the RuleEngine for ERC-3643 tokens. + */ interface IERC3643Compliance is IERC3643ComplianceRead, IERC3643IComplianceContract { /* ============ Events ============ */ /** @@ -47,6 +51,23 @@ interface IERC3643Compliance is IERC3643ComplianceRead, IERC3643IComplianceContr */ function unbindToken(address token) external; + /** + * @notice Updates the compliance contract state when tokens are created (minted). + * @dev Called by the token contract when new tokens are issued to an account. + * Reverts if the minting does not comply with the rules. + * @param to The address receiving the minted tokens. + * @param value The number of tokens created. + */ + function created(address to, uint256 value) external; + + /** + * @notice Updates the compliance contract state when tokens are destroyed (burned). + * @dev Called by the token contract when tokens are redeemed or burned. + * Reverts if the burning does not comply with the rules. + * @param from The address whose tokens are being destroyed. + * @param value The number of tokens destroyed. + */ + function destroyed(address from, uint256 value) external; /** * @notice Checks whether a token is currently bound to this compliance contract. @@ -65,22 +86,4 @@ interface IERC3643Compliance is IERC3643ComplianceRead, IERC3643IComplianceContr * @return token The address of the currently bound token. */ function getTokenBound() external view returns (address token); - - /** - * @notice Updates the compliance contract state when tokens are created (minted). - * @dev Called by the token contract when new tokens are issued to an account. - * Reverts if the minting does not comply with the rules. - * @param to The address receiving the minted tokens. - * @param value The number of tokens created. - */ - function created(address to, uint256 value) external; - - /** - * @notice Updates the compliance contract state when tokens are destroyed (burned). - * @dev Called by the token contract when tokens are redeemed or burned. - * Reverts if the burning does not comply with the rules. - * @param from The address whose tokens are being destroyed. - * @param value The number of tokens destroyed. - */ - function destroyed(address from, uint256 value) external; } diff --git a/src/interfaces/IERC3643ComplianceExtended.sol b/src/interfaces/IERC3643ComplianceExtended.sol index 7f334b0..bd72577 100644 --- a/src/interfaces/IERC3643ComplianceExtended.sol +++ b/src/interfaces/IERC3643ComplianceExtended.sol @@ -4,6 +4,10 @@ pragma solidity ^0.8.20; import {IERC3643Compliance} from "./IERC3643Compliance.sol"; +/** + * @title IERC3643ComplianceExtended + * @notice Extends the ERC-3643 compliance interface with token self-binding management. + */ interface IERC3643ComplianceExtended is IERC3643Compliance { /** * @notice Emitted when self-binding permission is updated for a token. diff --git a/src/interfaces/IRule.sol b/src/interfaces/IRule.sol index 86554b9..647da17 100644 --- a/src/interfaces/IRule.sol +++ b/src/interfaces/IRule.sol @@ -7,12 +7,19 @@ import {IRuleEngineERC1404} from "CMTAT/interfaces/engine/IRuleEngine.sol"; /* ==== Interfaces === */ +/** + * @title IRule + * @notice Interface every rule must implement to be usable by a RuleEngine. + */ interface IRule is IRuleEngineERC1404 { /** + * @notice Tells whether a restriction code belongs to this rule. * @dev Returns true if the restriction code exists, and false otherwise. * Rule authors should use unique restriction codes across rules when possible. * If a code is intentionally shared by multiple rules, all of them should return * the same message for that code in `messageForTransferRestriction`. + * @param restrictionCode The target restriction code. + * @return True if the restriction code is known by this rule, false otherwise. */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external view returns (bool); } diff --git a/src/interfaces/IRulesManagementModule.sol b/src/interfaces/IRulesManagementModule.sol index 823dbae..4c6866b 100644 --- a/src/interfaces/IRulesManagementModule.sol +++ b/src/interfaces/IRulesManagementModule.sol @@ -5,12 +5,11 @@ pragma solidity ^0.8.20; /* ==== Interfaces === */ import {IRule} from "./IRule.sol"; +/** + * @title IRulesManagementModule + * @notice Rule CRUD operations exposed by the RuleEngine. + */ 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). @@ -31,6 +30,37 @@ interface IRulesManagementModule { */ function setRules(IRule[] calldata rules_) external; + /** + * @notice Removes all configured rules. + * @dev After calling this function, no rules will remain set. + * Cost is O(n) in the number of configured rules. With the default cap of 10 this is + * negligible, but a high {maxRules} setting re-exposes unbounded cost here even though + * per-transfer cost remains bounded. + */ + function clearRules() external; + + /** + * @notice Adds a new rule to the current rule set. + * @dev Reverts if the rule address is zero or already exists in the set. + * Complexity: O(1). + * @param rule_ The IRule contract to add. + */ + function addRule(IRule rule_) external; + + /** + * @notice Removes a specific rule from the current rule set. + * @dev Reverts if the provided rule is not found or does not match the stored rule at its index. + * Complexity: O(1). + * @param rule_ The IRule contract to remove. + */ + function removeRule(IRule rule_) external; + + /** + * @notice Returns the maximum number of rules allowed in the engine. + * @return The maximum number of rules that can be configured. + */ + function maxRules() external view returns (uint256); + /** * @notice Returns the total number of currently configured rules. * @dev Equivalent to the length of the internal rules array. @@ -59,31 +89,6 @@ interface IRulesManagementModule { */ function rules() external view returns (address[] memory ruleAddresses); - /** - * @notice Removes all configured rules. - * @dev After calling this function, no rules will remain set. - * Cost is O(n) in the number of configured rules. With the default cap of 10 this is - * negligible, but a high {maxRules} setting re-exposes unbounded cost here even though - * per-transfer cost remains bounded. - */ - function clearRules() external; - - /** - * @notice Adds a new rule to the current rule set. - * @dev Reverts if the rule address is zero or already exists in the set. - * Complexity: O(1). - * @param rule_ The IRule contract to add. - */ - function addRule(IRule rule_) external; - - /** - * @notice Removes a specific rule from the current rule set. - * @dev Reverts if the provided rule is not found or does not match the stored rule at its index. - * Complexity: O(1). - * @param rule_ The IRule contract to remove. - */ - function removeRule(IRule rule_) external; - /** * @notice Checks whether a specific rule is currently configured. * @param rule_ The IRule contract to check for membership. diff --git a/src/mocks/ERC3643TokenMock.sol b/src/mocks/ERC3643TokenMock.sol new file mode 100644 index 0000000..4af841d --- /dev/null +++ b/src/mocks/ERC3643TokenMock.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/* ==== Interface and other library === */ +import {IERC3643Compliance} from "../interfaces/IERC3643Compliance.sol"; + +/** + * @title ERC3643TokenMock + * @notice Minimal ERC-3643 (T-REX) style token that drives a compliance contract exactly as the + * reference implementation does, used to test the RuleEngine through the ERC-3643 entry points. + * @dev The compliance interaction is modelled on `Token.sol` from the ERC-3643 reference + * implementation (submodule `lib/ERC-3643`, tag 4.1.3): + * + * - `setCompliance` unbinds the previous compliance, then binds itself to the new one + * - `transfer` calls `canTransfer(msg.sender, to, amount)` then `transferred(msg.sender, to, amount)` + * - `mint` calls `canTransfer(address(0), to, amount)` then `created(to, amount)` + * - `burn` calls `destroyed(from, amount)` + * + * NOTE: the reference token itself cannot be compiled into this test suite. It pins + * `pragma solidity 0.8.17` (this project compiles with 0.8.36), depends on OpenZeppelin 4.8.x + * (this project uses 5.7.0) and on the `onchain-id/solidity` package, which is not installed here. + * This mock therefore reproduces the compliance-facing behaviour rather than vendoring the token. + * + * ERC-3643 compliance callbacks carry no spender, so this token never reaches the 4-argument + * `transferred(spender, from, to, value)` overload. + */ +contract ERC3643TokenMock { + /* ==== Errors === */ + error ERC3643TokenMock_ComplianceNotFollowed(); + error ERC3643TokenMock_InsufficientBalance(); + + /* ==== Events === */ + /** + * @notice Emitted when the compliance contract is changed. + * @param compliance The address of the new compliance contract. + */ + event ComplianceAdded(address indexed compliance); + + /* ==== State Variables === */ + /** + * @notice Token balances. + */ + mapping(address account => uint256 balance) public balanceOf; + + /** + * @notice The compliance contract currently bound to this token. + */ + IERC3643Compliance public compliance; + + /*////////////////////////////////////////////////////////////// + PUBLIC/EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Binds this token to a compliance contract, unbinding the previous one. + * @dev Mirrors `Token.setCompliance` in the ERC-3643 reference implementation: the token + * unbinds itself from the old compliance and binds itself to the new one. Both calls are + * made by the token, so the compliance contract must have granted this token self-binding + * approval beforehand. + * @param compliance_ The address of the new compliance contract. + */ + function setCompliance(address compliance_) public virtual { + if (address(compliance) != address(0)) { + compliance.unbindToken(address(this)); + } + compliance = IERC3643Compliance(compliance_); + compliance.bindToken(address(this)); + emit ComplianceAdded(compliance_); + } + + /** + * @notice Transfers tokens, applying the compliance rules. + * @dev Mirrors `Token.transfer`: pre-checks with `canTransfer` then notifies with `transferred`. + * @param to The destination address. + * @param amount The number of tokens to transfer. + */ + function transfer(address to, uint256 amount) public virtual { + require(balanceOf[msg.sender] >= amount, ERC3643TokenMock_InsufficientBalance()); + require(compliance.canTransfer(msg.sender, to, amount), ERC3643TokenMock_ComplianceNotFollowed()); + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + compliance.transferred(msg.sender, to, amount); + } + + /** + * @notice Mints tokens, applying the compliance rules. + * @dev Mirrors `Token.mint`: pre-checks with `canTransfer(address(0), ...)` then notifies + * with `created`. Note the pre-check passes `address(0)` as the origin. + * @param to The address receiving the minted tokens. + * @param amount The number of tokens to mint. + */ + function mint(address to, uint256 amount) public virtual { + require(compliance.canTransfer(address(0), to, amount), ERC3643TokenMock_ComplianceNotFollowed()); + balanceOf[to] += amount; + compliance.created(to, amount); + } + + /** + * @notice Burns tokens, applying the compliance rules. + * @dev Mirrors `Token.burn`: notifies the compliance with `destroyed`. + * @param from The address whose tokens are burned. + * @param amount The number of tokens to burn. + */ + function burn(address from, uint256 amount) public virtual { + require(balanceOf[from] >= amount, ERC3643TokenMock_InsufficientBalance()); + balanceOf[from] -= amount; + compliance.destroyed(from, amount); + } +} diff --git a/src/mocks/ICompliance.sol b/src/mocks/ICompliance.sol index d2b8902..4385bdf 100644 --- a/src/mocks/ICompliance.sol +++ b/src/mocks/ICompliance.sol @@ -1,23 +1,76 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title ICompliance + * @notice Reference ERC-3643 compliance interface used by the mocks and tests. + */ interface ICompliance { - // events + /** + * @notice Emitted when a token is bound to the compliance contract. + * @param _token The address of the token that was bound. + */ event TokenBound(address _token); + + /** + * @notice Emitted when a token is unbound from the compliance contract. + * @param _token The address of the token that was unbound. + */ event TokenUnbound(address _token); - // functions - // initialization of the compliance contract + /** + * @notice Binds a token contract to this compliance contract. + * @param _token The address of the token to bind. + */ function bindToken(address _token) external; + + /** + * @notice Unbinds a token contract from this compliance contract. + * @param _token The address of the token to unbind. + */ function unbindToken(address _token) external; - // check the parameters of the compliance contract + /** + * @notice Updates the compliance state after a transfer has been executed. + * @param _from The address the tokens were sent from. + * @param _to The address the tokens were sent to. + * @param _amount The number of tokens transferred. + */ + function transferred(address _from, address _to, uint256 _amount) external; + + /** + * @notice Updates the compliance state after tokens have been minted. + * @param _to The address receiving the minted tokens. + * @param _amount The number of tokens created. + */ + function created(address _to, uint256 _amount) external; + + /** + * @notice Updates the compliance state after tokens have been burned. + * @param _from The address whose tokens were destroyed. + * @param _amount The number of tokens destroyed. + */ + function destroyed(address _from, uint256 _amount) external; + + /** + * @notice Checks whether a token is currently bound to this compliance contract. + * @param _token The token address to verify. + * @return True if the token is bound, false otherwise. + */ function isTokenBound(address _token) external view returns (bool); + + /** + * @notice Returns the token currently bound to this compliance contract. + * @return The address of the bound token. + */ function getTokenBound() external view returns (address); - // compliance check and state update + /** + * @notice Checks whether a transfer complies with the configured rules. + * @param _from The address the tokens would be sent from. + * @param _to The address the tokens would be sent to. + * @param _amount The number of tokens to transfer. + * @return True if the transfer is allowed, false otherwise. + */ function canTransfer(address _from, address _to, uint256 _amount) external view returns (bool); - function transferred(address _from, address _to, uint256 _amount) external; - function created(address _to, uint256 _amount) external; - function destroyed(address _from, uint256 _amount) external; } diff --git a/src/mocks/IERC1404Subset.sol b/src/mocks/IERC1404Subset.sol index 990f8aa..3b95404 100644 --- a/src/mocks/IERC1404Subset.sol +++ b/src/mocks/IERC1404Subset.sol @@ -6,6 +6,19 @@ pragma solidity ^0.8.20; * @dev Test-only subset matching IERC1404 for interfaceId checks. */ interface IERC1404Subset { + /** + * @notice Returns the restriction code applying to a transfer. + * @param from The origin address. + * @param to The destination address. + * @param value The number of tokens to transfer. + * @return The ERC-1404 restriction code, zero when the transfer is allowed. + */ function detectTransferRestriction(address from, address to, uint256 value) external view returns (uint8); + + /** + * @notice Returns the human readable message for a restriction code. + * @param restrictionCode The target restriction code. + * @return The message describing the restriction. + */ function messageForTransferRestriction(uint8 restrictionCode) external view returns (string memory); } diff --git a/src/mocks/IERC173Subset.sol b/src/mocks/IERC173Subset.sol index aca9d79..960d3dc 100644 --- a/src/mocks/IERC173Subset.sol +++ b/src/mocks/IERC173Subset.sol @@ -6,6 +6,15 @@ pragma solidity ^0.8.20; * @dev Test-only subset matching ERC-173 for interfaceId checks. */ interface IERC173Subset { - function owner() external view returns (address); + /** + * @notice Transfers ownership of the contract to a new account. + * @param newOwner The address of the new owner. + */ function transferOwnership(address newOwner) external; + + /** + * @notice Returns the address of the current owner. + * @return The address of the current owner. + */ + function owner() external view returns (address); } diff --git a/src/mocks/IERC3643ComplianceExtendedSubset.sol b/src/mocks/IERC3643ComplianceExtendedSubset.sol index a211c2c..28cd92a 100644 --- a/src/mocks/IERC3643ComplianceExtendedSubset.sol +++ b/src/mocks/IERC3643ComplianceExtendedSubset.sol @@ -1,11 +1,48 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title IERC3643ComplianceExtendedSubset + * @dev Test-only subset of the extended ERC-3643 compliance interface, used to validate + * the advertised ERC-165 interface ID. + */ interface IERC3643ComplianceExtendedSubset { + /** + * @notice Binds several token contracts in a single call. + * @param tokens The token addresses to bind. + */ function bindTokens(address[] calldata tokens) external; + + /** + * @notice Unbinds several token contracts in a single call. + * @param tokens The token addresses to unbind. + */ function unbindTokens(address[] calldata tokens) external; + + /** + * @notice Allows or forbids a token to bind and unbind itself. + * @param token The token whose self-binding permission is updated. + * @param approved True to allow self-binding, false to forbid it. + */ function setTokenSelfBindingApproval(address token, bool approved) external; + + /** + * @notice Updates the self-binding permission of several tokens at once. + * @param tokens The token addresses whose permission is updated. + * @param approved True to allow self-binding, false to forbid it. + */ function setTokenSelfBindingApprovalBatch(address[] calldata tokens, bool approved) external; + + /** + * @notice Tells whether a token may bind and unbind itself. + * @param token The token address to check. + * @return approved True if the token is allowed to self-bind, false otherwise. + */ function isTokenSelfBindingApproved(address token) external view returns (bool approved); + + /** + * @notice Returns every token currently bound to the compliance contract. + * @return tokens The list of bound token addresses. + */ function getTokenBounds() external view returns (address[] memory tokens); } diff --git a/src/mocks/IERC7551ComplianceSubset.sol b/src/mocks/IERC7551ComplianceSubset.sol index 8c5f46a..7e73125 100644 --- a/src/mocks/IERC7551ComplianceSubset.sol +++ b/src/mocks/IERC7551ComplianceSubset.sol @@ -7,5 +7,13 @@ pragma solidity ^0.8.20; * currently implemented by RuleEngine. */ interface IERC7551ComplianceSubset { + /** + * @notice Tells whether a spender-initiated transfer is allowed. + * @param spender The account moving the tokens on behalf of `from`. + * @param from The origin address. + * @param to The destination address. + * @param value The number of tokens to transfer. + * @return True if the transfer is allowed, false otherwise. + */ function canTransferFrom(address spender, address from, address to, uint256 value) external view returns (bool); } diff --git a/src/mocks/IOwnable2StepSubset.sol b/src/mocks/IOwnable2StepSubset.sol index fe5b99f..a322fc8 100644 --- a/src/mocks/IOwnable2StepSubset.sol +++ b/src/mocks/IOwnable2StepSubset.sol @@ -6,6 +6,15 @@ pragma solidity ^0.8.20; * @dev Test-only subset for Ownable2Step-specific ERC-165 checks. */ interface IOwnable2StepSubset { - function pendingOwner() external view returns (address); + /** + * @notice Accepts a pending ownership transfer. + * @dev Callable by the pending owner to complete the two-step handover. + */ function acceptOwnership() external; + + /** + * @notice Returns the account currently awaiting acceptance of ownership. + * @return The address of the pending owner. + */ + function pendingOwner() external view returns (address); } diff --git a/src/mocks/IRuleInterfaceIdHelper.sol b/src/mocks/IRuleInterfaceIdHelper.sol index 735caa5..3f03769 100644 --- a/src/mocks/IRuleInterfaceIdHelper.sol +++ b/src/mocks/IRuleInterfaceIdHelper.sol @@ -17,25 +17,83 @@ import {RuleInterfaceId} from "../modules/library/RuleInterfaceId.sol"; * type(IRuleAllFunctions).interfaceId covers the full hierarchy. */ interface IRuleAllFunctions { - // From IRule + /** + * @notice From IRuleEngine: applies the rule to a transfer carrying a spender. + * @param spender The account moving the tokens on behalf of `from`. + * @param from The origin address. + * @param to The destination address. + * @param value The number of tokens transferred. + */ + function transferred(address spender, address from, address to, uint256 value) external; + + /** + * @notice From IERC3643IComplianceContract: applies the rule to a transfer without a spender. + * @param from The origin address. + * @param to The destination address. + * @param value The number of tokens transferred. + */ + function transferred(address from, address to, uint256 value) external; + + /** + * @notice From IRule: tells whether the restriction code belongs to this rule. + * @param restrictionCode The target restriction code. + * @return True if the restriction code is known by the rule. + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external view returns (bool); - // From IERC1404 + + /** + * @notice From IERC1404: returns the restriction code applying to a transfer. + * @param from The origin address. + * @param to The destination address. + * @param value The number of tokens to transfer. + * @return The ERC-1404 restriction code, zero when the transfer is allowed. + */ function detectTransferRestriction(address from, address to, uint256 value) external view returns (uint8); + + /** + * @notice From IERC1404: returns the human readable message for a restriction code. + * @param restrictionCode The target restriction code. + * @return The message describing the restriction. + */ function messageForTransferRestriction(uint8 restrictionCode) external view returns (string memory); - // From IERC1404Extend + + /** + * @notice From IERC1404Extend: returns the restriction code for a spender-initiated transfer. + * @param spender The account moving the tokens on behalf of `from`. + * @param from The origin address. + * @param to The destination address. + * @param value The number of tokens to transfer. + * @return The ERC-1404 restriction code, zero when the transfer is allowed. + */ function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) external view returns (uint8); - // From IRuleEngine - function transferred(address spender, address from, address to, uint256 value) external; - // From IERC3643IComplianceContract - function transferred(address from, address to, uint256 value) external; - // From IERC3643ComplianceRead + + /** + * @notice From IERC3643ComplianceRead: tells whether a transfer is allowed. + * @param from The origin address. + * @param to The destination address. + * @param value The number of tokens to transfer. + * @return True if the transfer is allowed, false otherwise. + */ function canTransfer(address from, address to, uint256 value) external view returns (bool); - // From IERC7551Compliance + + /** + * @notice From IERC7551Compliance: tells whether a spender-initiated transfer is allowed. + * @param spender The account moving the tokens on behalf of `from`. + * @param from The origin address. + * @param to The destination address. + * @param value The number of tokens to transfer. + * @return True if the transfer is allowed, false otherwise. + */ function canTransferFrom(address spender, address from, address to, uint256 value) external view returns (bool); - // From IERC165 + + /** + * @notice From IERC165: ERC-165 interface detection. + * @param interfaceId The interface identifier to check. + * @return True if the interface is supported, false otherwise. + */ function supportsInterface(bytes4 interfaceId) external view returns (bool); } @@ -44,22 +102,42 @@ interface IRuleAllFunctions { * @dev Helper contract to expose IRule interface IDs and verify computation. */ contract IRuleInterfaceIdHelper { - /// @notice Returns type(IRule).interfaceId (only directly defined functions) + /** + * @notice Returns type(IRule).interfaceId (only directly defined functions) + * @return The ERC-165 interface ID of IRule alone. + */ function getIRuleInterfaceId() external pure returns (bytes4) { return type(IRule).interfaceId; } - /// @notice Returns the XOR of ALL function selectors in the IRule hierarchy (flattened) + /** + * @notice Returns the XOR of ALL function selectors in the IRule hierarchy (flattened) + * @return The ERC-165 interface ID covering the full IRule hierarchy. + */ function getIRuleAllFunctionsInterfaceId() external pure returns (bytes4) { return type(IRuleAllFunctions).interfaceId; } - /// @notice Returns the constant defined in RuleInterfaceId library + /** + * @notice Returns the constant defined in RuleInterfaceId library + * @return The interface ID constant shipped in RuleInterfaceId. + */ function getRuleInterfaceIdConstant() external pure returns (bytes4) { return RuleInterfaceId.IRULE_INTERFACE_ID; } - /// @notice Returns individual interface IDs from each parent interface + /** + * @notice Returns individual interface IDs from each parent interface + * @return iRuleId The interface ID of IRule. + * @return iRuleEngineERC1404Id The interface ID of IRuleEngineERC1404. + * @return iRuleEngineId The interface ID of IRuleEngine. + * @return iERC1404Id The interface ID of IERC1404. + * @return iERC1404ExtendId The interface ID of IERC1404Extend. + * @return iERC3643ComplianceReadId The interface ID of IERC3643ComplianceRead. + * @return iERC3643IComplianceContractId The interface ID of IERC3643IComplianceContract. + * @return iERC7551ComplianceId The interface ID of IERC7551Compliance. + * @return iERC165Id The interface ID of IERC165. + */ function getParentInterfaceIds() external pure @@ -86,8 +164,11 @@ contract IRuleInterfaceIdHelper { iERC165Id = type(IERC165).interfaceId; } - /// @notice Manually computes the XOR of all function selectors and returns it // forge-lint: disable-next-line(mixed-case-function) + /** + * @notice Manually computes the XOR of all function selectors and returns it + * @return The interface ID obtained by XOR-ing every selector by hand. + */ function computeManualXOR() external pure returns (bytes4) { return IRule.canReturnTransferRestrictionCode.selector ^ IERC1404.detectTransferRestriction.selector ^ IERC1404.messageForTransferRestriction.selector ^ IERC1404Extend.detectTransferRestrictionFrom.selector diff --git a/src/mocks/RuleEngineExposed.sol b/src/mocks/RuleEngineExposed.sol index 80d5da9..ba565e4 100644 --- a/src/mocks/RuleEngineExposed.sol +++ b/src/mocks/RuleEngineExposed.sol @@ -10,8 +10,18 @@ import {RuleEngineOwnable2Step} from "../deployment/RuleEngineOwnable2Step.sol"; * @dev Exposes internal functions for testing coverage */ contract RuleEngineExposed is RuleEngine { + /** + * @notice Deploys the exposed engine. + * @param admin Address receiving the initial privileges. + * @param forwarder Address of the trusted ERC-2771 forwarder. + * @param token Token to bind at deployment, or the zero address to bind none. + */ constructor(address admin, address forwarder, address token) RuleEngine(admin, forwarder, token) {} + /** + * @notice Exposes the internal {_msgData} for testing. + * @return The transaction calldata as seen by the engine. + */ function exposedMsgData() external view returns (bytes memory) { return _msgData(); } @@ -22,8 +32,18 @@ contract RuleEngineExposed is RuleEngine { * @dev Exposes internal functions for testing coverage */ contract RuleEngineOwnableExposed is RuleEngineOwnable { + /** + * @notice Deploys the exposed engine. + * @param owner_ Address receiving the initial privileges. + * @param forwarder Address of the trusted ERC-2771 forwarder. + * @param token Token to bind at deployment, or the zero address to bind none. + */ constructor(address owner_, address forwarder, address token) RuleEngineOwnable(owner_, forwarder, token) {} + /** + * @notice Exposes the internal {_msgData} for testing. + * @return The transaction calldata as seen by the engine. + */ function exposedMsgData() external view returns (bytes memory) { return _msgData(); } @@ -34,8 +54,18 @@ contract RuleEngineOwnableExposed is RuleEngineOwnable { * @dev Exposes internal functions for testing coverage */ contract RuleEngineOwnable2StepExposed is RuleEngineOwnable2Step { + /** + * @notice Deploys the exposed engine. + * @param owner_ Address receiving the initial privileges. + * @param forwarder Address of the trusted ERC-2771 forwarder. + * @param token Token to bind at deployment, or the zero address to bind none. + */ constructor(address owner_, address forwarder, address token) RuleEngineOwnable2Step(owner_, forwarder, token) {} + /** + * @notice Exposes the internal {_msgData} for testing. + * @return The transaction calldata as seen by the engine. + */ function exposedMsgData() external view returns (bytes memory) { return _msgData(); } diff --git a/src/mocks/rules/operation/RuleConditionalTransferLight.sol b/src/mocks/rules/operation/RuleConditionalTransferLightMock.sol similarity index 73% rename from src/mocks/rules/operation/RuleConditionalTransferLight.sol rename to src/mocks/rules/operation/RuleConditionalTransferLightMock.sol index 0d9beb1..0606e58 100644 --- a/src/mocks/rules/operation/RuleConditionalTransferLight.sol +++ b/src/mocks/rules/operation/RuleConditionalTransferLightMock.sol @@ -15,14 +15,28 @@ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; * @dev Requires operator approval for each ERC20 transfer. * Same transfer (from, to, value) can be approved multiple times. */ -contract RuleConditionalTransferLight is AccessControl, RuleConditionalTransferLightInvariantStorage, IRule { +contract RuleConditionalTransferLightMock is AccessControl, RuleConditionalTransferLightInvariantStorage, IRule { + /** + * @notice ERC-165 interface ID of the CMTAT RuleEngine interface. + */ bytes4 private constant RULE_ENGINE_INTERFACE_ID = 0x20c49ce7; + /** + * @notice ERC-165 interface ID of the extended ERC-1404 interface. + */ bytes4 private constant ERC1404EXTEND_INTERFACE_ID = 0x78a8de7d; // Mapping from transfer hash to approval count + /** + * @notice Number of outstanding approvals per transfer hash. + */ mapping(bytes32 => uint256) public approvalCounts; + /** + * @notice Deploys the conditional-transfer rule. + * @param admin Address granted OPERATOR_ROLE. + * @param ruleEngineContract RuleEngine granted RULE_ENGINE_CONTRACT_ROLE, or the zero address. + */ constructor(address admin, IRuleEngine ruleEngineContract) { - require(admin != address(0), "Invalid operator"); + require(admin != address(0), RuleConditionalTransferLight_AdminAddressZeroNotAllowed()); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(OPERATOR_ROLE, admin); @@ -31,33 +45,51 @@ 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 || AccessControl.supportsInterface(interfaceId); + /** + * @notice To know if the restriction code is valid for this rule or not. + * @param restrictionCode The target restriction code + * @return true if the restriction code is known, false otherwise + * + */ + function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { + return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; } /** - * @notice Approve a specific transfer. Can be approved multiple times. + * @notice Return the corresponding message + * @param restrictionCode The target restriction code + * @return true if the transfer is valid, false otherwise + * */ - function approveTransfer(address from, address to, uint256 value) public onlyRole(OPERATOR_ROLE) { - // forge-lint: disable-next-line(asm-keccak256) - bytes32 transferHash = keccak256(abi.encodePacked(from, to, value)); - approvalCounts[transferHash] += 1; - emit TransferApproved(from, to, value, approvalCounts[transferHash]); + function messageForTransferRestriction(uint8 restrictionCode) external pure override returns (string memory) { + if (restrictionCode == uint8(REJECTED_CODE_BASE.TRANSFER_OK)) { + return TEXT_TRANSFER_OK; + } else if (restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED) { + return TEXT_TRANSFER_REQUEST_NOT_APPROVED; + } else { + return TEXT_CODE_NOT_FOUND; + } } /** - * @notice Returns number of times a transfer is approved. + * @notice Approve a specific transfer. Can be approved multiple times. + * @param from the origin address + * @param to the destination address + * @param value the amount approved for transfer */ - function approvedCount(address from, address to, uint256 value) public view returns (uint256) { + function approveTransfer(address from, address to, uint256 value) public onlyRole(OPERATOR_ROLE) { // forge-lint: disable-next-line(asm-keccak256) bytes32 transferHash = keccak256(abi.encodePacked(from, to, value)); - return approvalCounts[transferHash]; + approvalCounts[transferHash] += 1; + emit TransferApproved(from, to, value, approvalCounts[transferHash]); } /** * @notice Called when a transfer occurs. Decrements approval count if allowed. * @dev `spender` is part of the interface but unused. + * @param from the origin address + * @param to the destination address + * @param value the amount transferred */ function transferred(address from, address to, uint256 value) public { // forge-lint: disable-next-line(asm-keccak256) @@ -70,6 +102,13 @@ contract RuleConditionalTransferLight is AccessControl, RuleConditionalTransferL emit TransferExecuted(from, to, value, approvalCounts[transferHash]); } + /** + * @notice Called when a transfer occurs, ignoring the spender. + * @dev Delegates to the 3-argument overload. + * @param from the origin address + * @param to the destination address + * @param value the amount transferred + */ function transferred( address, /* spender */ @@ -82,10 +121,34 @@ contract RuleConditionalTransferLight is AccessControl, RuleConditionalTransferL transferred(from, to, value); } + /** + * @notice ERC-165 interface detection. + * @param interfaceId The interface identifier to check. + * @return True if the interface is supported, false otherwise. + */ + 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 || AccessControl.supportsInterface(interfaceId); + } + + /** + * @notice Returns number of times a transfer is approved. + * @param from the origin address + * @param to the destination address + * @param value the amount of the approved transfer + * @return The number of outstanding approvals for this exact transfer. + */ + function approvedCount(address from, address to, uint256 value) public view returns (uint256) { + // forge-lint: disable-next-line(asm-keccak256) + bytes32 transferHash = keccak256(abi.encodePacked(from, to, value)); + return approvalCounts[transferHash]; + } + /** * @notice Check if the transfer is valid * @param from the origin address * @param to the destination address + * @param value the amount to transfer * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK * */ @@ -103,6 +166,7 @@ contract RuleConditionalTransferLight is AccessControl, RuleConditionalTransferL * @notice Check if the transfer is valid * @param from the origin address * @param to the destination address + * @param value the amount to transfer * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK * */ @@ -121,30 +185,6 @@ contract RuleConditionalTransferLight is AccessControl, RuleConditionalTransferL return detectTransferRestriction(from, to, value); } - /** - * @notice To know if the restriction code is valid for this rule or not. - * @param restrictionCode The target restriction code - * @return true if the restriction code is known, false otherwise - * - */ - function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { - return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; - } - - /** - * @notice Return the corresponding message - * @param restrictionCode The target restriction code - * @return true if the transfer is valid, false otherwise - * - */ - function messageForTransferRestriction(uint8 restrictionCode) external pure override returns (string memory) { - if (restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED) { - return TEXT_TRANSFER_REQUEST_NOT_APPROVED; - } else { - return TEXT_CODE_NOT_FOUND; - } - } - /** * @notice Validate a transfer * @param _from the origin address @@ -157,6 +197,14 @@ contract RuleConditionalTransferLight is AccessControl, RuleConditionalTransferL return detectTransferRestriction(_from, _to, _amount) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Validate a spender-initiated transfer + * @param spender the spender address (transferFrom) + * @param from the origin address + * @param to the destination address + * @param value the amount to transfer + * @return true if the transfer is valid, false otherwise + */ function canTransferFrom(address spender, address from, address to, uint256 value) public view diff --git a/src/mocks/rules/operation/RuleMintAllowance.sol b/src/mocks/rules/operation/RuleMintAllowanceMock.sol similarity index 60% rename from src/mocks/rules/operation/RuleMintAllowance.sol rename to src/mocks/rules/operation/RuleMintAllowanceMock.sol index c29fcd7..74e2f7e 100644 --- a/src/mocks/rules/operation/RuleMintAllowance.sol +++ b/src/mocks/rules/operation/RuleMintAllowanceMock.sol @@ -8,33 +8,35 @@ import {RuleInterfaceId} from "../../../modules/library/RuleInterfaceId.sol"; import {RuleMintAllowanceInvariantStorage} from "./abstract/RuleMintAllowanceInvariantStorage.sol"; /** - * @title RuleMintAllowance + * @title RuleMintAllowanceMock * @notice Rule that enforces per-minter mint allowances set by the contract admin. * The admin grants each minter address a maximum amount they may mint in total. * Each mint deducts from the minter's remaining allowance. * Burns and regular transfers are unrestricted by this rule. */ -contract RuleMintAllowance is AccessControl, RuleMintAllowanceInvariantStorage, IRule { +contract RuleMintAllowanceMock is AccessControl, RuleMintAllowanceInvariantStorage, IRule { + /** + * @notice ERC-165 interface ID of the CMTAT RuleEngine interface. + */ bytes4 private constant RULE_ENGINE_INTERFACE_ID = 0x20c49ce7; + /** + * @notice ERC-165 interface ID of the extended ERC-1404 interface. + */ bytes4 private constant ERC1404EXTEND_INTERFACE_ID = 0x78a8de7d; + /** + * @notice Remaining mint allowance per minter. + */ mapping(address minter => uint256 allowance) public mintAllowance; /** * @param admin Address granted DEFAULT_ADMIN_ROLE */ constructor(address admin) { - require(admin != address(0), "RuleMintAllowance: zero admin"); + require(admin != address(0), RuleMintAllowance_AdminAddressZeroNotAllowed()); _grantRole(DEFAULT_ADMIN_ROLE, admin); } - /* ============ ERC-165 ============ */ - - 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 || AccessControl.supportsInterface(interfaceId); - } - /* ============ Admin ============ */ /** @@ -47,11 +49,40 @@ contract RuleMintAllowance is AccessControl, RuleMintAllowanceInvariantStorage, emit MintAllowanceSet(minter, amount); } + /* ============ IRule — external view ============ */ + + /** + * @notice To know if the restriction code is valid for this rule or not. + * @param restrictionCode The target restriction code. + * @return True if the restriction code is known by this rule, false otherwise. + */ + function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { + return restrictionCode == CODE_MINTER_INSUFFICIENT_ALLOWANCE; + } + + /** + * @notice Returns the message matching a restriction code. + * @param restrictionCode The target restriction code. + * @return The message describing the restriction code. + */ + function messageForTransferRestriction(uint8 restrictionCode) external pure override returns (string memory) { + if (restrictionCode == uint8(REJECTED_CODE_BASE.TRANSFER_OK)) { + return TEXT_TRANSFER_OK; + } + if (restrictionCode == CODE_MINTER_INSUFFICIENT_ALLOWANCE) { + return TEXT_MINTER_INSUFFICIENT_ALLOWANCE; + } + return TEXT_CODE_NOT_FOUND; + } + /* ============ IRule — state-changing ============ */ /** * @notice Called for transfers where no spender context is available. * Mint allowance cannot be enforced without a spender; passes through. + * @param from the origin address + * @param to the destination address + * @param value the amount transferred */ function transferred(address from, address to, uint256 value) public { // no-op: spender unknown, enforcement requires transferred(spender,...) @@ -60,8 +91,19 @@ contract RuleMintAllowance is AccessControl, RuleMintAllowanceInvariantStorage, /** * @notice Called for every token operation (transfer, mint, burn) with spender context. * Deducts from the minter's allowance for mints; passes through for burns and transfers. + * @param spender the account performing the operation (the minter on a mint) + * @param from the origin address, zero on a mint + * @param value the amount transferred */ - function transferred(address spender, address from, address /* to */, uint256 value) public { + function transferred( + address spender, + address from, + address, + /* to */ + uint256 value + ) + public + { if (from == address(0)) { uint256 allowance = mintAllowance[spender]; if (allowance < value) { @@ -74,8 +116,25 @@ contract RuleMintAllowance is AccessControl, RuleMintAllowanceInvariantStorage, /* ============ IRule — view ============ */ + /** + * @notice ERC-165 interface detection. + * @param interfaceId The interface identifier to check. + * @return True if the interface is supported, false otherwise. + */ + 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 || AccessControl.supportsInterface(interfaceId); + } + /** * @notice Returns TRANSFER_OK; without spender context mint allowance cannot be evaluated. + * @dev WARNING: this path fails open. The ERC-1404 3-argument signature carries no spender, + * and the mint allowance is keyed by spender, so this rule cannot evaluate a mint here and + * answers TRANSFER_OK. The engine aggregates that answer, so `detectTransferRestriction` and + * `canTransfer` on the RuleEngine can report a mint as allowed that `transferred(spender, ...)` + * will revert. Integrators must use the 4-argument `detectTransferRestrictionFrom` / + * `canTransferFrom` to pre-check a mint. + * @return Always REJECTED_CODE_BASE.TRANSFER_OK. */ function detectTransferRestriction(address, address, uint256) public pure override returns (uint8) { return uint8(REJECTED_CODE_BASE.TRANSFER_OK); @@ -84,6 +143,10 @@ contract RuleMintAllowance is AccessControl, RuleMintAllowanceInvariantStorage, /** * @notice Returns CODE_MINTER_INSUFFICIENT_ALLOWANCE when a minter's allowance would be * exceeded. Burns and regular transfers always return TRANSFER_OK. + * @param spender the account performing the operation (the minter on a mint) + * @param from the origin address, zero on a mint + * @param value the amount to transfer + * @return The restriction code, or REJECTED_CODE_BASE.TRANSFER_OK when allowed. */ function detectTransferRestrictionFrom(address spender, address from, address, uint256 value) public @@ -97,10 +160,25 @@ contract RuleMintAllowance is AccessControl, RuleMintAllowanceInvariantStorage, return uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Validate a transfer + * @param from the origin address + * @param to the destination address + * @param value the amount to transfer + * @return true if the transfer is valid, false otherwise + */ function canTransfer(address from, address to, uint256 value) public pure override returns (bool) { return detectTransferRestriction(from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Validate a spender-initiated transfer + * @param spender the spender address (transferFrom) + * @param from the origin address + * @param to the destination address + * @param value the amount to transfer + * @return true if the transfer is valid, false otherwise + */ function canTransferFrom(address spender, address from, address to, uint256 value) public view @@ -109,15 +187,4 @@ contract RuleMintAllowance is AccessControl, RuleMintAllowanceInvariantStorage, { return detectTransferRestrictionFrom(spender, from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); } - - function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { - return restrictionCode == CODE_MINTER_INSUFFICIENT_ALLOWANCE; - } - - function messageForTransferRestriction(uint8 restrictionCode) external pure override returns (string memory) { - if (restrictionCode == CODE_MINTER_INSUFFICIENT_ALLOWANCE) { - return TEXT_MINTER_INSUFFICIENT_ALLOWANCE; - } - return TEXT_CODE_NOT_FOUND; - } } diff --git a/src/mocks/rules/operation/RuleOperationRevert.sol b/src/mocks/rules/operation/RuleOperationRevertMock.sol similarity index 79% rename from src/mocks/rules/operation/RuleOperationRevert.sol rename to src/mocks/rules/operation/RuleOperationRevertMock.sol index 1bc7fc7..97a584a 100644 --- a/src/mocks/rules/operation/RuleOperationRevert.sol +++ b/src/mocks/rules/operation/RuleOperationRevertMock.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -// forge-lint: disable-next-line(unaliased-plain-import) -import "../validation/abstract/RuleCommonInvariantStorage.sol"; +import {RuleCommonInvariantStorage} from "../validation/abstract/RuleCommonInvariantStorage.sol"; import {IRule} from "../../../interfaces/IRule.sol"; import {RuleInterfaceId} from "../../../modules/library/RuleInterfaceId.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; @@ -13,14 +12,54 @@ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; * @dev Requires operator approval for each ERC20 transfer. * Same transfer (from, to, value) can be approved multiple times. */ -contract RuleOperationRevert is AccessControl, IRule, RuleCommonInvariantStorage { +contract RuleOperationRevertMock is AccessControl, IRule, RuleCommonInvariantStorage { error RuleConditionalTransferLight_InvalidTransfer(); // It is very important that each rule uses an unique code + /** + * @notice Restriction code raised when the transfer request was not approved. + */ uint8 public constant CODE_TRANSFER_REQUEST_NOT_APPROVED = 71; + /** + * @notice ERC-165 interface ID of the CMTAT RuleEngine interface. + */ bytes4 private constant RULE_ENGINE_INTERFACE_ID = 0x20c49ce7; + /** + * @notice ERC-165 interface ID of the extended ERC-1404 interface. + */ bytes4 private constant ERC1404EXTEND_INTERFACE_ID = 0x78a8de7d; + /** + * @notice To know if the restriction code is valid for this rule or not. + * @param restrictionCode The target restriction code + * @return true if the restriction code is known, false otherwise + * + */ + function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { + return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; + } + + /** + * @notice Return the corresponding message + * @return true if the transfer is valid, false otherwise + * + */ + function messageForTransferRestriction( + uint8 /* restrictionCode */ + ) + external + pure + override + returns (string memory) + { + return TEXT_CODE_NOT_FOUND; + } + + /** + * @notice ERC-165 interface detection. + * @param interfaceId The interface identifier to check. + * @return True if the interface is supported, false otherwise. + */ 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 || AccessControl.supportsInterface(interfaceId); @@ -43,6 +82,9 @@ contract RuleOperationRevert is AccessControl, IRule, RuleCommonInvariantStorage revert RuleConditionalTransferLight_InvalidTransfer(); } + /** + * @notice Always reverts; used to test how the engine propagates a failing rule. + */ function transferred( address, /* spender */ @@ -82,6 +124,7 @@ contract RuleOperationRevert is AccessControl, IRule, RuleCommonInvariantStorage * @notice Check if the transfer is valid * @param from the origin address * @param to the destination address + * @param value the amount to transfer * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK * */ @@ -100,32 +143,6 @@ contract RuleOperationRevert is AccessControl, IRule, RuleCommonInvariantStorage return detectTransferRestriction(from, to, value); } - /** - * @notice To know if the restriction code is valid for this rule or not. - * @param restrictionCode The target restriction code - * @return true if the restriction code is known, false otherwise - * - */ - function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { - return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; - } - - /** - * @notice Return the corresponding message - * @return true if the transfer is valid, false otherwise - * - */ - function messageForTransferRestriction( - uint8 /* restrictionCode */ - ) - external - pure - override - returns (string memory) - { - return TEXT_CODE_NOT_FOUND; - } - /** * @notice Validate a transfer * @param _from the origin address @@ -138,6 +155,14 @@ contract RuleOperationRevert is AccessControl, IRule, RuleCommonInvariantStorage return detectTransferRestriction(_from, _to, _amount) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Validate a spender-initiated transfer + * @param spender the spender address (transferFrom) + * @param from the origin address + * @param to the destination address + * @param value the amount to transfer + * @return true if the transfer is valid, false otherwise + */ function canTransferFrom(address spender, address from, address to, uint256 value) public view diff --git a/src/mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol b/src/mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol index b6cf6dd..8a34ec7 100644 --- a/src/mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol +++ b/src/mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol @@ -2,26 +2,54 @@ pragma solidity ^0.8.20; -// forge-lint: disable-next-line(unaliased-plain-import) -import "../../validation/abstract/RuleCommonInvariantStorage.sol"; -// forge-lint: disable-next-line(unaliased-plain-import) -import "src/mocks/rules/validation/RuleWhitelist.sol"; +import {RuleCommonInvariantStorage} from "../../validation/abstract/RuleCommonInvariantStorage.sol"; +/** + * @title RuleConditionalTransferLightInvariantStorage + * @notice Roles, codes, errors and events for the conditional-transfer rule. + */ abstract contract RuleConditionalTransferLightInvariantStorage is RuleCommonInvariantStorage { /* ============ Role ============ */ + /** + * @notice Role held by the RuleEngine allowed to consume approvals. + */ bytes32 public constant RULE_ENGINE_CONTRACT_ROLE = keccak256("RULE_ENGINE_CONTRACT_ROLE"); + /** + * @notice Role allowed to approve transfers. + */ bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); /* ============ State variables ============ */ + /** + * @notice Message returned when the transfer was not approved. + */ string constant TEXT_TRANSFER_REQUEST_NOT_APPROVED = "ConditionalTransferLight: The request is not approved"; // Code // It is very important that each rule uses an unique code + /** + * @notice Restriction code raised when the transfer was not approved. + */ uint8 public constant CODE_TRANSFER_REQUEST_NOT_APPROVED = 71; /* ============ Custom error ============ */ error TransferNotApproved(); + error RuleConditionalTransferLight_AdminAddressZeroNotAllowed(); /* ============ Events ============ */ + /** + * @notice Emitted when an operator approves a transfer. + * @param from The origin address of the approved transfer. + * @param to The destination address of the approved transfer. + * @param value The amount approved. + * @param count The number of outstanding approvals after this one. + */ event TransferApproved(address indexed from, address indexed to, uint256 value, uint256 count); + /** + * @notice Emitted when an approved transfer is consumed. + * @param from The origin address of the transfer. + * @param to The destination address of the transfer. + * @param value The amount transferred. + * @param remaining The number of approvals still outstanding. + */ event TransferExecuted(address indexed from, address indexed to, uint256 value, uint256 remaining); } diff --git a/src/mocks/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol b/src/mocks/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol index 9ebce87..ca3a0ba 100644 --- a/src/mocks/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol +++ b/src/mocks/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol @@ -1,21 +1,42 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; -// forge-lint: disable-next-line(unaliased-plain-import) -import "../../validation/abstract/RuleCommonInvariantStorage.sol"; +import {RuleCommonInvariantStorage} from "../../validation/abstract/RuleCommonInvariantStorage.sol"; +/** + * @title RuleMintAllowanceInvariantStorage + * @notice Errors, events, codes and messages for the mint-allowance rule. + */ abstract contract RuleMintAllowanceInvariantStorage is RuleCommonInvariantStorage { /* ============ Error ============ */ error RuleMintAllowance_InsufficientAllowance(address minter, uint256 allowance, uint256 value); + error RuleMintAllowance_AdminAddressZeroNotAllowed(); /* ============ Events ============ */ + /** + * @notice Emitted when a minter's allowance is set. + * @param minter The minter whose allowance changed. + * @param allowance The new allowance. + */ event MintAllowanceSet(address indexed minter, uint256 allowance); + /** + * @notice Emitted when a minter consumes part of its allowance. + * @param minter The minter consuming the allowance. + * @param consumed The amount consumed. + * @param remaining The allowance left after the operation. + */ event MintAllowanceConsumed(address indexed minter, uint256 consumed, uint256 remaining); /* ============ Restriction codes ============ */ // It is very important that each rule uses a unique code + /** + * @notice Restriction code raised when a minter's allowance is insufficient. + */ uint8 public constant CODE_MINTER_INSUFFICIENT_ALLOWANCE = 81; /* ============ Restriction messages ============ */ + /** + * @notice Message returned when a minter's allowance is insufficient. + */ string constant TEXT_MINTER_INSUFFICIENT_ALLOWANCE = "MintAllowance: Insufficient allowance for minter"; } diff --git a/src/mocks/rules/validation/RuleWhitelist.sol b/src/mocks/rules/validation/RuleWhitelistMock.sol similarity index 70% rename from src/mocks/rules/validation/RuleWhitelist.sol rename to src/mocks/rules/validation/RuleWhitelistMock.sol index cf03e00..19d12a6 100644 --- a/src/mocks/rules/validation/RuleWhitelist.sol +++ b/src/mocks/rules/validation/RuleWhitelistMock.sol @@ -12,22 +12,57 @@ import {RuleInterfaceId} from "../../../modules/library/RuleInterfaceId.sol"; /** * @title a whitelist manager */ -contract RuleWhitelist is RuleAddressList, RuleWhitelistCommon { +contract RuleWhitelistMock is RuleAddressList, RuleWhitelistCommon { + /** + * @notice ERC-165 interface ID of the CMTAT RuleEngine interface. + */ bytes4 private constant RULE_ENGINE_INTERFACE_ID = 0x20c49ce7; + /** + * @notice ERC-165 interface ID of the extended ERC-1404 interface. + */ bytes4 private constant ERC1404EXTEND_INTERFACE_ID = 0x78a8de7d; error RuleWhitelist_InvalidTransfer(address from, address to, uint256 value, uint8 code); - 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 || AccessControl.supportsInterface(interfaceId); - } - /** + * @notice Deploys the whitelist rule. * @param admin Address of the contract (Access Control) * @param forwarderIrrevocable Address of the forwarder, required for the gasless support */ constructor(address admin, address forwarderIrrevocable) RuleAddressList(admin, forwarderIrrevocable) {} + /** + * @notice Validates a transfer and reverts when the whitelist forbids it. + * @param from the origin address + * @param to the destination address + * @param value the amount to transfer + */ + function transferred(address from, address to, uint256 value) public { + uint8 code = detectTransferRestriction(from, to, value); + require(code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), RuleWhitelist_InvalidTransfer(from, to, value, code)); + } + + /** + * @notice Validates a spender-initiated transfer and reverts when the whitelist forbids it. + * @param spender the spender address (transferFrom) + * @param from the origin address + * @param to the destination address + * @param value the amount to transfer + */ + function transferred(address spender, address from, address to, uint256 value) public { + uint8 code = detectTransferRestrictionFrom(spender, from, to, value); + require(code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), RuleWhitelist_InvalidTransfer(from, to, value, code)); + } + + /** + * @notice ERC-165 interface detection. + * @param interfaceId The interface identifier to check. + * @return True if the interface is supported, false otherwise. + */ + 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 || AccessControl.supportsInterface(interfaceId); + } + /** * @notice Validate a transfer * @param _from the origin address @@ -40,6 +75,14 @@ contract RuleWhitelist is RuleAddressList, RuleWhitelistCommon { return detectTransferRestriction(_from, _to, _amount) == uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Validate a spender-initiated transfer + * @param spender the spender address (transferFrom) + * @param from the origin address + * @param to the destination address + * @param value the amount to transfer + * @return true if the transfer is valid, false otherwise + */ function canTransferFrom(address spender, address from, address to, uint256 value) public view @@ -76,6 +119,14 @@ contract RuleWhitelist is RuleAddressList, RuleWhitelistCommon { } } + /** + * @notice Check if a spender-initiated transfer is valid + * @param spender the spender address (transferFrom) + * @param from the origin address + * @param to the destination address + * @param value the amount to transfer + * @return The restriction code or REJECTED_CODE_BASE.TRANSFER_OK + */ function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) public view @@ -89,14 +140,4 @@ contract RuleWhitelist is RuleAddressList, RuleWhitelistCommon { return detectTransferRestriction(from, to, value); } } - - function transferred(address from, address to, uint256 value) public { - uint8 code = detectTransferRestriction(from, to, value); - require(code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), RuleWhitelist_InvalidTransfer(from, to, value, code)); - } - - function transferred(address spender, address from, address to, uint256 value) public { - uint8 code = detectTransferRestrictionFrom(spender, from, to, value); - require(code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), RuleWhitelist_InvalidTransfer(from, to, value, code)); - } } diff --git a/src/mocks/rules/validation/abstract/RuleAddressList/RuleAddressList.sol b/src/mocks/rules/validation/abstract/RuleAddressList/RuleAddressList.sol index 2bb727f..9956414 100644 --- a/src/mocks/rules/validation/abstract/RuleAddressList/RuleAddressList.sol +++ b/src/mocks/rules/validation/abstract/RuleAddressList/RuleAddressList.sol @@ -2,14 +2,12 @@ pragma solidity ^0.8.20; -// forge-lint: disable-next-line(unaliased-plain-import) -import "@openzeppelin/contracts/access/AccessControl.sol"; -// forge-lint: disable-next-line(unaliased-plain-import) -import "../../../../../modules/ERC2771ModuleStandalone.sol"; -// forge-lint: disable-next-line(unaliased-plain-import) -import "./RuleAddressListInternal.sol"; -// forge-lint: disable-next-line(unaliased-plain-import) -import "./invariantStorage/RuleAddressListInvariantStorage.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol"; +import {ERC2771ModuleStandalone} from "../../../../../modules/ERC2771ModuleStandalone.sol"; +import {RuleAddressListInternal} from "./RuleAddressListInternal.sol"; +import {RuleAddressListInvariantStorage} from "./invariantStorage/RuleAddressListInvariantStorage.sol"; /** * @title an addresses list manager @@ -22,6 +20,9 @@ abstract contract RuleAddressList is RuleAddressListInvariantStorage { // Number of addresses in the list at the moment + /** + * @notice Number of addresses currently in the list. + */ uint256 private numAddressesWhitelisted; /** @@ -101,9 +102,11 @@ abstract contract RuleAddressList is /** * @notice batch version of {addressIsListed} + * @param _targetAddresses The addresses to check. + * @return One boolean per input address, true when listed. * */ - function addressIsListedBatch(address[] memory _targetAddresses) public view returns (bool[] memory) { + function addressIsListedBatch(address[] calldata _targetAddresses) public view virtual returns (bool[] memory) { bool[] memory isListed = new bool[](_targetAddresses.length); for (uint256 i = 0; i < _targetAddresses.length; ++i) { isListed[i] = _addressIsListed(_targetAddresses[i]); @@ -114,6 +117,9 @@ abstract contract RuleAddressList is /* ============ ACCESS CONTROL ============ */ /** * @dev Returns `true` if `account` has been granted `role`. + * @param role The role identifier to check. + * @param account The account to check. + * @return True if the account holds the role (or is the default admin), false otherwise. */ function hasRole(bytes32 role, address account) public view virtual override(AccessControl) returns (bool) { // The Default Admin has all roles @@ -129,22 +135,25 @@ abstract contract RuleAddressList is /** * @dev This surcharge is not necessary if you do not use the ERC2771Module + * @return sender The transaction sender, unwrapped from the forwarder calldata when relayed. */ - function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) { + function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { return ERC2771Context._msgSender(); } /** * @dev This surcharge is not necessary if you do not use the ERC2771Module + * @return The transaction calldata, with the appended sender stripped when relayed. */ - function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) { + function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { return ERC2771Context._msgData(); } /** * @dev This surcharge is not necessary if you do not use the ERC2771Module + * @return The length of the ERC-2771 calldata suffix. */ - function _contextSuffixLength() internal view override(ERC2771Context, Context) returns (uint256) { + function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); } } diff --git a/src/mocks/rules/validation/abstract/RuleAddressList/RuleAddressListInternal.sol b/src/mocks/rules/validation/abstract/RuleAddressList/RuleAddressListInternal.sol index 0ade70c..824ba82 100644 --- a/src/mocks/rules/validation/abstract/RuleAddressList/RuleAddressListInternal.sol +++ b/src/mocks/rules/validation/abstract/RuleAddressList/RuleAddressListInternal.sol @@ -10,8 +10,14 @@ abstract contract RuleAddressListInternal { error Rulelist_AddressAlreadylisted(); error Rulelist_AddressNotPresent(); + /** + * @notice Membership flag per address. + */ mapping(address => bool) private list; // Number of addresses in the list at the moment + /** + * @notice Number of addresses currently in the list. + */ uint256 private numAddressesList; /** @@ -19,7 +25,7 @@ abstract contract RuleAddressListInternal { * If one of addresses already exist, there is no change for this address. The transaction remains valid (no revert). * @param listTargetAddresses an array with the addresses to list */ - function _addAddressesToThelist(address[] calldata listTargetAddresses) internal { + function _addAddressesToThelist(address[] calldata listTargetAddresses) internal virtual { uint256 numAddressesListLocal = numAddressesList; for (uint256 i = 0; i < listTargetAddresses.length; ++i) { if (!list[listTargetAddresses[i]]) { @@ -36,7 +42,7 @@ abstract contract RuleAddressListInternal { * The transaction remains valid (no revert). * @param listTargetAddresses an array with the addresses to remove */ - function _removeAddressesFromThelist(address[] calldata listTargetAddresses) internal { + function _removeAddressesFromThelist(address[] calldata listTargetAddresses) internal virtual { uint256 numAddressesListLocal = numAddressesList; for (uint256 i = 0; i < listTargetAddresses.length; ++i) { if (list[listTargetAddresses[i]]) { @@ -52,7 +58,7 @@ abstract contract RuleAddressListInternal { * If the address already exists, the transaction is reverted to save gas. * @param targetAddress The address to list */ - function _addAddressToThelist(address targetAddress) internal { + function _addAddressToThelist(address targetAddress) internal virtual { if (list[targetAddress]) { revert Rulelist_AddressAlreadylisted(); } @@ -66,7 +72,7 @@ abstract contract RuleAddressListInternal { * @param targetAddress The address to remove * */ - function _removeAddressFromThelist(address targetAddress) internal { + function _removeAddressFromThelist(address targetAddress) internal virtual { if (!list[targetAddress]) { revert Rulelist_AddressNotPresent(); } @@ -79,7 +85,7 @@ abstract contract RuleAddressListInternal { * @return Number of listed addresses * */ - function _numberListedAddress() internal view returns (uint256) { + function _numberListedAddress() internal view virtual returns (uint256) { return numAddressesList; } @@ -89,7 +95,7 @@ abstract contract RuleAddressListInternal { * @return True if the address is listed, false otherwise * */ - function _addressIsListed(address _targetAddress) internal view returns (bool) { + function _addressIsListed(address _targetAddress) internal view virtual returns (bool) { return list[_targetAddress]; } } diff --git a/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleAddressListInvariantStorage.sol b/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleAddressListInvariantStorage.sol index f82dfc6..6ff44a5 100644 --- a/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleAddressListInvariantStorage.sol +++ b/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleAddressListInvariantStorage.sol @@ -2,17 +2,47 @@ pragma solidity ^0.8.20; +/** + * @title RuleAddressListInvariantStorage + * @notice Events, errors and roles used by the address-list based rules. + */ abstract contract RuleAddressListInvariantStorage { /* ============ Events ============ */ + /** + * @notice Emitted when several addresses are added to the list. + * @param listTargetAddresses The addresses added to the list. + */ event AddAddressesToTheList(address[] listTargetAddresses); + + /** + * @notice Emitted when several addresses are removed from the list. + * @param listTargetAddresses The addresses removed from the list. + */ event RemoveAddressesFromTheList(address[] listTargetAddresses); + + /** + * @notice Emitted when a single address is added to the list. + * @param targetAddress The address added to the list. + */ event AddAddressToTheList(address targetAddress); + + /** + * @notice Emitted when a single address is removed from the list. + * @param targetAddress The address removed from the list. + */ event RemoveAddressFromTheList(address targetAddress); /* ============ Custom errors ============ */ error RuleAddressList_AdminWithAddressZeroNotAllowed(); /* ============ Role ============ */ + /** + * @notice Role allowed to remove addresses from the list. + */ bytes32 public constant ADDRESS_LIST_REMOVE_ROLE = keccak256("ADDRESS_LIST_REMOVE_ROLE"); + + /** + * @notice Role allowed to add addresses to the list. + */ bytes32 public constant ADDRESS_LIST_ADD_ROLE = keccak256("ADDRESS_LIST_ADD_ROLE"); } diff --git a/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleBlacklistInvariantStorage.sol b/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleBlacklistInvariantStorage.sol index 3d3dbe4..b7f4b0f 100644 --- a/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleBlacklistInvariantStorage.sol +++ b/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleBlacklistInvariantStorage.sol @@ -2,18 +2,39 @@ pragma solidity ^0.8.20; -// forge-lint: disable-next-line(unaliased-plain-import) -import "../../RuleCommonInvariantStorage.sol"; +import {RuleCommonInvariantStorage} from "../../RuleCommonInvariantStorage.sol"; +/** + * @title RuleBlacklistInvariantStorage + * @notice Restriction codes and messages for the blacklist rule. + */ abstract contract RuleBlacklistInvariantStorage is RuleCommonInvariantStorage { /* ============ String message ============ */ + /** + * @notice Message returned when the sender is blacklisted. + */ string constant TEXT_ADDRESS_FROM_IS_BLACKLISTED = "The sender is blacklisted"; + /** + * @notice Message returned when the recipient is blacklisted. + */ string constant TEXT_ADDRESS_TO_IS_BLACKLISTED = "The recipient is blacklisted"; + /** + * @notice Message returned when the spender is blacklisted. + */ string constant TEXT_ADDRESS_SPENDER_IS_BLACKLISTED = "The spender is blacklisted"; /* ============ Code ============ */ // It is very important that each rule uses an unique code + /** + * @notice Restriction code raised when the sender is blacklisted. + */ uint8 public constant CODE_ADDRESS_FROM_IS_BLACKLISTED = 41; + /** + * @notice Restriction code raised when the recipient is blacklisted. + */ uint8 public constant CODE_ADDRESS_TO_IS_BLACKLISTED = 42; + /** + * @notice Restriction code raised when the spender is blacklisted. + */ uint8 public constant CODE_ADDRESS_SPENDER_IS_BLACKLISTED = 43; } diff --git a/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleWhitelistInvariantStorage.sol b/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleWhitelistInvariantStorage.sol index d1e218b..65685bb 100644 --- a/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleWhitelistInvariantStorage.sol +++ b/src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleWhitelistInvariantStorage.sol @@ -2,18 +2,39 @@ pragma solidity ^0.8.20; -// forge-lint: disable-next-line(unaliased-plain-import) -import "../../RuleCommonInvariantStorage.sol"; +import {RuleCommonInvariantStorage} from "../../RuleCommonInvariantStorage.sol"; +/** + * @title RuleWhitelistInvariantStorage + * @notice Restriction codes and messages for the whitelist rule. + */ abstract contract RuleWhitelistInvariantStorage is RuleCommonInvariantStorage { /* ============ String message ============ */ + /** + * @notice Message returned when the sender is not whitelisted. + */ string constant TEXT_ADDRESS_FROM_NOT_WHITELISTED = "The sender is not in the whitelist"; + /** + * @notice Message returned when the recipient is not whitelisted. + */ string constant TEXT_ADDRESS_TO_NOT_WHITELISTED = "The recipient is not in the whitelist"; + /** + * @notice Message returned when the spender is not whitelisted. + */ string constant TEXT_ADDRESS_SPENDER_NOT_WHITELISTED = "The spender is not in the whitelist"; /* ============ Code ============ */ // It is very important that each rule uses an unique code + /** + * @notice Restriction code raised when the sender is not whitelisted. + */ uint8 public constant CODE_ADDRESS_FROM_NOT_WHITELISTED = 21; + /** + * @notice Restriction code raised when the recipient is not whitelisted. + */ uint8 public constant CODE_ADDRESS_TO_NOT_WHITELISTED = 22; + /** + * @notice Restriction code raised when the spender is not whitelisted. + */ uint8 public constant CODE_ADDRESS_SPENDER_NOT_WHITELISTED = 23; } diff --git a/src/mocks/rules/validation/abstract/RuleCommonInvariantStorage.sol b/src/mocks/rules/validation/abstract/RuleCommonInvariantStorage.sol index 855e72f..1929f87 100644 --- a/src/mocks/rules/validation/abstract/RuleCommonInvariantStorage.sol +++ b/src/mocks/rules/validation/abstract/RuleCommonInvariantStorage.sol @@ -1,7 +1,21 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title RuleCommonInvariantStorage + * @notice Restriction messages shared by every reference rule. + */ abstract contract RuleCommonInvariantStorage { // Text + /** + * @notice Message returned when no rule claims the queried restriction code. + */ string constant TEXT_CODE_NOT_FOUND = "Unknown restriction code"; + + // ERC-1404 reserves the code 0 as the "no restriction" sentinel + // Same message as the one returned by CMTAT (ValidationModuleERC1404) + /** + * @notice Message returned for the reserved ERC-1404 code 0 (no restriction). + */ + string constant TEXT_TRANSFER_OK = "NoRestriction"; } diff --git a/src/mocks/rules/validation/abstract/RuleWhitelistCommon.sol b/src/mocks/rules/validation/abstract/RuleWhitelistCommon.sol index 2622057..03a4838 100644 --- a/src/mocks/rules/validation/abstract/RuleWhitelistCommon.sol +++ b/src/mocks/rules/validation/abstract/RuleWhitelistCommon.sol @@ -2,10 +2,13 @@ pragma solidity ^0.8.20; -// forge-lint: disable-next-line(unaliased-plain-import) -import "./RuleAddressList/invariantStorage/RuleWhitelistInvariantStorage.sol"; +import {RuleWhitelistInvariantStorage} from "./RuleAddressList/invariantStorage/RuleWhitelistInvariantStorage.sol"; import {IRule} from "../../../../interfaces/IRule.sol"; +/** + * @title RuleWhitelistCommon + * @notice Shared restriction-code helpers for the whitelist rules. + */ abstract contract RuleWhitelistCommon is RuleWhitelistInvariantStorage, IRule { /** * @notice To know if the restriction code is valid for this rule or not @@ -26,7 +29,9 @@ abstract contract RuleWhitelistCommon is RuleWhitelistInvariantStorage, IRule { * */ function messageForTransferRestriction(uint8 restrictionCode) external pure override returns (string memory) { - if (restrictionCode == CODE_ADDRESS_FROM_NOT_WHITELISTED) { + if (restrictionCode == uint8(REJECTED_CODE_BASE.TRANSFER_OK)) { + return TEXT_TRANSFER_OK; + } else if (restrictionCode == CODE_ADDRESS_FROM_NOT_WHITELISTED) { return TEXT_ADDRESS_FROM_NOT_WHITELISTED; } else if (restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED) { return TEXT_ADDRESS_TO_NOT_WHITELISTED; diff --git a/src/modules/ERC2771ModuleStandalone.sol b/src/modules/ERC2771ModuleStandalone.sol index 98f8af9..5570e10 100644 --- a/src/modules/ERC2771ModuleStandalone.sol +++ b/src/modules/ERC2771ModuleStandalone.sol @@ -9,6 +9,11 @@ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol" * @dev Meta transaction (gasless) module. */ abstract contract ERC2771ModuleStandalone is ERC2771Context { + /** + * @notice Sets the trusted ERC-2771 forwarder. + * @dev The forwarder is immutable: it cannot be changed after construction. + * @param trustedForwarder Address of the trusted forwarder. + */ constructor(address trustedForwarder) ERC2771Context(trustedForwarder) { // Nothing to do } diff --git a/src/modules/ERC3643ComplianceExtendedModule.sol b/src/modules/ERC3643ComplianceExtendedModule.sol index 5f7b3c4..4cbdd6f 100644 --- a/src/modules/ERC3643ComplianceExtendedModule.sol +++ b/src/modules/ERC3643ComplianceExtendedModule.sol @@ -7,9 +7,16 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {IERC3643ComplianceExtended} from "../interfaces/IERC3643ComplianceExtended.sol"; import {ERC3643ComplianceModule} from "./ERC3643ComplianceModule.sol"; +/** + * @title ERC3643ComplianceExtendedModule + * @notice Extends the core ERC-3643 compliance module with batch binding and token self-binding. + */ abstract contract ERC3643ComplianceExtendedModule is ERC3643ComplianceModule, IERC3643ComplianceExtended { using EnumerableSet for EnumerableSet.AddressSet; + /** + * @notice Tracks which tokens are allowed to bind and unbind themselves. + */ mapping(address token => bool approved) private _tokenSelfBindingApproval; /** @@ -66,6 +73,7 @@ abstract contract ERC3643ComplianceExtendedModule is ERC3643ComplianceModule, IE /** * @dev Authorizes bind/unbind operations. * Allows compliance manager, or approved token self-calls for T-REX compatibility. + * @param token The token being bound or unbound. */ function _authorizeComplianceBindingChange(address token) internal virtual override { if (_msgSender() == token && _tokenSelfBindingApproval[token]) { diff --git a/src/modules/ERC3643ComplianceModule.sol b/src/modules/ERC3643ComplianceModule.sol index f0bb51d..5473f1d 100644 --- a/src/modules/ERC3643ComplianceModule.sol +++ b/src/modules/ERC3643ComplianceModule.sol @@ -9,15 +9,18 @@ import {Context} from "@openzeppelin/contracts/utils/Context.sol"; import {IERC3643Compliance} from "../interfaces/IERC3643Compliance.sol"; import {ERC3643ComplianceModuleInvariantStorage} from "./library/ERC3643ComplianceModuleInvariantStorage.sol"; -abstract contract ERC3643ComplianceModule is - Context, - IERC3643Compliance, - ERC3643ComplianceModuleInvariantStorage -{ +/** + * @title ERC3643ComplianceModule + * @notice Core ERC-3643 compliance module: tracks the tokens bound to this engine. + */ +abstract contract ERC3643ComplianceModule is Context, IERC3643Compliance, ERC3643ComplianceModuleInvariantStorage { /* ==== Type declaration === */ using EnumerableSet for EnumerableSet.AddressSet; /* ==== State Variables === */ // Token binding tracking + /** + * @notice Set of tokens allowed to call the compliance callbacks. + */ EnumerableSet.AddressSet internal _boundTokens; /* ==== Modifier === */ @@ -41,8 +44,8 @@ abstract contract ERC3643ComplianceModule is * @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. - * @custom:security-note Operation rules (stateful rules such as `RuleConditionalTransferLight` - * or `RuleMintAllowance`) maintain per-address accounting that is shared across all bound tokens. + * @custom:security-note Operation rules (stateful rules such as `RuleConditionalTransferLightMock` + * or `RuleMintAllowanceMock`) maintain per-address accounting that is shared across all bound tokens. * Binding tokens from different issuers to the same engine will silently cross-contaminate * their accounting. Only bind tokens that are equally trusted and governed together. */ @@ -72,7 +75,7 @@ abstract contract ERC3643ComplianceModule is if (_boundTokens.length() > 0) { // Note that there are no guarantees on the ordering of values inside the array, // and it may change when more values are added or removed. - return _boundTokens.at(0); + return _boundTokens.pos(0); } else { return address(0); } @@ -82,28 +85,47 @@ abstract contract ERC3643ComplianceModule is INTERNAL/PRIVATE FUNCTIONS //////////////////////////////////////////////////////////////*/ - function _unbindToken(address token) internal { - require(_boundTokens.contains(token), RuleEngine_ERC3643Compliance_TokenNotBound()); - // Should never revert because we check if the token address is already set before - require(_boundTokens.remove(token), RuleEngine_ERC3643Compliance_OperationNotSuccessful()); + /** + * @dev Removes a token from the bound set. + * @param token The token to unbind; reverts when it is not currently bound. + */ + function _unbindToken(address token) internal virtual { + // remove() returns false when the token was not bound, so a separate + // contains() lookup is unnecessary. + require(_boundTokens.remove(token), RuleEngine_ERC3643Compliance_TokenNotBound()); emit TokenUnbound(token); } - function _bindToken(address token) internal { + /** + * @dev Adds a token to the bound set. + * @param token The token to bind; reverts on the zero address or when already bound. + */ + function _bindToken(address token) internal virtual { require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress()); - require(!_boundTokens.contains(token), RuleEngine_ERC3643Compliance_TokenAlreadyBound()); - // Should never revert because we check if the token address is already set before - require(_boundTokens.add(token), RuleEngine_ERC3643Compliance_OperationNotSuccessful()); + // add() returns false when the token is already bound, so a separate + // contains() lookup is unnecessary. + require(_boundTokens.add(token), RuleEngine_ERC3643Compliance_TokenAlreadyBound()); emit TokenBound(token); } + /** + * @dev Authorization hook for bind/unbind, implemented by the deployable contracts. + * @param token The token being bound or unbound. + */ + function _authorizeComplianceBindingChange(address token) internal virtual; + + /** + * @dev Access control hook guarding compliance management operations. + */ + function _onlyComplianceManager() internal virtual; + + /** + * @dev Reverts when the caller is not a bound token. + */ function _checkBoundToken() internal view virtual { if (!_boundTokens.contains(_msgSender())) { revert RuleEngine_ERC3643Compliance_UnauthorizedCaller(); } } - - function _authorizeComplianceBindingChange(address token) internal virtual; - function _onlyComplianceManager() internal virtual; } diff --git a/src/modules/RulesManagementModule.sol b/src/modules/RulesManagementModule.sol index 8551f95..317199f 100644 --- a/src/modules/RulesManagementModule.sol +++ b/src/modules/RulesManagementModule.sol @@ -60,10 +60,7 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage _clearRules(); } for (uint256 i = 0; i < rules_.length; ++i) { - _checkRule(address(rules_[i])); - // Should never revert because we check the presence of the rule before - require(_rules.add(address(rules_[i])), RuleEngine_RulesManagementModule_OperationNotSuccessful()); - emit AddRule(rules_[i]); + _addRule(rules_[i]); } } @@ -83,27 +80,14 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage 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; + _addRule(rule_); } /** * @inheritdoc IRulesManagementModule */ function setMaxRules(uint256 maxRules_) public virtual override(IRulesManagementModule) onlyRulesLimitManager { - if (maxRules_ == 0) { - revert RuleEngine_RulesManagementModule_MaxRulesZeroNotAllowed(); - } - _maxRules = maxRules_; - emit SetMaxRules(maxRules_); + _setMaxRules(maxRules_); } /** @@ -115,6 +99,12 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage } /* ============ View functions ============ */ + /** + * @inheritdoc IRulesManagementModule + */ + function maxRules() public view virtual override(IRulesManagementModule) returns (uint256) { + return _maxRules; + } /** * @inheritdoc IRulesManagementModule @@ -137,7 +127,7 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage if (ruleId < _rules.length()) { // Note that there are no guarantees on the ordering of values inside the array, // and it may change when more values are added or removed. - return _rules.at(ruleId); + return _rules.pos(ruleId); } else { return address(0); } @@ -162,6 +152,36 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage _rules.clear(); } + /** + * @notice Set the maximum number of rules and emit the corresponding event + * @dev Single point where `_maxRules` is written, so the invariant "every change to the cap emits + * {SetMaxRules}" holds structurally rather than by convention. Called by {setMaxRules} and by the + * deployable contracts' constructors, which emit the initial cap so the event log alone is enough to + * reconstruct it. + * @param maxRules_ New maximum number of rules; must not be zero. + */ + function _setMaxRules(uint256 maxRules_) internal virtual { + if (maxRules_ == 0) { + revert RuleEngine_RulesManagementModule_MaxRulesZeroNotAllowed(); + } + _maxRules = maxRules_; + emit SetMaxRules(maxRules_); + } + + /** + * @notice Validate a rule, add it to the array of rules and emit the corresponding event + * @dev Single point where a rule is inserted, so the invariant "every rule added emits {AddRule}" holds + * structurally. The `maxRules` cap is deliberately *not* checked here: {addRule} checks it per insertion + * while {setRules} checks the whole batch up front, so the two callers need different cap logic. + * @param rule_ The rule to validate and add. + */ + function _addRule(IRule rule_) internal virtual { + _checkRule(address(rule_)); + // Should never revert because we check the presence of the rule before + require(_rules.add(address(rule_)), RuleEngine_RulesManagementModule_OperationNotSuccessful()); + emit AddRule(rule_); + } + /** * @notice Remove a rule from the array of rules * Revert if the rule found at the specified index does not match the rule in argument @@ -175,18 +195,6 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage emit RemoveRule(rule_); } - /** - * @dev check if a rule is valid, revert otherwise - */ - function _checkRule(address rule_) internal view virtual { - if (rule_ == address(0x0)) { - revert RuleEngine_RulesManagementModule_RuleAddressZeroNotAllowed(); - } - if (_rules.contains(rule_)) { - revert RuleEngine_RulesManagementModule_RuleAlreadyExists(); - } - } - /* ============ Transferred functions ============ */ /** @@ -203,7 +211,7 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage function _transferred(address from, address to, uint256 value) internal virtual { uint256 rulesLength = _rules.length(); for (uint256 i = 0; i < rulesLength; ++i) { - IRule(_rules.at(i)).transferred(from, to, value); + IRule(_rules.pos(i)).transferred(from, to, value); } } @@ -222,10 +230,30 @@ abstract contract RulesManagementModule is RulesManagementModuleInvariantStorage function _transferred(address spender, address from, address to, uint256 value) internal virtual { uint256 rulesLength = _rules.length(); for (uint256 i = 0; i < rulesLength; ++i) { - IRule(_rules.at(i)).transferred(spender, from, to, value); + IRule(_rules.pos(i)).transferred(spender, from, to, value); } } + /** + * @dev Access control hook guarding rule management operations. + */ function _onlyRulesManager() internal virtual; + + /** + * @dev Access control hook guarding updates to the rule cap. + */ function _onlyRulesLimitManager() internal virtual; + + /** + * @dev check if a rule is valid, revert otherwise + * @param rule_ The candidate rule address to validate. + */ + function _checkRule(address rule_) internal view virtual { + if (rule_ == address(0x0)) { + revert RuleEngine_RulesManagementModule_RuleAddressZeroNotAllowed(); + } + if (_rules.contains(rule_)) { + revert RuleEngine_RulesManagementModule_RuleAlreadyExists(); + } + } } diff --git a/src/modules/VersionModule.sol b/src/modules/VersionModule.sol index 8b57d1b..975f11b 100644 --- a/src/modules/VersionModule.sol +++ b/src/modules/VersionModule.sol @@ -5,6 +5,10 @@ pragma solidity ^0.8.20; /* ==== CMTAT === */ import {IERC3643Version} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; +/** + * @title VersionModule + * @notice Exposes the RuleEngine release version. + */ abstract contract VersionModule is IERC3643Version { /* ============ State Variables ============ */ /** diff --git a/src/modules/library/ComplianceInterfaceId.sol b/src/modules/library/ComplianceInterfaceId.sol index 477948a..e4f4c29 100644 --- a/src/modules/library/ComplianceInterfaceId.sol +++ b/src/modules/library/ComplianceInterfaceId.sol @@ -7,7 +7,18 @@ pragma solidity ^0.8.20; * @dev ERC-165 interface IDs used by RuleEngine for compliance interfaces. */ library ComplianceInterfaceId { + /** + * @notice ERC-165 interface ID of the core ERC-3643 compliance interface. + */ bytes4 public constant ERC3643_COMPLIANCE_INTERFACE_ID = 0x3144991c; + + /** + * @notice ERC-165 interface ID of the extended ERC-3643 compliance interface. + */ bytes4 public constant ERC3643_COMPLIANCE_EXTENDED_INTERFACE_ID = 0x646ba2be; + + /** + * @notice ERC-165 interface ID of the ERC-7551 compliance interface. + */ bytes4 public constant IERC7551_COMPLIANCE_INTERFACE_ID = 0x7157797f; } diff --git a/src/modules/library/ERC1404InterfaceId.sol b/src/modules/library/ERC1404InterfaceId.sol index b4bd60e..48d375c 100644 --- a/src/modules/library/ERC1404InterfaceId.sol +++ b/src/modules/library/ERC1404InterfaceId.sol @@ -7,5 +7,8 @@ pragma solidity ^0.8.20; * @dev ERC-165 interface IDs for ERC-1404 interfaces. */ library ERC1404InterfaceId { + /** + * @notice ERC-165 interface ID of the ERC-1404 restriction interface. + */ bytes4 public constant IERC1404_INTERFACE_ID = 0xab84a5c8; } diff --git a/src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol b/src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol index c32a06b..d6c9c04 100644 --- a/src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol +++ b/src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol @@ -2,11 +2,14 @@ pragma solidity ^0.8.20; +/** + * @title ERC3643ComplianceModuleInvariantStorage + * @notice Holds the custom errors raised by the ERC-3643 compliance module. + */ abstract contract ERC3643ComplianceModuleInvariantStorage { /* ==== Errors === */ error RuleEngine_ERC3643Compliance_InvalidTokenAddress(); error RuleEngine_ERC3643Compliance_TokenAlreadyBound(); error RuleEngine_ERC3643Compliance_TokenNotBound(); error RuleEngine_ERC3643Compliance_UnauthorizedCaller(); - error RuleEngine_ERC3643Compliance_OperationNotSuccessful(); } diff --git a/src/modules/library/ERC3643ComplianceRolesStorage.sol b/src/modules/library/ERC3643ComplianceRolesStorage.sol index b7d007a..1149b56 100644 --- a/src/modules/library/ERC3643ComplianceRolesStorage.sol +++ b/src/modules/library/ERC3643ComplianceRolesStorage.sol @@ -2,7 +2,14 @@ pragma solidity ^0.8.20; +/** + * @title ERC3643ComplianceRolesStorage + * @notice Holds the RBAC role identifier used by the ERC-3643 compliance module. + */ abstract contract ERC3643ComplianceRolesStorage { /* ==== Role === */ + /** + * @notice Role allowed to bind and unbind tokens on the compliance contract. + */ bytes32 public constant COMPLIANCE_MANAGER_ROLE = keccak256("COMPLIANCE_MANAGER_ROLE"); } diff --git a/src/modules/library/Ownable2StepInterfaceId.sol b/src/modules/library/Ownable2StepInterfaceId.sol index a04a4c7..9d1c80f 100644 --- a/src/modules/library/Ownable2StepInterfaceId.sol +++ b/src/modules/library/Ownable2StepInterfaceId.sol @@ -7,6 +7,9 @@ pragma solidity ^0.8.20; * @dev ERC-165 interface ID for Ownable2Step-specific functions only. */ library Ownable2StepInterfaceId { - // bytes4(keccak256("acceptOwnership()")) ^ bytes4(keccak256("pendingOwner()")) + /** + * @notice ERC-165 interface ID of the Ownable2Step-specific functions. + * @dev 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 index af9bb7d..d66e818 100644 --- a/src/modules/library/OwnableInterfaceId.sol +++ b/src/modules/library/OwnableInterfaceId.sol @@ -7,5 +7,8 @@ pragma solidity ^0.8.20; * @dev ERC-165 interface IDs used by ownable RuleEngine variants. */ library OwnableInterfaceId { + /** + * @notice ERC-165 interface ID of ERC-173 (contract ownership). + */ bytes4 public constant IERC173_INTERFACE_ID = 0x7f5828d0; } diff --git a/src/modules/library/RuleEngineInvariantStorage.sol b/src/modules/library/RuleEngineInvariantStorage.sol index e2d9b5f..cc58ef6 100644 --- a/src/modules/library/RuleEngineInvariantStorage.sol +++ b/src/modules/library/RuleEngineInvariantStorage.sol @@ -2,6 +2,10 @@ pragma solidity ^0.8.20; +/** + * @title RuleEngineInvariantStorage + * @notice Holds the custom errors shared by the RuleEngine deployable contracts. + */ abstract contract RuleEngineInvariantStorage { /* ==== Errors === */ error RuleEngine_AdminWithAddressZeroNotAllowed(); diff --git a/src/modules/library/RuleInterfaceId.sol b/src/modules/library/RuleInterfaceId.sol index d967ae3..8456240 100644 --- a/src/modules/library/RuleInterfaceId.sol +++ b/src/modules/library/RuleInterfaceId.sol @@ -9,5 +9,8 @@ pragma solidity ^0.8.20; * See src/mocks/IRuleInterfaceIdHelper.sol for the detailed computation. */ library RuleInterfaceId { + /** + * @notice ERC-165 interface ID advertised by every rule usable by the RuleEngine. + */ bytes4 public constant IRULE_INTERFACE_ID = 0x2497d6cb; } diff --git a/src/modules/library/RulesManagementModuleInvariantStorage.sol b/src/modules/library/RulesManagementModuleInvariantStorage.sol index 1bada7a..19432d3 100644 --- a/src/modules/library/RulesManagementModuleInvariantStorage.sol +++ b/src/modules/library/RulesManagementModuleInvariantStorage.sol @@ -4,7 +4,15 @@ pragma solidity ^0.8.20; import {IRule} from "../../interfaces/IRule.sol"; +/** + * @title RulesManagementModuleInvariantStorage + * @notice Holds the errors, events and default limits used by the rules management module. + */ abstract contract RulesManagementModuleInvariantStorage { + /** + * @notice Default upper bound on the number of rules a RuleEngine may hold. + * @dev Bounds the per-transfer gas cost of iterating the rule set. + */ uint256 public constant DEFAULT_MAX_RULES = 10; /* ==== Errors === */ @@ -40,5 +48,4 @@ abstract contract RulesManagementModuleInvariantStorage { * @param maxRules The new rule cap. */ event SetMaxRules(uint256 maxRules); - } diff --git a/src/modules/library/RulesManagementModuleRolesStorage.sol b/src/modules/library/RulesManagementModuleRolesStorage.sol index f26ca5f..9da0ade 100644 --- a/src/modules/library/RulesManagementModuleRolesStorage.sol +++ b/src/modules/library/RulesManagementModuleRolesStorage.sol @@ -2,7 +2,14 @@ pragma solidity ^0.8.20; +/** + * @title RulesManagementModuleRolesStorage + * @notice Holds the RBAC role identifier used by the rules management module. + */ abstract contract RulesManagementModuleRolesStorage { /* ==== Role === */ + /** + * @notice Role allowed to add, remove, set and clear rules. + */ bytes32 public constant RULES_MANAGEMENT_ROLE = keccak256("RULES_MANAGEMENT_ROLE"); } diff --git a/test/HelperContract.sol b/test/HelperContract.sol index 7f68e27..ca4ddb4 100644 --- a/test/HelperContract.sol +++ b/test/HelperContract.sol @@ -15,12 +15,12 @@ import {RulesManagementModule} from "src/RuleEngineBase.sol"; // forge-lint: disable-next-line(unused-import) import {ERC3643ComplianceModuleInvariantStorage} from "src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol"; // RuleConditionalTransfer -import {RuleConditionalTransferLight} from "src/mocks/rules/operation/RuleConditionalTransferLight.sol"; +import {RuleConditionalTransferLightMock} from "src/mocks/rules/operation/RuleConditionalTransferLightMock.sol"; import { RuleConditionalTransferLightInvariantStorage } from "src/mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol"; -// RuleWhitelist -import {RuleWhitelist} from "src/mocks/rules/validation/RuleWhitelist.sol"; +// RuleWhitelistMock +import {RuleWhitelistMock} from "src/mocks/rules/validation/RuleWhitelistMock.sol"; import { RuleWhitelistInvariantStorage } from "src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleWhitelistInvariantStorage.sol"; @@ -69,8 +69,8 @@ abstract contract HelperContract is string constant DEFAULT_ADMIN_ROLE_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000"; // contract - RuleWhitelist public ruleWhitelist; - RuleConditionalTransferLight public ruleConditionalTransferLight; + RuleWhitelistMock public ruleWhitelist; + RuleConditionalTransferLightMock public ruleConditionalTransferLight; // CMTAT CMTATDeployment cmtatDeployment; @@ -85,7 +85,7 @@ abstract contract HelperContract is uint8 codeNonexistent = 255; // Defined in CMTAT.sol uint8 constant TRANSFER_OK = 0; - string constant TEXT_TRANSFER_OK = "NoRestriction"; + // TEXT_TRANSFER_OK ("NoRestriction") comes from RuleCommonInvariantStorage // Forwarder string ERC2771ForwarderDomain = "ERC2771ForwarderDomain"; diff --git a/test/HelperContractOwnable.sol b/test/HelperContractOwnable.sol index cad8d6a..15e788c 100644 --- a/test/HelperContractOwnable.sol +++ b/test/HelperContractOwnable.sol @@ -14,12 +14,12 @@ import {RulesManagementModule} from "src/RuleEngineBase.sol"; // forge-lint: disable-next-line(unused-import) import {ERC3643ComplianceModuleInvariantStorage} from "src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol"; // RuleConditionalTransfer -import {RuleConditionalTransferLight} from "src/mocks/rules/operation/RuleConditionalTransferLight.sol"; +import {RuleConditionalTransferLightMock} from "src/mocks/rules/operation/RuleConditionalTransferLightMock.sol"; import { RuleConditionalTransferLightInvariantStorage } from "src/mocks/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol"; -// RuleWhitelist -import {RuleWhitelist} from "src/mocks/rules/validation/RuleWhitelist.sol"; +// RuleWhitelistMock +import {RuleWhitelistMock} from "src/mocks/rules/validation/RuleWhitelistMock.sol"; import { RuleWhitelistInvariantStorage } from "src/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleWhitelistInvariantStorage.sol"; @@ -64,8 +64,8 @@ abstract contract HelperContractOwnable is address constant NEW_OWNER_ADDRESS = address(8); // contract - RuleWhitelist public ruleWhitelist; - RuleConditionalTransferLight public ruleConditionalTransferLight; + RuleWhitelistMock public ruleWhitelist; + RuleConditionalTransferLightMock public ruleConditionalTransferLight; // CMTAT CMTATDeployment cmtatDeployment; @@ -78,7 +78,7 @@ abstract contract HelperContractOwnable is uint8 codeNonexistent = 255; // Defined in CMTAT.sol uint8 constant TRANSFER_OK = 0; - string constant TEXT_TRANSFER_OK = "NoRestriction"; + // TEXT_TRANSFER_OK ("NoRestriction") comes from RuleCommonInvariantStorage // Forwarder string ERC2771ForwarderDomain = "ERC2771ForwarderDomain"; diff --git a/test/HelperContractOwnable2Step.sol b/test/HelperContractOwnable2Step.sol index ace2ff8..a5ad93a 100644 --- a/test/HelperContractOwnable2Step.sol +++ b/test/HelperContractOwnable2Step.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import {RuleEngineOwnable2Step} from "src/deployment/RuleEngineOwnable2Step.sol"; -import {RuleConditionalTransferLight} from "src/mocks/rules/operation/RuleConditionalTransferLight.sol"; +import {RuleConditionalTransferLightMock} from "src/mocks/rules/operation/RuleConditionalTransferLightMock.sol"; /** * @title Constants used by tests for RuleEngineOwnable2Step @@ -15,7 +15,13 @@ abstract contract HelperContractOwnable2Step { address internal constant CONDITIONAL_TRANSFER_OPERATOR_ADDRESS = address(9); RuleEngineOwnable2Step public ruleEngineMock; - RuleConditionalTransferLight public ruleConditionalTransferLight; + RuleConditionalTransferLightMock public ruleConditionalTransferLight; string internal constant ERC2771_FORWARDER_DOMAIN = "ERC2771ForwarderDomain"; + + // ERC-1404 restriction codes and messages + uint8 internal constant TRANSFER_OK = 0; + uint8 internal constant CODE_NONEXISTENT = 255; + string internal constant TEXT_TRANSFER_OK = "NoRestriction"; + string internal constant TEXT_CODE_NOT_FOUND = "Unknown restriction code"; } diff --git a/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol b/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol index 9664f9f..e28845c 100644 --- a/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol +++ b/test/RuleEngine/AccessControl/RuleEngineAccessControl.sol @@ -14,7 +14,7 @@ contract RuleEngineTest is Test, HelperContract { // Arrange function setUp() public { - ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + ruleWhitelist = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock = new RuleEngine(RULE_ENGINE_OPERATOR_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); resUint256 = ruleEngineMock.rulesCount(); @@ -29,9 +29,9 @@ contract RuleEngineTest is Test, HelperContract { function testCannotAttackerSetRules() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist2 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist2 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); IRule[] memory ruleWhitelistTab = new IRule[](2); ruleWhitelistTab[0] = ruleWhitelist1; ruleWhitelistTab[1] = ruleWhitelist2; @@ -91,7 +91,9 @@ contract RuleEngineTest is Test, HelperContract { function testCannotAttackerOperateOnTransfer() public { // Act vm.prank(ATTACKER); - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector + ); ruleEngineMock.transferred(address(0), ADDRESS1, ADDRESS2, 10); } diff --git a/test/RuleEngine/AccessControl/RuleEngineAccessControlOZ.t.sol b/test/RuleEngine/AccessControl/RuleEngineAccessControlOZ.t.sol index 7634d7f..02f4223 100644 --- a/test/RuleEngine/AccessControl/RuleEngineAccessControlOZ.t.sol +++ b/test/RuleEngine/AccessControl/RuleEngineAccessControlOZ.t.sol @@ -12,7 +12,7 @@ import "../../HelperContract.sol"; contract RuleEngineTest is Test, HelperContract, AccessControl { // Arrange function setUp() public { - ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + ruleWhitelist = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock = new RuleEngine(RULE_ENGINE_OPERATOR_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); resUint256 = ruleEngineMock.rulesCount(); diff --git a/test/RuleEngine/ERC3643Compliance.t.sol b/test/RuleEngine/ERC3643Compliance.t.sol index 5a53655..d9c075d 100644 --- a/test/RuleEngine/ERC3643Compliance.t.sol +++ b/test/RuleEngine/ERC3643Compliance.t.sol @@ -6,7 +6,9 @@ import {Vm} from "forge-std/Vm.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../HelperContract.sol"; import {IERC3643Compliance} from "../../src/interfaces/IERC3643Compliance.sol"; -import {ERC3643ComplianceModuleInvariantStorage} from "../../src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol"; +import { + ERC3643ComplianceModuleInvariantStorage +} from "../../src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol"; // Minimal mock ERC-3643 token to simulate calls to RuleEngine contract ERC3643MockToken { @@ -166,7 +168,9 @@ contract RuleEngineTest is Test, HelperContract { } function testCannotBoundIfInvalidAddress() public { - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector + ); vm.prank(admin); ruleEngine.bindToken(address(ZERO_ADDRESS)); } @@ -214,9 +218,7 @@ contract RuleEngineTest is Test, HelperContract { function testTokenCannotBindItselfWithoutApproval() public { vm.expectRevert( abi.encodeWithSelector( - ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, - address(token1), - ruleEngine.COMPLIANCE_MANAGER_ROLE() + ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, address(token1), ruleEngine.COMPLIANCE_MANAGER_ROLE() ) ); vm.prank(address(token1)); @@ -229,9 +231,7 @@ contract RuleEngineTest is Test, HelperContract { vm.expectRevert( abi.encodeWithSelector( - ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, - address(token1), - ruleEngine.COMPLIANCE_MANAGER_ROLE() + ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, address(token1), ruleEngine.COMPLIANCE_MANAGER_ROLE() ) ); vm.prank(address(token1)); @@ -241,9 +241,7 @@ contract RuleEngineTest is Test, HelperContract { function testTokenCannotBindAnotherToken() public { vm.expectRevert( abi.encodeWithSelector( - ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, - address(token1), - ruleEngine.COMPLIANCE_MANAGER_ROLE() + ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, address(token1), ruleEngine.COMPLIANCE_MANAGER_ROLE() ) ); vm.prank(address(token1)); @@ -256,9 +254,7 @@ contract RuleEngineTest is Test, HelperContract { vm.expectRevert( abi.encodeWithSelector( - ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, - address(token1), - ruleEngine.COMPLIANCE_MANAGER_ROLE() + ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, address(token1), ruleEngine.COMPLIANCE_MANAGER_ROLE() ) ); vm.prank(address(token1)); @@ -268,9 +264,7 @@ contract RuleEngineTest is Test, HelperContract { function testOnlyComplianceManagerCanSetTokenSelfBindingApproval() public { vm.expectRevert( abi.encodeWithSelector( - ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, - user1, - ruleEngine.COMPLIANCE_MANAGER_ROLE() + ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, user1, ruleEngine.COMPLIANCE_MANAGER_ROLE() ) ); vm.prank(user1); @@ -278,7 +272,9 @@ contract RuleEngineTest is Test, HelperContract { } function testCannotSetTokenSelfBindingApprovalForZeroAddress() public { - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector + ); vm.prank(operator); ruleEngine.setTokenSelfBindingApproval(address(0), true); } @@ -315,9 +311,7 @@ contract RuleEngineTest is Test, HelperContract { vm.expectRevert( abi.encodeWithSelector( - ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, - user1, - ruleEngine.COMPLIANCE_MANAGER_ROLE() + ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, user1, ruleEngine.COMPLIANCE_MANAGER_ROLE() ) ); vm.prank(user1); @@ -329,7 +323,9 @@ contract RuleEngineTest is Test, HelperContract { tokens[0] = address(token1); tokens[1] = address(0); - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector + ); vm.prank(operator); ruleEngine.setTokenSelfBindingApprovalBatch(tokens, true); } @@ -366,9 +362,7 @@ contract RuleEngineTest is Test, HelperContract { vm.expectRevert( abi.encodeWithSelector( - ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, - user1, - ruleEngine.COMPLIANCE_MANAGER_ROLE() + ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, user1, ruleEngine.COMPLIANCE_MANAGER_ROLE() ) ); vm.prank(user1); @@ -384,9 +378,7 @@ contract RuleEngineTest is Test, HelperContract { vm.expectRevert( abi.encodeWithSelector( - ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, - user1, - ruleEngine.COMPLIANCE_MANAGER_ROLE() + ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR, user1, ruleEngine.COMPLIANCE_MANAGER_ROLE() ) ); vm.prank(user1); @@ -398,7 +390,9 @@ contract RuleEngineTest is Test, HelperContract { tokens[0] = address(token1); tokens[1] = address(0); - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector + ); vm.prank(operator); ruleEngine.bindTokens(tokens); } @@ -430,17 +424,23 @@ contract RuleEngineTest is Test, HelperContract { } function testCannotCreatedIfNotBound() public { - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector + ); ruleEngine.created(user1, 100); } function testCannotDestroyedIfNotBound() public { - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector + ); ruleEngine.destroyed(user2, 50); } function testCannotTransferredIfNotBound() public { - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector + ); ruleEngine.transferred(user1, user2, 200); } diff --git a/test/RuleEngine/ERC3643TokenIntegration.t.sol b/test/RuleEngine/ERC3643TokenIntegration.t.sol new file mode 100644 index 0000000..aa863bb --- /dev/null +++ b/test/RuleEngine/ERC3643TokenIntegration.t.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {RuleEngine} from "src/deployment/RuleEngine.sol"; +import {ERC3643TokenMock} from "src/mocks/ERC3643TokenMock.sol"; +import {RuleWhitelistMock} from "src/mocks/rules/validation/RuleWhitelistMock.sol"; +import {RuleMintAllowanceMock} from "src/mocks/rules/operation/RuleMintAllowanceMock.sol"; +import {IRule} from "src/interfaces/IRule.sol"; +import {ERC3643ComplianceModuleInvariantStorage} from "src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol"; + +/** + * @title ERC3643TokenIntegrationTest + * @notice Drives the RuleEngine through the ERC-3643 entry points, using a token whose compliance + * interaction is modelled on `Token.sol` from the reference implementation in `lib/ERC-3643` (4.1.3). + * @dev Covers the entry points an ERC-3643 token actually uses: the `setCompliance` self-binding + * dance, `transferred(from, to, value)` for transfers, and `created` / `destroyed` for mint and + * burn. An ERC-3643 token never reaches the 4-argument `transferred(spender, ...)` overload, which + * belongs to CMTAT's `IRuleEngine`. + */ +contract ERC3643TokenIntegrationTest is Test, ERC3643ComplianceModuleInvariantStorage { + RuleEngine engine; + ERC3643TokenMock token; + RuleWhitelistMock whitelist; + + address constant ADMIN = address(1); + address constant ALICE = address(2); + address constant BOB = address(3); + address constant CAROL = address(4); + + function setUp() public { + vm.startPrank(ADMIN); + engine = new RuleEngine(ADMIN, address(0), address(0)); + token = new ERC3643TokenMock(); + + whitelist = new RuleWhitelistMock(ADMIN, address(0)); + address[] memory listed = new address[](3); + listed[0] = ALICE; + listed[1] = BOB; + // address(0) must be listed for mint and burn to pass the whitelist: the rule treats the + // zero address as an ordinary participant, and an ERC-3643 mint is pre-checked as + // canTransfer(address(0), to, amount). See testMintIsBlockedWhenZeroAddressNotListed. + listed[2] = address(0); + whitelist.addAddressesToTheList(listed); + engine.addRule(IRule(address(whitelist))); + + // The token binds itself in setCompliance, so it needs self-binding approval first. + engine.setTokenSelfBindingApproval(address(token), true); + vm.stopPrank(); + + token.setCompliance(address(engine)); + } + + /* ============ Binding ============ */ + + /// @notice The documented setCompliance sequence binds the token to the engine. + function testSetComplianceBindsTheToken() public view { + assertTrue(engine.isTokenBound(address(token)), "token should be bound after setCompliance"); + assertEq(engine.getTokenBound(), address(token)); + } + + /// @notice Re-pointing the token at a second engine unbinds it from the first. + function testSetComplianceUnbindsThePreviousEngine() public { + vm.startPrank(ADMIN); + RuleEngine engine2 = new RuleEngine(ADMIN, address(0), address(0)); + engine2.setTokenSelfBindingApproval(address(token), true); + vm.stopPrank(); + + token.setCompliance(address(engine2)); + + assertFalse(engine.isTokenBound(address(token)), "old engine should no longer be bound"); + assertTrue(engine2.isTokenBound(address(token)), "new engine should be bound"); + } + + /// @notice Without self-binding approval the token cannot bind itself. + function testSetComplianceRevertsWithoutSelfBindingApproval() public { + vm.prank(ADMIN); + RuleEngine engine2 = new RuleEngine(ADMIN, address(0), address(0)); + + ERC3643TokenMock token2 = new ERC3643TokenMock(); + vm.expectRevert(); + token2.setCompliance(address(engine2)); + } + + /* ============ transferred ============ */ + + /// @notice A transfer between whitelisted addresses is allowed and notifies the engine. + function testTransferBetweenListedAddressesSucceeds() public { + token.mint(ALICE, 100); + + vm.prank(ALICE); + token.transfer(BOB, 40); + + assertEq(token.balanceOf(ALICE), 60); + assertEq(token.balanceOf(BOB), 40); + } + + /// @notice A transfer to a non-whitelisted address is rejected by the rule via the engine. + function testTransferToUnlistedAddressReverts() public { + token.mint(ALICE, 100); + + vm.prank(ALICE); + vm.expectRevert(); + token.transfer(CAROL, 10); + } + + /// @notice Only a bound token may call the ERC-3643 callbacks. + function testUnboundCallerCannotCallTransferred() public { + vm.prank(CAROL); + vm.expectRevert(RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); + engine.transferred(ALICE, BOB, 1); + } + + /* ============ created / destroyed ============ */ + + /// @notice Mint routes through created() and is accepted for a listed recipient. + function testMintRoutesThroughCreated() public { + token.mint(BOB, 25); + assertEq(token.balanceOf(BOB), 25); + } + + /// @notice Burn routes through destroyed(). + function testBurnRoutesThroughDestroyed() public { + token.mint(ALICE, 30); + token.burn(ALICE, 10); + assertEq(token.balanceOf(ALICE), 20); + } + + /// @notice created() and destroyed() are restricted to bound tokens. + function testUnboundCallerCannotCallCreatedOrDestroyed() public { + vm.startPrank(CAROL); + vm.expectRevert(RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); + engine.created(BOB, 1); + + vm.expectRevert(RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); + engine.destroyed(ALICE, 1); + vm.stopPrank(); + } + + /** + * @notice The zero address must be whitelisted for an ERC-3643 token to mint. + * @dev {RuleWhitelistMock} treats `address(0)` as an ordinary participant, and the reference + * ERC-3643 token pre-checks a mint as `canTransfer(address(0), to, amount)`. An issuer who + * whitelists only real holders therefore cannot mint at all. This test pins that operational + * requirement so the behaviour is not changed unnoticed. + */ + function testMintIsBlockedWhenZeroAddressNotListed() public { + vm.startPrank(ADMIN); + RuleEngine engine2 = new RuleEngine(ADMIN, address(0), address(0)); + RuleWhitelistMock whitelist2 = new RuleWhitelistMock(ADMIN, address(0)); + address[] memory listed = new address[](1); + listed[0] = BOB; // real holder only, zero address deliberately omitted + whitelist2.addAddressesToTheList(listed); + engine2.addRule(IRule(address(whitelist2))); + + ERC3643TokenMock token2 = new ERC3643TokenMock(); + engine2.setTokenSelfBindingApproval(address(token2), true); + vm.stopPrank(); + token2.setCompliance(address(engine2)); + + // The mint pre-check reports the origin (address(0)) as not whitelisted. + assertEq(engine2.detectTransferRestriction(address(0), BOB, 10), whitelist2.CODE_ADDRESS_FROM_NOT_WHITELISTED()); + assertFalse(engine2.canTransfer(address(0), BOB, 10)); + + vm.expectRevert(ERC3643TokenMock.ERC3643TokenMock_ComplianceNotFollowed.selector); + token2.mint(BOB, 10); + } + + /* ============ H-1: the mint pre-check fails open ============ */ + + /** + * @notice Regression guard for H-1 (see doc/security/audits/tools/v3.0.0-rc5/CLAUDE_ANALYSIS.md). + * @dev The reference ERC-3643 token pre-checks a mint with `canTransfer(address(0), to, amount)`, + * which carries no spender. A spender-keyed rule cannot evaluate the mint on that path and + * answers "no restriction", so the pre-check passes while the spender-aware path rejects. + * This test pins that documented behaviour so a change to it is noticed. + */ + function testMintPreCheckFailsOpenForSpenderKeyedRule() public { + vm.startPrank(ADMIN); + RuleMintAllowanceMock mintRule = new RuleMintAllowanceMock(ADMIN); + mintRule.setMintAllowance(address(token), 10); + engine.addRule(IRule(address(mintRule))); + vm.stopPrank(); + + uint256 overAllowance = 500; + + // The 3-argument path the ERC-3643 token uses reports no restriction. + assertTrue( + engine.canTransfer(address(0), BOB, overAllowance), "canTransfer fails open: no spender on this path" + ); + assertEq(engine.detectTransferRestriction(address(0), BOB, overAllowance), 0); + + // The 4-argument path, which has the spender, reports the restriction. + assertFalse(engine.canTransferFrom(address(token), address(0), BOB, overAllowance)); + assertEq(engine.detectTransferRestrictionFrom(address(token), address(0), BOB, overAllowance), 81); + } +} diff --git a/test/RuleEngine/RuleEngineMaxRulesEvent.t.sol b/test/RuleEngine/RuleEngineMaxRulesEvent.t.sol new file mode 100644 index 0000000..4947a5e --- /dev/null +++ b/test/RuleEngine/RuleEngineMaxRulesEvent.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {RuleEngine} from "src/deployment/RuleEngine.sol"; +import {RuleEngineOwnable} from "src/deployment/RuleEngineOwnable.sol"; +import {RuleEngineOwnable2Step} from "src/deployment/RuleEngineOwnable2Step.sol"; +import {RulesManagementModuleInvariantStorage} from "src/modules/library/RulesManagementModuleInvariantStorage.sol"; + +/** + * @title RuleEngineMaxRulesEventTest + * @notice C-1: deployment must emit {SetMaxRules} so the event log alone is sufficient + * to reconstruct the configured rule cap. Without it an event-only indexer sees + * no value until the first administrative change. + */ +contract RuleEngineMaxRulesEventTest is Test, RulesManagementModuleInvariantStorage { + address constant ADMIN = address(1); + address constant FORWARDER = address(0); + + function testRuleEngineEmitsInitialMaxRulesOnDeployment() public { + vm.expectEmit(true, true, true, true); + emit SetMaxRules(DEFAULT_MAX_RULES); + new RuleEngine(ADMIN, FORWARDER, address(0)); + } + + function testRuleEngineOwnableEmitsInitialMaxRulesOnDeployment() public { + vm.expectEmit(true, true, true, true); + emit SetMaxRules(DEFAULT_MAX_RULES); + new RuleEngineOwnable(ADMIN, FORWARDER, address(0)); + } + + function testRuleEngineOwnable2StepEmitsInitialMaxRulesOnDeployment() public { + vm.expectEmit(true, true, true, true); + emit SetMaxRules(DEFAULT_MAX_RULES); + new RuleEngineOwnable2Step(ADMIN, FORWARDER, address(0)); + } +} diff --git a/test/RuleEngine/RulesManagementModuleTest/CMTATIntegrationBase.sol b/test/RuleEngine/RulesManagementModuleTest/CMTATIntegrationBase.sol index 26fb0cf..64060e8 100644 --- a/test/RuleEngine/RulesManagementModuleTest/CMTATIntegrationBase.sol +++ b/test/RuleEngine/RulesManagementModuleTest/CMTATIntegrationBase.sol @@ -28,7 +28,7 @@ abstract contract RuleEngineCMTATIntegrationBase is Test, HelperContract { vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock = new RuleEngine(RULE_ENGINE_OPERATOR_ADDRESS, ZERO_ADDRESS, address(cmtatContract)); ruleConditionalTransferLight = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight); diff --git a/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperation.t.sol b/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperation.t.sol index 72f76bf..174d8fc 100644 --- a/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperation.t.sol +++ b/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperation.t.sol @@ -16,7 +16,7 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock = new RuleEngine(RULE_ENGINE_OPERATOR_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); ruleConditionalTransferLight = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight); @@ -29,11 +29,11 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testCanSetRules() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); // Act @@ -54,8 +54,8 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testCannotSetRuleIfARuleIsAlreadyPresent() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = ruleConditionalTransferLight1; ruleConditionalTransferLightTab[1] = ruleConditionalTransferLight1; @@ -110,11 +110,11 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testCanClearRules() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); @@ -155,11 +155,11 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testCanClearRulesAndAddAgain() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); @@ -202,8 +202,8 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testCanAddRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); // Act vm.expectEmit(true, false, false, false); @@ -223,15 +223,15 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testCannotAddRuleAboveMaxRules() public { for (uint256 i = 0; i < 9; ++i) { vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight rule = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock rule = + new RuleConditionalTransferLightMock(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); + RuleConditionalTransferLightMock extraRule = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.expectRevert( abi.encodeWithSelector( RuleEngine_RulesManagementModule_MaxRulesExceeded.selector, ruleEngineMock.maxRules() @@ -245,8 +245,8 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { 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); + RuleConditionalTransferLightMock rule = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); manyRules[i] = IRule(rule); } @@ -263,8 +263,8 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { 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); + RuleConditionalTransferLightMock rule = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); atMaxRules[i] = IRule(rule); } @@ -289,8 +289,8 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testLoweredMaxRulesDoesNotRemoveExistingRulesButBlocksNewAdds() public { vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight secondRule = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock secondRule = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(secondRule); assertEq(ruleEngineMock.rulesCount(), 2); @@ -301,8 +301,8 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { assertEq(ruleEngineMock.rulesCount(), 2); vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight thirdRule = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock thirdRule = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.expectRevert(abi.encodeWithSelector(RuleEngine_RulesManagementModule_MaxRulesExceeded.selector, uint256(1))); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(thirdRule); @@ -354,8 +354,8 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testCannotRemoveNonExistantRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); // Act vm.expectRevert(RuleEngine_RulesManagementModule_RuleDoNotMatch.selector); @@ -370,8 +370,8 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testCanRemoveLatestRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight1); @@ -389,8 +389,8 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testCanRemoveFirstRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight1); @@ -409,14 +409,14 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { // Arrange // First rule vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight1); // Second rule vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight2); @@ -444,10 +444,10 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { assertEq(resUint256, 1); // Arrange - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); @@ -462,10 +462,10 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testGetRule() public { // Arrange - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); @@ -480,10 +480,10 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testGetRules() public { // Arrange - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); @@ -501,10 +501,10 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { function testCanGetRuleIndex() public { // Arrange - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); @@ -520,7 +520,7 @@ contract RulesManagementModuleInvariantStorageTest is Test, HelperContract { uint256 index2 = ruleEngineMock.getRuleIndex( ruleConditionalTransferLight2 ); - // Length of the list because RuleConditionalTransferLight is not in the list + // Length of the list because RuleConditionalTransferLightMock is not in the list uint256 index3 = ruleEngineMock.getRuleIndex( ruleConditionalTransferLight ); diff --git a/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperationRevertBase.sol b/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperationRevertBase.sol index 87d1019..48cbc99 100644 --- a/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperationRevertBase.sol +++ b/test/RuleEngine/RulesManagementModuleTest/RuleEngineOperationRevertBase.sol @@ -5,7 +5,7 @@ import {Test} from "forge-std/Test.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../../HelperContract.sol"; -import {RuleOperationRevert} from "src/mocks/rules/operation/RuleOperationRevert.sol"; +import {RuleOperationRevertMock} from "src/mocks/rules/operation/RuleOperationRevertMock.sol"; /** * @title Base test for RuleEngine operation revert with CMTAT @@ -20,7 +20,7 @@ abstract contract RuleEngineOperationRevertBase is Test, HelperContract { vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock = new RuleEngine(RULE_ENGINE_OPERATOR_ADDRESS, ZERO_ADDRESS, address(cmtatContract)); - RuleOperationRevert ruleOperationRevert = new RuleOperationRevert(); + RuleOperationRevertMock ruleOperationRevert = new RuleOperationRevertMock(); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleOperationRevert); @@ -34,7 +34,7 @@ abstract contract RuleEngineOperationRevertBase is Test, HelperContract { function testRuleEngineTransferredRevert() public { // Arrange - vm.expectRevert(RuleOperationRevert.RuleConditionalTransferLight_InvalidTransfer.selector); + vm.expectRevert(RuleOperationRevertMock.RuleConditionalTransferLight_InvalidTransfer.selector); // Act // forge-lint: disable-next-line(erc20-unchecked-transfer) cmtatContract.transfer(ADDRESS2, 21); diff --git a/test/RuleEngine/RulesManagementModuleTest/RuleEngineRestriction.t.sol b/test/RuleEngine/RulesManagementModuleTest/RuleEngineRestriction.t.sol index 2405f4b..39cc173 100644 --- a/test/RuleEngine/RulesManagementModuleTest/RuleEngineRestriction.t.sol +++ b/test/RuleEngine/RulesManagementModuleTest/RuleEngineRestriction.t.sol @@ -17,7 +17,7 @@ contract RuleEngineTest is Test, HelperContract { vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock = new RuleEngine(RULE_ENGINE_OPERATOR_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); ruleConditionalTransferLight = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight); @@ -108,4 +108,24 @@ contract RuleEngineTest is Test, HelperContract { // Assert assertEq(resString, TEXT_TRANSFER_REQUEST_NOT_APPROVED); } + + function testMessageForTransferRestrictionWithTransferOKCode() public { + // Act + resString = ruleEngineMock.messageForTransferRestriction(TRANSFER_OK); + + // Assert + assertEq(resString, TEXT_TRANSFER_OK); + } + + function testMessageForTransferRestrictionWithTransferOKCodeNoRule() public { + // Arrange + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.clearRules(); + + // Act + resString = ruleEngineMock.messageForTransferRestriction(TRANSFER_OK); + + // Assert + assertEq(resString, TEXT_TRANSFER_OK); + } } diff --git a/test/RuleEngine/ruleEngineValidation/RuleEngineRestriction.sol b/test/RuleEngine/ruleEngineValidation/RuleEngineRestriction.sol index 31d0e54..9eb92b0 100644 --- a/test/RuleEngine/ruleEngineValidation/RuleEngineRestriction.sol +++ b/test/RuleEngine/ruleEngineValidation/RuleEngineRestriction.sol @@ -9,11 +9,11 @@ import "../../HelperContract.sol"; * @title tests concerning the restrictions and for the transfers */ contract RuleEngineRestrictionTest is Test, HelperContract { - RuleWhitelist ruleWhitelist1; + RuleWhitelistMock ruleWhitelist1; // Arrange function setUp() public { - ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + ruleWhitelist = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock = new RuleEngine(RULE_ENGINE_OPERATOR_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); @@ -23,7 +23,7 @@ contract RuleEngineRestrictionTest is Test, HelperContract { assertEq(resUint256, 1); // Arrange - ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); IRule[] memory ruleWhitelistTab = new IRule[](1); ruleWhitelistTab[0] = ruleWhitelist1; vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); @@ -161,6 +161,26 @@ contract RuleEngineRestrictionTest is Test, HelperContract { assertEq(resString, "Unknown restriction code"); } + function testMessageForTransferRestrictionWithTransferOKCode() public { + // Act + resString = ruleEngineMock.messageForTransferRestriction(TRANSFER_OK); + + // Assert + assertEq(resString, TEXT_TRANSFER_OK); + } + + function testMessageForTransferRestrictionWithTransferOKCodeNoRule() public { + // Arrange + vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); + ruleEngineMock.clearRules(); + + // Act + resString = ruleEngineMock.messageForTransferRestriction(TRANSFER_OK); + + // Assert + assertEq(resString, TEXT_TRANSFER_OK); + } + function testcanTransferOK() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); diff --git a/test/RuleEngine/ruleEngineValidation/RuleEngineValidation.sol b/test/RuleEngine/ruleEngineValidation/RuleEngineValidation.sol index 38328d7..90497d8 100644 --- a/test/RuleEngine/ruleEngineValidation/RuleEngineValidation.sol +++ b/test/RuleEngine/ruleEngineValidation/RuleEngineValidation.sol @@ -13,7 +13,7 @@ contract RuleEngineTest is Test, HelperContract { // Arrange function setUp() public { - ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + ruleWhitelist = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock = new RuleEngine(RULE_ENGINE_OPERATOR_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); @@ -27,9 +27,9 @@ contract RuleEngineTest is Test, HelperContract { function testCanSetRules() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist2 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist2 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); ruleWhitelistTab[0] = IRule(ruleWhitelist1); ruleWhitelistTab[1] = IRule(ruleWhitelist2); // Act @@ -48,7 +48,7 @@ contract RuleEngineTest is Test, HelperContract { function testCannotSetRuleWithSameRulePresentTwice() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); ruleWhitelistTab[0] = ruleWhitelist1; ruleWhitelistTab[1] = ruleWhitelist1; @@ -64,7 +64,7 @@ contract RuleEngineTest is Test, HelperContract { function testCanSetWithTheSameRuleAlreadyPresent() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); ruleWhitelistTab = new IRule[](1); ruleWhitelistTab[0] = ruleWhitelist1; @@ -132,9 +132,9 @@ contract RuleEngineTest is Test, HelperContract { function testCanClearRules() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist2 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist2 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); ruleWhitelistTab[0] = IRule(ruleWhitelist1); ruleWhitelistTab[1] = IRule(ruleWhitelist2); @@ -159,9 +159,9 @@ contract RuleEngineTest is Test, HelperContract { function testCanClearRulesAndAddAgain() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist2 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist2 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); ruleWhitelistTab[0] = IRule(ruleWhitelist1); ruleWhitelistTab[1] = IRule(ruleWhitelist2); @@ -201,7 +201,7 @@ contract RuleEngineTest is Test, HelperContract { function testCanAddRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); // Act vm.expectEmit(true, false, false, false); @@ -260,7 +260,7 @@ contract RuleEngineTest is Test, HelperContract { function testCanRemoveNonExistantRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); // Act vm.expectRevert(RuleEngine_RulesManagementModule_RuleDoNotMatch.selector); @@ -275,7 +275,7 @@ contract RuleEngineTest is Test, HelperContract { function testCanRemoveLatestRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleWhitelist1); @@ -293,7 +293,7 @@ contract RuleEngineTest is Test, HelperContract { function testCanRemoveFirstRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleWhitelist1); @@ -312,12 +312,12 @@ contract RuleEngineTest is Test, HelperContract { // Arrange // First rule vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleWhitelist1); // Second rule vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist ruleWhitelist2 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist2 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); ruleEngineMock.addRule(ruleWhitelist2); @@ -345,8 +345,8 @@ contract RuleEngineTest is Test, HelperContract { assertEq(resUint256, 1); // Arrange - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); - RuleWhitelist ruleWhitelist2 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist2 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); ruleWhitelistTab[0] = IRule(ruleWhitelist1); ruleWhitelistTab[1] = IRule(ruleWhitelist2); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); @@ -361,8 +361,8 @@ contract RuleEngineTest is Test, HelperContract { function testGetRule() public { // Arrange - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); - RuleWhitelist ruleWhitelist2 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist2 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); ruleWhitelistTab[0] = IRule(ruleWhitelist1); ruleWhitelistTab[1] = IRule(ruleWhitelist2); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); @@ -377,8 +377,8 @@ contract RuleEngineTest is Test, HelperContract { function testGetRules() public { // Arrange - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); - RuleWhitelist ruleWhitelist2 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist2 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); ruleWhitelistTab[0] = IRule(ruleWhitelist1); ruleWhitelistTab[1] = IRule(ruleWhitelist2); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); @@ -396,8 +396,8 @@ contract RuleEngineTest is Test, HelperContract { function testCanGetRuleIndex() public { // Arrange - RuleWhitelist ruleWhitelist1 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); - RuleWhitelist ruleWhitelist2 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist1 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + RuleWhitelistMock ruleWhitelist2 = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); ruleWhitelistTab[0] = IRule(ruleWhitelist1); ruleWhitelistTab[1] = IRule(ruleWhitelist2); vm.prank(RULE_ENGINE_OPERATOR_ADDRESS); diff --git a/test/RuleEngineOwnable/AccessControl/RuleEngineOwnableAccessControl.t.sol b/test/RuleEngineOwnable/AccessControl/RuleEngineOwnableAccessControl.t.sol index fd4685b..dd3367d 100644 --- a/test/RuleEngineOwnable/AccessControl/RuleEngineOwnableAccessControl.t.sol +++ b/test/RuleEngineOwnable/AccessControl/RuleEngineOwnableAccessControl.t.sol @@ -15,7 +15,7 @@ contract RuleEngineOwnableAccessControlTest is Test, HelperContractOwnable { function setUp() public { ruleEngineMock = new RuleEngineOwnable(OWNER_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); ruleConditionalTransferLight = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); } /*////////////////////////////////////////////////////////////// @@ -48,10 +48,10 @@ contract RuleEngineOwnableAccessControlTest is Test, HelperContractOwnable { function testOwnerCanSetRules() public { // Arrange - RuleConditionalTransferLight rule1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); - RuleConditionalTransferLight rule2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock rule1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock rule2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); IRule[] memory rules = new IRule[](2); rules[0] = IRule(rule1); rules[1] = IRule(rule2); diff --git a/test/RuleEngineOwnable/ERC3643Compliance.t.sol b/test/RuleEngineOwnable/ERC3643Compliance.t.sol index 54acd8c..d252689 100644 --- a/test/RuleEngineOwnable/ERC3643Compliance.t.sol +++ b/test/RuleEngineOwnable/ERC3643Compliance.t.sol @@ -7,7 +7,9 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; // forge-lint: disable-next-line(unaliased-plain-import) import "../HelperContractOwnable.sol"; import {IERC3643Compliance} from "../../src/interfaces/IERC3643Compliance.sol"; -import {ERC3643ComplianceModuleInvariantStorage} from "../../src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol"; +import { + ERC3643ComplianceModuleInvariantStorage +} from "../../src/modules/library/ERC3643ComplianceModuleInvariantStorage.sol"; // Minimal mock ERC-3643 token to simulate calls to RuleEngine contract ERC3643MockTokenOwnable { @@ -135,7 +137,9 @@ contract RuleEngineOwnableERC3643Test is Test, HelperContractOwnable { } function testCannotBoundIfInvalidAddress() public { - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector + ); vm.prank(OWNER_ADDRESS); ruleEngineMock.bindToken(address(ZERO_ADDRESS)); } @@ -217,7 +221,9 @@ contract RuleEngineOwnableERC3643Test is Test, HelperContractOwnable { } function testCannotSetTokenSelfBindingApprovalForZeroAddress() public { - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector + ); vm.prank(OWNER_ADDRESS); ruleEngineMock.setTokenSelfBindingApproval(address(0), true); } @@ -262,7 +268,9 @@ contract RuleEngineOwnableERC3643Test is Test, HelperContractOwnable { tokens[0] = address(token1); tokens[1] = address(0); - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector + ); vm.prank(OWNER_ADDRESS); ruleEngineMock.setTokenSelfBindingApprovalBatch(tokens, true); } @@ -319,7 +327,9 @@ contract RuleEngineOwnableERC3643Test is Test, HelperContractOwnable { tokens[0] = address(token1); tokens[1] = address(0); - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector + ); vm.prank(OWNER_ADDRESS); ruleEngineMock.bindTokens(tokens); } @@ -351,17 +361,23 @@ contract RuleEngineOwnableERC3643Test is Test, HelperContractOwnable { } function testCannotCreatedIfNotBound() public { - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector + ); ruleEngineMock.created(user1, 100); } function testCannotDestroyedIfNotBound() public { - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector + ); ruleEngineMock.destroyed(user2, 50); } function testCannotTransferredIfNotBound() public { - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector + ); ruleEngineMock.transferred(user1, user2, 200); } } diff --git a/test/RuleEngineOwnable/RulesManagementModuleTest/RuleEngineOwnableOperation.t.sol b/test/RuleEngineOwnable/RulesManagementModuleTest/RuleEngineOwnableOperation.t.sol index 530c5f6..7b8af81 100644 --- a/test/RuleEngineOwnable/RulesManagementModuleTest/RuleEngineOwnableOperation.t.sol +++ b/test/RuleEngineOwnable/RulesManagementModuleTest/RuleEngineOwnableOperation.t.sol @@ -16,7 +16,7 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { vm.prank(OWNER_ADDRESS); ruleEngineMock = new RuleEngineOwnable(OWNER_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); ruleConditionalTransferLight = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(OWNER_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight); @@ -29,11 +29,11 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { function testCanSetRules() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); // Act @@ -54,8 +54,8 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { function testCannotSetRuleIfARuleIsAlreadyPresent() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = ruleConditionalTransferLight1; ruleConditionalTransferLightTab[1] = ruleConditionalTransferLight1; @@ -110,11 +110,11 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { function testCanClearRules() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); @@ -155,11 +155,11 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { function testCanClearRulesAndAddAgain() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); @@ -202,8 +202,8 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { function testCanAddRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); // Act vm.expectEmit(true, false, false, false); @@ -262,8 +262,8 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { function testCannotRemoveNonExistantRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); // Act vm.expectRevert(RuleEngine_RulesManagementModule_RuleDoNotMatch.selector); @@ -278,8 +278,8 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { function testCanRemoveLatestRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(OWNER_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight1); @@ -297,8 +297,8 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { function testCanRemoveFirstRule() public { // Arrange vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(OWNER_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight1); @@ -317,14 +317,14 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { // Arrange // First rule vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(OWNER_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight1); // Second rule vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); vm.prank(OWNER_ADDRESS); ruleEngineMock.addRule(ruleConditionalTransferLight2); @@ -352,10 +352,10 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { assertEq(resUint256, 1); // Arrange - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); vm.prank(OWNER_ADDRESS); @@ -370,10 +370,10 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { function testGetRule() public { // Arrange - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); vm.prank(OWNER_ADDRESS); @@ -388,10 +388,10 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { function testGetRules() public { // Arrange - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); vm.prank(OWNER_ADDRESS); @@ -409,10 +409,10 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { function testCanGetRuleIndex() public { // Arrange - RuleConditionalTransferLight ruleConditionalTransferLight1 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); - RuleConditionalTransferLight ruleConditionalTransferLight2 = - new RuleConditionalTransferLight(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight1 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); + RuleConditionalTransferLightMock ruleConditionalTransferLight2 = + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); ruleConditionalTransferLightTab[0] = IRule(ruleConditionalTransferLight1); ruleConditionalTransferLightTab[1] = IRule(ruleConditionalTransferLight2); vm.prank(OWNER_ADDRESS); @@ -421,4 +421,32 @@ contract RuleEngineOwnableOperationTest is Test, HelperContractOwnable { // Arrange - Assert assertEq(resCallBool, true); } + + function testMessageForTransferRestrictionWithTransferOKCode() public { + // Act + resString = ruleEngineMock.messageForTransferRestriction(TRANSFER_OK); + + // Assert + assertEq(resString, TEXT_TRANSFER_OK); + } + + function testMessageForTransferRestrictionWithTransferOKCodeNoRule() public { + // Arrange + vm.prank(OWNER_ADDRESS); + ruleEngineMock.clearRules(); + + // Act + resString = ruleEngineMock.messageForTransferRestriction(TRANSFER_OK); + + // Assert + assertEq(resString, TEXT_TRANSFER_OK); + } + + function testMessageForTransferRestrictionWithUnknownRestrictionCode() public { + // Act + resString = ruleEngineMock.messageForTransferRestriction(codeNonexistent); + + // Assert + assertEq(resString, "Unknown restriction code"); + } } diff --git a/test/RuleEngineOwnable2Step/RuleEngineOwnable2Step.t.sol b/test/RuleEngineOwnable2Step/RuleEngineOwnable2Step.t.sol index e425051..bab72d8 100644 --- a/test/RuleEngineOwnable2Step/RuleEngineOwnable2Step.t.sol +++ b/test/RuleEngineOwnable2Step/RuleEngineOwnable2Step.t.sol @@ -36,7 +36,7 @@ contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { 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); + new RuleConditionalTransferLightMock(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS, ruleEngineMock); } function testDeploymentSetsOwner() public view { @@ -225,7 +225,9 @@ contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { } function testCannotSetTokenSelfBindingApprovalForZeroAddress() public { - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector + ); vm.prank(OWNER_ADDRESS); ruleEngineMock.setTokenSelfBindingApproval(address(0), true); } @@ -270,7 +272,9 @@ contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { tokens[0] = TOKEN_1; tokens[1] = address(0); - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector + ); vm.prank(OWNER_ADDRESS); ruleEngineMock.setTokenSelfBindingApprovalBatch(tokens, true); } @@ -327,7 +331,9 @@ contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { tokens[0] = TOKEN_1; tokens[1] = address(0); - vm.expectRevert(ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector); + vm.expectRevert( + ERC3643ComplianceModuleInvariantStorage.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector + ); vm.prank(OWNER_ADDRESS); ruleEngineMock.bindTokens(tokens); } @@ -364,4 +370,22 @@ contract RuleEngineOwnable2StepTest is Test, HelperContractOwnable2Step { // forge-lint: disable-next-line(unsafe-typecast) assertEq(bytes4(data), ruleEngineOwnable2StepExposed.exposedMsgData.selector); } + + function testMessageForTransferRestrictionWithTransferOKCode() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.addRule(ruleConditionalTransferLight); + + assertEq(ruleEngineMock.messageForTransferRestriction(TRANSFER_OK), TEXT_TRANSFER_OK); + } + + function testMessageForTransferRestrictionWithTransferOKCodeNoRule() public view { + assertEq(ruleEngineMock.messageForTransferRestriction(TRANSFER_OK), TEXT_TRANSFER_OK); + } + + function testMessageForTransferRestrictionWithUnknownRestrictionCode() public { + vm.prank(OWNER_ADDRESS); + ruleEngineMock.addRule(ruleConditionalTransferLight); + + assertEq(ruleEngineMock.messageForTransferRestriction(CODE_NONEXISTENT), TEXT_CODE_NOT_FOUND); + } } diff --git a/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControl.t.sol b/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControl.t.sol index 1e5ba9c..c19f630 100644 --- a/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControl.t.sol +++ b/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControl.t.sol @@ -15,7 +15,7 @@ contract RuleWhitelistAccessControl is Test, HelperContract { // Arrange function setUp() public { vm.prank(WHITELIST_OPERATOR_ADDRESS); - ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + ruleWhitelist = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); } function testCannotAttackerAddAddressToTheList() public { diff --git a/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControlOZ.t.sol b/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControlOZ.t.sol index 771224e..c39927e 100644 --- a/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControlOZ.t.sol +++ b/test/RuleWhitelist/AccessControl/RuleWhitelistAccessControlOZ.t.sol @@ -13,7 +13,7 @@ contract RuleWhitelistAccessControlOZ is Test, HelperContract, AccessControl { // Arrange function setUp() public { vm.prank(WHITELIST_OPERATOR_ADDRESS); - ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + ruleWhitelist = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); } function testCanGrantRoleAsAdmin() public { diff --git a/test/RuleWhitelist/CMTATIntegrationBase.sol b/test/RuleWhitelist/CMTATIntegrationBase.sol index e3658b3..2eaa855 100644 --- a/test/RuleWhitelist/CMTATIntegrationBase.sol +++ b/test/RuleWhitelist/CMTATIntegrationBase.sol @@ -21,7 +21,7 @@ abstract contract CMTATIntegrationBase is Test, HelperContract { // Arrange function setUp() public virtual { vm.prank(DEFAULT_ADMIN_ADDRESS); - ruleWhitelist = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS); + ruleWhitelist = new RuleWhitelistMock(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS); // global arrange cmtatContract = _deployCmtat(); @@ -53,7 +53,9 @@ abstract contract CMTATIntegrationBase is Test, HelperContract { // Arrange vm.prank(ADDRESS1); vm.expectRevert( - abi.encodeWithSelector(RuleWhitelist.RuleWhitelist_InvalidTransfer.selector, ADDRESS1, ADDRESS2, 21, code) + abi.encodeWithSelector( + RuleWhitelistMock.RuleWhitelist_InvalidTransfer.selector, ADDRESS1, ADDRESS2, 21, code + ) ); // Act // forge-lint: disable-next-line(erc20-unchecked-transfer) @@ -70,7 +72,7 @@ abstract contract CMTATIntegrationBase is Test, HelperContract { vm.prank(ADDRESS1); vm.expectRevert( abi.encodeWithSelector( - RuleWhitelist.RuleWhitelist_InvalidTransfer.selector, ADDRESS1, ADDRESS2, amount, code + RuleWhitelistMock.RuleWhitelist_InvalidTransfer.selector, ADDRESS1, ADDRESS2, amount, code ) ); // Act @@ -88,7 +90,7 @@ abstract contract CMTATIntegrationBase is Test, HelperContract { vm.prank(ADDRESS1); vm.expectRevert( abi.encodeWithSelector( - RuleWhitelist.RuleWhitelist_InvalidTransfer.selector, ADDRESS1, ADDRESS2, amount, code + RuleWhitelistMock.RuleWhitelist_InvalidTransfer.selector, ADDRESS1, ADDRESS2, amount, code ) ); // Act @@ -111,7 +113,7 @@ abstract contract CMTATIntegrationBase is Test, HelperContract { vm.prank(ADDRESS3); vm.expectRevert( abi.encodeWithSelector( - RuleWhitelist.RuleWhitelist_InvalidTransfer.selector, ADDRESS1, ADDRESS2, amount, code + RuleWhitelistMock.RuleWhitelist_InvalidTransfer.selector, ADDRESS1, ADDRESS2, amount, code ) ); // Act diff --git a/test/RuleWhitelist/RuleWhitelist.t.sol b/test/RuleWhitelist/RuleWhitelist.t.sol index d832e3e..16941dc 100644 --- a/test/RuleWhitelist/RuleWhitelist.t.sol +++ b/test/RuleWhitelist/RuleWhitelist.t.sol @@ -6,13 +6,13 @@ import {Test} from "forge-std/Test.sol"; import "../HelperContract.sol"; /** - * @title General functions of the RuleWhitelist + * @title General functions of the RuleWhitelistMock */ contract RuleWhitelistTest is Test, HelperContract { // Arrange function setUp() public { vm.prank(WHITELIST_OPERATOR_ADDRESS); - ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + ruleWhitelist = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); } function _addAddressesToTheList() internal { @@ -112,6 +112,13 @@ contract RuleWhitelistTest is Test, HelperContract { assertEq(resString, TEXT_CODE_NOT_FOUND); } + function testReturnTheNoRestrictionMessageForTheCodeZero() public { + // Act + resString = ruleWhitelist.messageForTransferRestriction(TRANSFER_OK); + // Assert + assertEq(resString, TEXT_TRANSFER_OK); + } + function testCanTransfer() public { // Arrange _addAddressesToTheList(); diff --git a/test/RuleWhitelist/RuleWhitelistAdd.t.sol b/test/RuleWhitelist/RuleWhitelistAdd.t.sol index baebe68..186c1d9 100644 --- a/test/RuleWhitelist/RuleWhitelistAdd.t.sol +++ b/test/RuleWhitelist/RuleWhitelistAdd.t.sol @@ -12,7 +12,7 @@ contract RuleWhitelistAddTest is Test, HelperContract { // Arrange function setUp() public { vm.prank(WHITELIST_OPERATOR_ADDRESS); - ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + ruleWhitelist = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); } function _addAddressesToTheList() internal { diff --git a/test/RuleWhitelist/RuleWhitelistDeployment.t.sol b/test/RuleWhitelist/RuleWhitelistDeployment.t.sol index 1cfc39f..702d59f 100644 --- a/test/RuleWhitelist/RuleWhitelistDeployment.t.sol +++ b/test/RuleWhitelist/RuleWhitelistDeployment.t.sol @@ -7,7 +7,7 @@ import "../HelperContract.sol"; import {MinimalForwarderMock} from "CMTAT/mocks/MinimalForwarderMock.sol"; /** - * @title General functions of the RuleWhitelist + * @title General functions of the RuleWhitelistMock */ contract RuleWhitelistDeploymentTest is Test, HelperContract { // Arrange @@ -19,7 +19,7 @@ contract RuleWhitelistDeploymentTest is Test, HelperContract { MinimalForwarderMock forwarder = new MinimalForwarderMock(); forwarder.initialize(ERC2771ForwarderDomain); vm.prank(WHITELIST_OPERATOR_ADDRESS); - ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, address(forwarder)); + ruleWhitelist = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, address(forwarder)); // assert resBool = ruleWhitelist.hasRole(ADDRESS_LIST_ADD_ROLE, WHITELIST_OPERATOR_ADDRESS); @@ -37,6 +37,6 @@ contract RuleWhitelistDeploymentTest is Test, HelperContract { forwarder.initialize(ERC2771ForwarderDomain); vm.expectRevert(RuleAddressList_AdminWithAddressZeroNotAllowed.selector); vm.prank(WHITELIST_OPERATOR_ADDRESS); - ruleWhitelist = new RuleWhitelist(address(0), address(forwarder)); + ruleWhitelist = new RuleWhitelistMock(address(0), address(forwarder)); } } diff --git a/test/RuleWhitelist/RuleWhitelistRemove.t.sol b/test/RuleWhitelist/RuleWhitelistRemove.t.sol index 4ff9b2c..42c5114 100644 --- a/test/RuleWhitelist/RuleWhitelistRemove.t.sol +++ b/test/RuleWhitelist/RuleWhitelistRemove.t.sol @@ -12,7 +12,7 @@ contract RuleWhitelistRemoveTest is Test, HelperContract { // Arrange function setUp() public { vm.prank(WHITELIST_OPERATOR_ADDRESS); - ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); + ruleWhitelist = new RuleWhitelistMock(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS); } function _addAddressesToTheList() internal { diff --git a/test/script/RuleEngineScript.t.sol b/test/script/RuleEngineScript.t.sol index 1c9d423..da449eb 100644 --- a/test/script/RuleEngineScript.t.sol +++ b/test/script/RuleEngineScript.t.sol @@ -5,6 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {CMTATDeployment} from "../utils/CMTATDeployment.sol"; import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {RuleEngineScript} from "../../script/RuleEngineScript.s.sol"; +import {RuleEngine} from "src/deployment/RuleEngine.sol"; /** * @title Test for the RuleEngineScript deployment script @@ -32,5 +33,17 @@ contract RuleEngineScriptTest is Test { RuleEngineScript deployScript = new RuleEngineScript(); deployScript.run(); + + // The script must leave a *working* deployment, not merely run to completion. + RuleEngine engine = RuleEngine(address(cmtat.ruleEngine())); + assertTrue(address(engine) != address(0), "the engine must be set on the token"); + assertTrue(engine.isTokenBound(address(cmtat)), "the token must be bound to the engine"); + assertEq(engine.getTokenBound(), address(cmtat)); + assertEq(engine.rulesCount(), 1, "the whitelist rule must be configured"); + + // With the token bound and the zero address listed, issuance works end to end. + vm.prank(deployer); + cmtat.mint(deployer, 100); + assertEq(cmtat.balanceOf(deployer), 100); } }