diff --git a/.gitignore b/.gitignore index 1bb896c..faaf0d1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ docOut/ node_modules/ /history /ERCSpecification +FEEDBACK.md *.dbg.json # Codex @@ -27,3 +28,5 @@ node_modules/ #drawio *.bkp *.dtmp + + diff --git a/AGENTS.md b/AGENTS.md index b151852..ec596fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,8 @@ Modular compliance-rule library for CMTAT / ERC-3643 security tokens. Each rule | `RuleERC2980Ownable2Step` | Ownable2Step variant of RuleERC2980 | | `RuleConditionalTransferLight` | Require operator approval before each transfer; bound to exactly one token at a time (`bindToken` reverts if a token is already bound; use `unbindToken` first to migrate) | | `RuleConditionalTransferLightOwnable2Step` | Owner-only approval and execution for conditional transfers | +| `RuleMintAllowance` | Enforce a per-minter mint quota; each mint reduces the minter's allowance; operator can set/increase/decrease allowances; bind to RuleEngine address before use | +| `RuleMintAllowanceOwnable2Step` | Ownable2Step variant of RuleMintAllowance | | `AccessControlModuleStandalone` | Base RBAC module; admin implicitly holds all roles | | `MetaTxModuleStandalone` | ERC-2771 meta-transaction support | | `VersionModule` | Implements `IERC3643Version`; returns the contract version string | @@ -36,7 +38,7 @@ Modular compliance-rule library for CMTAT / ERC-3643 security tokens. Each rule - `openzeppelin-contracts` v5.6.1 — `AccessControl`, `Ownable2Step`, `EnumerableSet`, `ERC2771Context` - `openzeppelin-contracts-upgradeable` v5.6.1 - `CMTAT` v3.0.0 — `IERC1404`, `IERC3643`, `IRuleEngine` interfaces -- `RuleEngine` v3.0.0-rc2 — `IRule`, `RulesManagementModule` +- `RuleEngine` v3.0.0-rc4 — `IRule`, `RulesManagementModule` - `forge-std` — Foundry test utilities Remappings are in `remappings.txt`; aliases used in source: `OZ/`, `CMTAT/`, `RuleEngine/`. @@ -60,6 +62,7 @@ Foundry config: `foundry.toml` (solc 0.8.34, EVM prague, optimizer 200 runs). | RuleIdentityRegistry | 55–57 | | RuleERC2980 | 60–63 | | RuleSpenderWhitelist | 66 | +| RuleMintAllowance | 70 | ## Conventions - Each rule has an `InvariantStorage` abstract contract holding its constants, custom errors, and events. @@ -76,3 +79,4 @@ Foundry config: `foundry.toml` (solc 0.8.34, EVM prague, optimizer 200 runs). - `AGENTS.md` and `CLAUDE.md` are identical — always update both together. - Always update README.md with the latest change - New rule or features implemented: create/update technical documentation in `doc/technical`, update README, create/update test (target: 100% of code coverage), update CHANGELOG.md. Code coverage, run `forge coverage --report summary` +- After each implemented feature or fix, provide a one-line GitHub commit message for all changes since the last commit. diff --git a/CHANGELOG.md b/CHANGELOG.md index 68490c9..497538f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,45 @@ Custom changelog tag: `Dependencies`, `Documentation`, `Testing` - Update surya doc by running the 3 scripts in [./doc/script](./doc/script) - Update changelog -## v0.3.0 - + + +## v0.4.0 + +### Added + +- `RuleConditionalTransferLightMultiToken` and `RuleConditionalTransferLightMultiTokenOwnable2Step` — multi-token conditional transfer rules with token-scoped approvals keyed by `(token, from, to, value)`. +- `RuleMintAllowance` and `RuleMintAllowanceOwnable2Step` — operation rule enforcing a per-minter mint quota managed by an operator. Each mint reduces the minter's allowance; the operator can set an absolute quota or increment/decrement it. Regular transfers and burns are not restricted. Restriction code 70 (`CODE_MINTER_ALLOWANCE_EXCEEDED`). + +### Changed + +- Update contract version in `VersionModule` to `0.4.0`. +- Ownable2Step rule deployments now explicitly advertise ERC-165 `IERC165` (`0x01ffc9a7`), ERC-173 (`0x7f5828d0`), and Ownable2Step (`0x9ab669ef`) interface IDs. +- `RuleMintAllowance` now enforces single-target binding like `RuleConditionalTransferLight`: a second `bindToken` call reverts with `RuleMintAllowance_TokenAlreadyBound` until the current RuleEngine/token is unbound. +- `RuleMintAllowance` no longer advertises the full ERC-3643 `ICompliance` interface through ERC-165 because its mint quota requires spender-aware callbacks. + +### Dependencies + +- Update RuleEngine to `v3.0.0-rc4`. Role constants were isolated into dedicated storage contracts (`RulesManagementModuleRolesStorage`, `ERC3643ComplianceRolesStorage`); concrete rules that reference `RULES_MANAGEMENT_ROLE` / `COMPLIANCE_MANAGER_ROLE` now inherit the corresponding storage contract. + +### Documentation + +- Added technical documentation: `doc/technical/RuleConditionalTransferLightMultiToken.md`. +- Updated README operation-rule sections and tables to include `RuleConditionalTransferLightMultiToken`. +- Added technical documentation: `doc/technical/RuleMintAllowance.md`. +- Updated restriction code table, rule index, role summary, and Ownable2Step list in README. +- Documented that `RuleMintAllowance` does not work with pure ERC-3643 3-arg mint callbacks; it requires the spender-aware CMTAT/RuleEngine path. + +### Testing + +- Added `RuleConditionalTransferLightMultiToken` tests proving approvals are token-scoped and cannot be consumed cross-token. +- Added explicit RuleEngine integration tests for `RuleConditionalTransferLightMultiToken` documenting caller-context behavior in shared RuleEngine topology. +- Added `Ownable2StepERC165Support` test covering all Ownable2Step rule deployments. +- Extended `Ownable2StepERC165Support` with negative assertions to ensure Ownable2Step rule deployments do not advertise unrelated interfaces (`IAccessControl`, `0xdeadbeef`). +- Added unit tests (`test/RuleMintAllowance/RuleMintAllowance.t.sol`, `test/RuleMintAllowance/RuleMintAllowanceOwnable2Step.t.sol`) and CMTAT integration test (`test/RuleMintAllowance/CMTATIntegration.t.sol`) — 54 tests, including batch mint rollback and ERC-165 advertised-interface coverage, >98% line coverage on `RuleMintAllowanceBase`. + +## v0.3.0 - 2026-04-16 + +Commit: `91c21c1191e84ff938892267ec443b0d1bb9efb0` ### Security diff --git a/CLAUDE.md b/CLAUDE.md index b151852..dc9c6d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ Modular compliance-rule library for CMTAT / ERC-3643 security tokens. Each rule - `openzeppelin-contracts` v5.6.1 — `AccessControl`, `Ownable2Step`, `EnumerableSet`, `ERC2771Context` - `openzeppelin-contracts-upgradeable` v5.6.1 - `CMTAT` v3.0.0 — `IERC1404`, `IERC3643`, `IRuleEngine` interfaces -- `RuleEngine` v3.0.0-rc2 — `IRule`, `RulesManagementModule` +- `RuleEngine` v3.0.0-rc4 — `IRule`, `RulesManagementModule` - `forge-std` — Foundry test utilities Remappings are in `remappings.txt`; aliases used in source: `OZ/`, `CMTAT/`, `RuleEngine/`. @@ -76,3 +76,4 @@ Foundry config: `foundry.toml` (solc 0.8.34, EVM prague, optimizer 200 runs). - `AGENTS.md` and `CLAUDE.md` are identical — always update both together. - Always update README.md with the latest change - New rule or features implemented: create/update technical documentation in `doc/technical`, update README, create/update test (target: 100% of code coverage), update CHANGELOG.md. Code coverage, run `forge coverage --report summary` +- After each implemented feature or fix, provide a one-line GitHub commit message for all changes since the last commit. diff --git a/README.md b/README.md index 097c66c..50e7877 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,33 @@ Each rule can be used **standalone**, directly plugged into a CMTAT token, **or** managed collectively via a RuleEngine. +The **RuleEngine** is an external smart contract that applies transfer restrictions to security tokens such as **CMTAT** or [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643)-compatible tokens through a RuleEngine. +Rules are modular validator contracts that the `RuleEngine` or `CMTAT` compatible token can call on every transfer to ensure regulatory and business-logic compliance. + +**Current package version:** `v0.4.0` (contracts report `version()` → `"0.4.0"`). Built against CMTAT `v3.3.0-rc1` and RuleEngine `v3.0.0-rc4`; see [Compatibility](#compatibility) for the supported range. + > This project has not undergone an audit and is provided as-is without any warranties. +## Table of Contents + +- [Schema](#schema) +- [Overview](#overview) +- [Compatibility](#compatibility) +- [Specifications](#specifications) +- [Architecture](#architecture) +- [Types of Rules](#types-of-rules) +- [Quick Start](#quick-start) +- [Deployment Guide](#deployment-guide) +- [Rules details](#rules-details) +- [Access Control](#access-control) +- [Toolchains and Usage](#toolchains-and-usage) +- [API](#api) +- [Security](#security) +- [Intellectual property](#intellectual-property) + ## Schema -- Using rules with CMTAT and ERC-3643 tokens through a [RuleEngine](ttps://github.com/CMTA/RuleEngine) +- Using rules with CMTAT and ERC-3643 tokens through a [RuleEngine](https://github.com/CMTA/RuleEngine) ![Rule-RuleEngine.drawio](./doc/schema/Rule-RuleEngine.drawio.png) @@ -16,15 +38,8 @@ Each rule can be used **standalone**, directly plugged into a CMTAT token, **or* ![Rule-Rule.drawio](./doc/schema/Rule-Rule.drawio.png) -## Table of Contents - -[TOC] - ## Overview -The **RuleEngine** is an external smart contract that applies transfer restrictions to security tokens such as **CMTAT** or [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643)-compatible tokens through a RuleEngine. -Rules are modular validator contracts that the `RuleEngine` or `CMTAT` compatible token can call on every transfer to ensure regulatory and business-logic compliance. - ### Key Concepts - **Rules are controllers** that validate or modify token transfers. @@ -38,51 +53,51 @@ Rules are modular validator contracts that the `RuleEngine` or `CMTAT` compatibl - Conditional approvals - Arbitrary compliance logic -## Quick Start - -```bash -# 1. Clone the repository -git clone -cd Rules - -# 2. Install Foundry (if not already installed) -# https://book.getfoundry.sh/getting-started/installation +### Integration modes -# 3. Install submodule dependencies -forge install +A rule can be consumed in three ways. All three call the same rule contract; they differ only in who calls it and how much of the compliance interface is required. -# 4. Compile -forge build +| Mode | Caller | What the rule must implement | When to use | +| --- | --- | --- | --- | +| **Direct CMTAT rule** | A CMTAT token calls the rule directly (no RuleEngine) | `IRuleEngine` (`canTransfer` + `transferred`, including the spender-aware overload) | A single rule is enough; no multi-rule orchestration needed | +| **RuleEngine-managed rule** | A `RuleEngine` aggregates one or more rules and calls each on every transfer | `IRule` (`IRuleEngine` + `canReturnTransferRestrictionCode`) | Several rules must be combined, ordered, or share restriction codes | +| **ERC-3643 through RuleEngine** | An ERC-3643 token drives `created` / `destroyed` / transfer hooks on a RuleEngine, which forwards them to the rules | Rules as above; the **RuleEngine** implements the full ERC-3643 `ICompliance` | The token is ERC-3643 and needs full `ICompliance` — a standalone rule cannot back an ERC-3643 token directly | -# 5. Run tests -forge test -``` +Interface details for each mode are documented under [Architecture](#architecture); full signatures live in the [API](#api) reference. ## Compatibility -| Component | Compatible Versions | -| ---------------- | ----------------------------------------- | -| **Rules v0.1.0** | CMTAT ≥ v3.0.0
RuleEngine v3.0.0-rc2 | +| Component | Compatible Versions | +| ---------------- | ---------------------------------------------------------- | +| **Rules v0.4.0** | CMTAT ≥ v3.0.0 (tested against v3.3.0-rc1)
RuleEngine v3.0.0-rc4 | + +Spender-aware paths (e.g. `RuleMintAllowance`) rely on the 4-argument `canTransferFrom` / `transferred(spender, from, to, value)` callbacks, which require a CMTAT / RuleEngine that forwards the spender to the rule; this repository is validated against CMTAT `v3.3.0-rc1`. The other rules only use the 3-argument path and work across the full CMTAT ≥ v3.0.0 range. Each Rule implements the interface `IRuleEngine` defined in CMTAT. -This interface declares the ERC-3643 functions `transferred`(read-write) and `canTransfer`(read-only) with several other functions related to [ERC-1404](https://github.com/ethereum/eips/issues/1404), [ERC-7551](https://ethereum-magicians.org/t/erc-7551-crypto-security-token-smart-contract-interface-ewpg-reworked/25477) and [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643). +This interface declares the ERC-3643 functions `transferred` (read-write) and `canTransfer` (read-only) with several other functions related to [ERC-1404](https://github.com/ethereum/eips/issues/1404), [ERC-7551](https://ethereum-magicians.org/t/erc-7551-crypto-security-token-smart-contract-interface-ewpg-reworked/25477) and [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643). ## Specifications ### ERC-3643 -Each rule implements the following functions from the ERC-3643 `ICompliance`interface +Each rule implements the following functions from the ERC-3643 `ICompliance` interface ```solidity function canTransfer(address _from, address _to, uint256 _amount) external view returns (bool); function transferred(address _from, address _to, uint256 _amount) external; ``` -However, contrary to the RuleEngine, the whole interface is currently not implemented (e.g. `created`and `destroyed`) and as a result, the rule can not directly support ERC-3643 token. +However, contrary to the RuleEngine, the whole interface is currently not implemented (e.g. `created` and `destroyed`) and as a result, the rule cannot directly support ERC-3643 token. The alternative to use a Rule with an ERC-3643 token is through the RuleEngine, which implements the whole `ICompliance` interface. +The diagram below shows the recommended integration: the ERC-3643 token drives transfer, mint (`created`) and burn (`destroyed`) compliance hooks on the RuleEngine, which forwards them to the rules. A rule used on its own only implements `canTransfer` + `transferred`, so it cannot back an ERC-3643 token directly. + +![Using a rule with an ERC-3643 token through a RuleEngine](./doc/img/readme-erc3643-integration.png) + +_Diagram source: doc/img/readme-erc3643-integration.puml._ + ### ERC-721/ERC-1155 To improve compatibility with [ERC-721](https://eips.ethereum.org/EIPS/eip-721) and [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155), most validation rules implement the interface `IERC7943NonFungibleComplianceExtend` which includes compliance functions with the `tokenId` argument. Operation rules (such as `RuleConditionalTransferLight`) are ERC-20 only and do not expose the ERC-721/1155 interfaces. `RuleMaxTotalSupply` is ERC-20 only as well and does not expose ERC-721/1155 interfaces. @@ -92,19 +107,25 @@ While no rules currently apply restriction on the token id, the validation inter ```solidity // IERC7943NonFungibleCompliance interface // Read-only functions -function canTransfer(address from, address to, uint256 tokenId, uint256 amount)external view returns (bool allowed) +function canTransfer(address from, address to, uint256 tokenId, uint256 amount) external view returns (bool allowed) // IERC7943NonFungibleComplianceExtend interface // Read-only functions -function detectTransferRestriction(address from, address to, uint256 tokenId, uint256 amount)external view returns (uint8 code); -function detectTransferRestrictionFrom(address spender, address from, address to, uint256 tokenId, uint256 value)external view returns (uint8 code); -function canTransferFrom(address spender, address from, address to, uint256 tokenId, uint256 value)external returns (bool allowed); +function detectTransferRestriction(address from, address to, uint256 tokenId, uint256 amount) external view returns (uint8 code); +function detectTransferRestrictionFrom(address spender, address from, address to, uint256 tokenId, uint256 value) external view returns (uint8 code); +function canTransferFrom(address spender, address from, address to, uint256 tokenId, uint256 value) external returns (bool allowed); // State modifying functions (write) function transferred(address from, address to, uint256 tokenId, uint256 value) external; function transferred(address spender, address from, address to, uint256 tokenId, uint256 value) external; ``` +The diagram below shows a non-fungible transfer flowing through the `tokenId`-aware compliance signatures. For validation rules a single `transferred(...)` call both validates and reverts — it internally runs `detectTransferRestrictionFrom` and requires `TRANSFER_OK` — so no separate pre-check is required in the transfer path; the read-only `detectTransferRestriction*` / `canTransfer*` overloads remain available for off-chain queries. The `RuleNFTAdapter` carries the `tokenId` argument but currently delegates to the address-based checks (`from` / `to` / `spender`), so no rule restricts on the token id yet. + +![ERC-721 / ERC-1155 compliance interface flow](./doc/img/readme-erc721-erc1155-compliance.png) + +_Diagram source: doc/img/readme-erc721-erc1155-compliance.puml._ + ## Architecture @@ -134,36 +155,38 @@ function transferred(address spender, address from, address to, uint256 tokenId, Here is the list of codes used by the different rules -| Contract | Constant name | Value | -| ----------------------- | ------------------------------------ | ----- | -| All | TRANSFER_OK (from CMTAT) | 0 | -| RuleWhitelist | CODE_ADDRESS_FROM_NOT_WHITELISTED | 21 | -| | CODE_ADDRESS_TO_NOT_WHITELISTED | 22 | -| | CODE_ADDRESS_SPENDER_NOT_WHITELISTED | 23 | -| | Reserved slot | 24-29 | -| RuleSanctionList | CODE_ADDRESS_FROM_IS_SANCTIONED | 30 | -| | CODE_ADDRESS_TO_IS_SANCTIONED | 31 | -| | CODE_ADDRESS_SPENDER_IS_SANCTIONED | 32 | -| | Reserved slot | 33-35 | -| RuleBlacklist | CODE_ADDRESS_FROM_IS_BLACKLISTED | 36 | -| | CODE_ADDRESS_TO_IS_BLACKLISTED | 37 | -| | CODE_ADDRESS_SPENDER_IS_BLACKLISTED | 38 | -| | Reserved slot | 39-45 | -| RuleConditionalTransferLight | CODE_TRANSFER_REQUEST_NOT_APPROVED | 46 | -| | Reserved slot | 47-49 | -| RuleMaxTotalSupply | CODE_MAX_TOTAL_SUPPLY_EXCEEDED | 50 | -| | Reserved slot | 51-54 | -| RuleIdentityRegistry | CODE_ADDRESS_FROM_NOT_VERIFIED | 55 | -| | CODE_ADDRESS_TO_NOT_VERIFIED | 56 | -| | CODE_ADDRESS_SPENDER_NOT_VERIFIED | 57 | -| | Reserved slot | 58-59 | -| RuleERC2980 | CODE_ADDRESS_FROM_IS_FROZEN | 60 | -| | CODE_ADDRESS_TO_IS_FROZEN | 61 | -| | CODE_ADDRESS_SPENDER_IS_FROZEN | 62 | -| | CODE_ADDRESS_TO_NOT_WHITELISTED | 63 | -| | Reserved slot | 64-65 | -| RuleSpenderWhitelist | CODE_ADDRESS_SPENDER_NOT_WHITELISTED | 66 | -| | Reserved slot | 67-70 | +| Contract | Constant name | Value | +| ---------------------------- | ------------------------------------ | ----- | +| All | TRANSFER_OK (from CMTAT) | 0 | +| RuleWhitelist | CODE_ADDRESS_FROM_NOT_WHITELISTED | 21 | +| | CODE_ADDRESS_TO_NOT_WHITELISTED | 22 | +| | CODE_ADDRESS_SPENDER_NOT_WHITELISTED | 23 | +| | Reserved slot | 24-29 | +| RuleSanctionList | CODE_ADDRESS_FROM_IS_SANCTIONED | 30 | +| | CODE_ADDRESS_TO_IS_SANCTIONED | 31 | +| | CODE_ADDRESS_SPENDER_IS_SANCTIONED | 32 | +| | Reserved slot | 33-35 | +| RuleBlacklist | CODE_ADDRESS_FROM_IS_BLACKLISTED | 36 | +| | CODE_ADDRESS_TO_IS_BLACKLISTED | 37 | +| | CODE_ADDRESS_SPENDER_IS_BLACKLISTED | 38 | +| | Reserved slot | 39-45 | +| RuleConditionalTransferLight | CODE_TRANSFER_REQUEST_NOT_APPROVED | 46 | +| | Reserved slot | 47-49 | +| RuleMaxTotalSupply | CODE_MAX_TOTAL_SUPPLY_EXCEEDED | 50 | +| | Reserved slot | 51-54 | +| RuleIdentityRegistry | CODE_ADDRESS_FROM_NOT_VERIFIED | 55 | +| | CODE_ADDRESS_TO_NOT_VERIFIED | 56 | +| | CODE_ADDRESS_SPENDER_NOT_VERIFIED | 57 | +| | Reserved slot | 58-59 | +| RuleERC2980 | CODE_ADDRESS_FROM_IS_FROZEN | 60 | +| | CODE_ADDRESS_TO_IS_FROZEN | 61 | +| | CODE_ADDRESS_SPENDER_IS_FROZEN | 62 | +| | CODE_ADDRESS_TO_NOT_WHITELISTED | 63 | +| | Reserved slot | 64-65 | +| RuleSpenderWhitelist | CODE_ADDRESS_SPENDER_NOT_WHITELISTED | 66 | +| | Reserved slot | 67-69 | +| RuleMintAllowance | CODE_MINTER_ALLOWANCE_EXCEEDED | 70 | +| | Reserved slot | 71-74 | Note: @@ -235,62 +258,29 @@ The RuleEngine can then: - Return restriction codes - Mutate rule state (operation rules) -#### CMTAT - -Each rule can be directly plugged to a CMTAT token similar to a RuleEngine. - -Indeed, each rule implements the required interface (`IRuleEngine`) with notably the following function as entrypoint. - -```solidity -function transferred(address from,address to,uint256 value) -function transferred(address spender,address from,address to,uint256 value) -``` - -```solidity -/* -* @title Minimum interface to define a RuleEngine -*/ -interface IRuleEngine is IERC1404Extend, IERC7551Compliance, IERC3643IComplianceContract { - /** - * @notice - * Function called whenever tokens are transferred from one wallet to another - * @dev - * Must revert if the transfer is invalid - * Same name as ERC-3643 but with one supplementary argument `spender` - * This function can be used to update state variables of the RuleEngine contract - * This function can be called ONLY by the token contract bound to the RuleEngine - * @param spender spender address (sender) - * @param from token holder address - * @param to receiver address - * @param value value of tokens involved in the transfer - */ - function transferred(address spender, address from, address to, uint256 value) external; -} -``` - - - -#### RuleEngine - -For a RuleEngine, each rule implements also the required entry point similar to CMTAT, and as well some specific interface for the RuleEngine through the implementation of `IRule` interface defined in the RuleEngine repository. +The same rule can also be plugged **directly** into a CMTAT token (see [Rules as Standalone Compliance Contracts](#rules-as-standalone-compliance-contracts) above): the direct-CMTAT path only requires `IRuleEngine`, while the RuleEngine-managed path additionally requires `IRule`. Full signatures for both interfaces are documented in the [API](#api) reference (`IRuleEngine`, `IERC1404Extend`, `IERC7551Compliance`, `IERC3643IComplianceContract`). -```solidity -interface IRule is IRuleEngine { - /** - * @dev Returns true if the restriction code exists, and false otherwise. - */ - function canReturnTransferRestrictionCode( - uint8 restrictionCode - ) external view returns (bool); -} +## Types of Rules -``` +There are two categories of rules: validation rules (read-only) and operation rules (read-write). +### Which rule should I use? +| Need | Rule | +| --- | --- | +| Only approved holders can send/receive | `RuleWhitelist` | +| Combine several whitelists (OR logic) | `RuleWhitelistWrapper` | +| Restrict `transferFrom` operators (spenders) | `RuleSpenderWhitelist` | +| Block known bad addresses | `RuleBlacklist` | +| Block sanctioned addresses (Chainalysis oracle) | `RuleSanctionsList` | +| Cap total token supply | `RuleMaxTotalSupply` | +| Require identity-registry verification (ERC-3643) | `RuleIdentityRegistry` | +| ERC-2980 Swiss compliance (whitelist + frozenlist) | `RuleERC2980` | +| Require operator approval per transfer | `RuleConditionalTransferLight` | +| Per-transfer approval across several bound tokens | `RuleConditionalTransferLightMultiToken` | +| Limit mint quota per minter | `RuleMintAllowance` | -## Types of Rules - -There are two categories of rules: validation rules (read-only) and operation rules (read-write). +Each rule is also available in `Ownable2Step` and `AccessControl` variants; see [Choosing a Rule Variant](#choosing-a-rule-variant). Stateful rules have binding constraints — see the [Binding model](#binding-model) table. ### Validation Rules (Read-Only) @@ -306,12 +296,34 @@ Available validation rules: `RuleWhitelist`, `RuleWhitelistWrapper`, `RuleSpende Operation rules modify blockchain state during transfer execution. Their `transferred()` function is state-mutating: it consumes or updates stored data as part of the transfer flow. -Available operation rules: `RuleConditionalTransferLight`. +Available operation rules: `RuleConditionalTransferLight`, `RuleConditionalTransferLightMultiToken`, `RuleMintAllowance`. A full-featured variant, `RuleConditionalTransfer`, is maintained as a separate experimental repository at [CMTA/RuleConditionalTransfer](https://github.com/CMTA/RuleConditionalTransfer). +## Quick Start + +```bash +# 1. Clone the repository +git clone +cd Rules + +# 2. Install Foundry (if not already installed) +# https://book.getfoundry.sh/getting-started/installation + +# 3. Install submodule dependencies +forge install + +# 4. Compile +forge build + +# 5. Run tests +forge test +``` + ## Deployment Guide +> ⚠️ **Before production deployment:** this project has [not undergone an audit](#ruleengine---rules). Review the unaudited status, configure roles with least privilege (grant only the roles each operator needs, and prefer the `Ownable2Step` variants for single-owner setups), and run an end-to-end transfer test on the target token setup. + 1. Deploy the rule contract(s) with the desired admin and optional module addresses. 2. Configure the rule state and roles, including whitelist/blacklist entries and oracle or registry addresses. 3. Add rules to the RuleEngine, or set the rule directly on the CMTAT token. @@ -351,7 +363,7 @@ Several rules are available in multiple access-control variants. Use the simples ### Summary tab -| Rule | Type
[read-only / read-write] | ERC-721 / ERC-1155 | ERC-3643 | Security Audit planned in the roadmap | Description | +| Rule | Type
[read-only / read-write] | ERC-721 / ERC-1155 | ERC-3643 via RuleEngine / CMTAT path * | Security Audit planned in the roadmap | 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. | @@ -362,10 +374,16 @@ Several rules are available in multiple access-control variants. Use the simples | 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. | +| RuleConditionalTransferLightMultiToken | Read-Write | | | | Multi-token variant of ConditionalTransferLight. Approvals are token-scoped with key `(token, from, to, value)` so one token cannot consume another token's approvals. | +| RuleMintAllowance | Read-Write | | Partial | | Enforces a per-minter mint quota managed by an operator; each mint reduces the minter's allowance. Regular transfers and burns are not restricted. | | [RuleConditionalTransfer](https://github.com/CMTA/RuleConditionalTransfer) (external) | Read-Write | | |
(experimental rule) | 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) | — | | — |
(community project) | 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. | -All rules are compatible with CMTAT, as noted earlier in this README. +All rules implement the CMTAT rule interfaces needed by their supported transfer paths. Some operation rules require the spender-aware callback, as documented in their rule-specific notes. + +* A checkmark in this column means the rule enforces compliance for ERC-3643 tokens **through a RuleEngine or the CMTAT transfer path** — it does **not** mean the rule is itself a full ERC-3643 `ICompliance` contract. A standalone rule implements only `canTransfer` + `transferred`, so it cannot back an ERC-3643 token directly; use it through a RuleEngine, which implements the full `ICompliance` interface (see [Integration modes](#integration-modes)). + + `RuleMintAllowance` is **Partial**: it does not advertise the full ERC-3643 `ICompliance` interface via ERC-165 because its per-minter mint quota requires the spender-aware mint callback to identify the minter, which the 3-argument ERC-3643 mint callback cannot provide. ### Technical documentation @@ -382,9 +400,23 @@ Detailed technical documentation for each rule is available in [`doc/technical/` | RuleSpenderWhitelist | [RuleSpenderWhitelist.md](./doc/technical/RuleSpenderWhitelist.md) | | RuleERC2980 | [RuleERC2980.md](./doc/technical/RuleERC2980.md) | | RuleConditionalTransferLight | [RuleConditionalTransferLight.md](./doc/technical/RuleConditionalTransferLight.md) | +| RuleConditionalTransferLightMultiToken | [RuleConditionalTransferLightMultiToken.md](./doc/technical/RuleConditionalTransferLightMultiToken.md) | +| RuleMintAllowance | [RuleMintAllowance.md](./doc/technical/RuleMintAllowance.md) | ### Operational Notes +#### Binding model + +Stateful (operation) rules restrict which caller may consume their state via `transferred()`, so the target must be explicitly bound with `bindToken`. The binding model differs per rule: + +| Rule | Binding model | Notes | +| --- | --- | --- | +| `RuleConditionalTransferLight` | Single token | Rebind only after `unbindToken`; second `bindToken` reverts with `RuleConditionalTransferLight_TokenAlreadyBound` | +| `RuleConditionalTransferLightMultiToken` | Multiple direct tokens | Approvals keyed by `(token, from, to, value)`; not suitable for per-token isolation behind one shared `RuleEngine` | +| `RuleMintAllowance` | Single RuleEngine/token | Bind the RuleEngine address in a CMTAT + RuleEngine setup; rebind only after `unbindToken`. Requires the spender-aware mint callback | + +Validation (read-only) rules have no binding requirement: they hold no per-transfer state and can be shared across tokens and RuleEngines freely. + #### RuleIdentityRegistry - `RuleIdentityRegistry`: allows burns (`to == address(0)`) even if the sender is not verified. This matters only if the token allows self-burn. @@ -422,17 +454,26 @@ Detailed technical documentation for each rule is available in [`doc/technical/` - `RuleConditionalTransferLight`: `transferred()` is restricted to the single token bound via `bindToken`; second bind reverts with `RuleConditionalTransferLight_TokenAlreadyBound` until `unbindToken`. - `RuleConditionalTransferLight`: mints (`from == address(0)`) and burns (`to == address(0)`) are exempt from approval checks; `created` and `destroyed` delegate to `_transferred`. +#### RuleConditionalTransferLightMultiToken + +- `RuleConditionalTransferLightMultiToken`: approvals are keyed by `(token, from, to, value)` and are not nonce-based. +- `RuleConditionalTransferLightMultiToken`: operator functions are token-scoped (`approveTransfer(token, ...)`, `cancelTransferApproval(token, ...)`, `approvedCount(token, ...)`, `approveAndTransferIfAllowed(token, ...)`). +- `RuleConditionalTransferLightMultiToken`: execution is restricted to bound tokens; only the calling bound token can consume approvals for its own key space. +- `RuleConditionalTransferLightMultiToken`: mints (`from == address(0)`) and burns (`to == address(0)`) are exempt from approval checks; `created` and `destroyed` delegate to `_transferred`. +- `RuleConditionalTransferLightMultiToken`: with a shared `RuleEngine`, the caller seen by the rule is the engine address (not the underlying token). In that topology, token-scoped approvals are not visible unless approvals are keyed to the engine address, which is not per-token scoping. +- **Warning**: `RuleConditionalTransferLightMultiToken` supports several tokens when integrated directly with each token contract. It must not be used for per-token approval isolation through a shared `RuleEngine`. + #### General notes - All validation rules: read-only rules still implement `transferred()` for ERC-3643 and RuleEngine compatibility, but do not change state. - All AccessControl variants: use `onlyRole(ROLE)` in `_authorize*()` and mark internal helpers `virtual`. - All AccessControl variants: use `AccessControlEnumerable`, so role members can be enumerated with `getRoleMember` / `getRoleMemberCount`; default admin is treated as having all roles via `hasRole`, but may not appear in role member lists unless explicitly granted. - All meta-tx-enabled rules: `forwarderIrrevocable` is accepted as-is (including `address(0)`) and is not validated against ERC-165 because some forwarders do not implement it. -- All rules: implement `IERC3643Version` via `VersionModule` and expose `version()` returning `"0.3.0"`. +- All rules: implement `IERC3643Version` via `VersionModule` and expose `version()` returning `"0.4.0"`. ### Read-only (validation) rule -Currently, there are eight validation rules: whitelist, whitelistWrapper, spender whitelist, blacklist, sanctionlist, max total supply, identity registry, and ERC-2980. +Currently, there are eight validation rules: whitelist, whitelist wrapper, spender whitelist, blacklist, sanctions list, max total supply, identity registry, and ERC-2980. #### Whitelist @@ -579,7 +620,7 @@ The operator calls `setIdentityRegistry(registry)`. The issuer attempts a transf ### Read-Write (Operation) rule -For the moment, there is only one operation rule available: ConditionalTransferLight. +There are three operation rules available: `RuleConditionalTransferLight`, `RuleConditionalTransferLightMultiToken`, and `RuleMintAllowance`. #### Conditional transfer (light) @@ -591,6 +632,26 @@ This rule requires that transfers must be approved by an operator before being e An operator calls `approveTransfer(from, to, value)`. The compliance manager binds exactly one token with `bindToken(token)`; attempting to bind a second token reverts. The token calls `detectTransferRestriction` (passes) and later `transferred` to consume the approval. Without approval, `detectTransferRestriction` returns code 46 and the transfer is rejected. The operator can revoke with `cancelTransferApproval`. To migrate to a different token, the compliance manager must first call `unbindToken` before binding the new one. +#### Mint allowance + +This rule enforces a per-minter mint quota for one bound RuleEngine/token at a time. An operator sets the number of tokens each minter address is allowed to mint via `setMintAllowance(minter, amount)`. Every successful mint reduces the minter's remaining quota. The operator can adjust quotas at any time with `increaseMintAllowance` / `decreaseMintAllowance`. Regular transfers and burns are not restricted. + +Compatibility warning: `RuleMintAllowance` does not enforce quotas for a token that only calls the standard ERC-3643 3-arg compliance functions. It requires the CMTAT/RuleEngine spender-aware path so the minter address is passed as `spender`. + +For the same reason, it does not advertise the full ERC-3643 `ICompliance` interface through ERC-165; the 3-arg callbacks alone cannot enforce the mint quota. + +**Usage scenario** + +The compliance manager binds the rule to the RuleEngine with `bindToken(ruleEngine)`. Attempting to bind a second RuleEngine/token reverts until the current binding is removed with `unbindToken`. The operator assigns `setMintAllowance(alice, 100_000e18)`. Alice's mints deduct from her quota through `transferred(alice, address(0), recipient, amount)`; once exhausted, further mints revert with code 70 until the operator increases the quota. + +#### Conditional transfer (light, multi-token) + +This variant scopes approvals by token address. It hashes `(token, from, to, value)` and supports multiple bound tokens in a single rule instance. Each successful transfer consumes one approval in the calling token namespace. Mints (`from == address(0)`) and burns (`to == address(0)`) remain exempt. + +**Usage scenario** + +An operator calls `approveTransfer(tokenA, from, to, value)` for `tokenA`. A transfer on `tokenA` succeeds and consumes the approval. The same `(from, to, value)` transfer on `tokenB` is still rejected until separately approved with `approveTransfer(tokenB, from, to, value)`. + ## Access Control The module `AccessControlModuleStandalone` implements RBAC access control by inheriting from OpenZeppelin's `AccessControlEnumerable`. @@ -618,8 +679,9 @@ See also [docs.openzeppelin.com - AccessControl](https://docs.openzeppelin.com/c | `ADDRESS_LIST_REMOVE_ROLE` | `0x1b94c92b564251ed6b49246d9a82eb7a486b6490f3b3a3bf3b28d2e99801f3ec` | `removeAddress`, `removeAddresses` (RuleWhitelist, RuleBlacklist) | | `SANCTIONLIST_ROLE` | `0x30842281ac34bdc7d568c7ab276f84ba6fc1a1de1ae858b0afd35e716fb0650d` | `setSanctionListOracle`, `clearSanctionListOracle` (RuleSanctionsList) | | `RULES_MANAGEMENT_ROLE` | `0xea5f4eb72290e50c32abd6c23e45de3d8300b3286e1cbc2e293114b92e034e5e` | `setRules`, `clearRules`, `addRule`, `removeRule` (RuleWhitelistWrapper) | -| `OPERATOR_ROLE` | `0x97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b929` | `approveTransfer`, `cancelTransferApproval` (RuleConditionalTransferLight) | -| `COMPLIANCE_MANAGER_ROLE` | `0xe5c50d0927e06141e032cb9a67e1d7092dc85c0b0825191f7e1cede600028568` | `bindToken`, `unbindToken` (RuleConditionalTransferLight) | +| `OPERATOR_ROLE` | `0x97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b929` | `approveTransfer`, `cancelTransferApproval` (RuleConditionalTransferLight / RuleConditionalTransferLightMultiToken) | +| `COMPLIANCE_MANAGER_ROLE` | `0xe5c50d0927e06141e032cb9a67e1d7092dc85c0b0825191f7e1cede600028568` | `bindToken`, `unbindToken` (RuleConditionalTransferLight / RuleConditionalTransferLightMultiToken / RuleMintAllowance) | +| `ALLOWANCE_OPERATOR_ROLE` | `0x86a2482724302deea267bc1ca14032806c318aeaf8d1e0d445a6fb7e7c997beb` | `setMintAllowance`, `increaseMintAllowance`, `decreaseMintAllowance` (RuleMintAllowance) | | `WHITELIST_ADD_ROLE` | `0x77c0b4c0975a0b0417d8ce295502737b95fee8923755fed0cce952907a1861ed` | `addWhitelistAddress`, `addWhitelistAddresses` (RuleERC2980) | | `WHITELIST_REMOVE_ROLE` | `0xf4d11a530c5b90f459c6ab1e335d3d77156b8ff3093308e4fca6d100ee87ade9` | `removeWhitelistAddress`, `removeWhitelistAddresses` (RuleERC2980) | | `FROZENLIST_ADD_ROLE` | `0xc52c49807a071974b9260f4b553ee09bd9fd85f687d8d4cc3232de7104ff7835` | `addFrozenlistAddress`, `addFrozenlistAddresses` (RuleERC2980) | @@ -637,231 +699,43 @@ For simpler ownership-based control, `Ownable2Step` variants (two-step ownership - `RuleMaxTotalSupplyOwnable2Step` - `RuleERC2980Ownable2Step` - `RuleConditionalTransferLightOwnable2Step` +- `RuleConditionalTransferLightMultiTokenOwnable2Step` +- `RuleMintAllowanceOwnable2Step` `RuleConditionalTransferLightOwnable2Step` now grants approval and execution permissions exclusively to the owner. All `Ownable2Step` variants enforce access using OpenZeppelin's `onlyOwner` modifier. +All `Ownable2Step` variants also advertise ERC-165 support for `IERC165` (`0x01ffc9a7`), ERC-173 ownership (`0x7f5828d0`), and Ownable2Step handover (`0x9ab669ef`). ### Address List -Common access control between `blacklistRule`and `WhitelistRule` +Common access control between the blacklist rule and whitelist rule. These roles are listed above in the Role Summary table. - ## Toolchains and Usage -### 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 - -- Library - - - Foundry [v1.5.0](https://github.com/foundry-rs/foundry) - - - Forge std [v1.12.0](https://github.com/foundry-rs/forge-std/releases/tag/v1.12.0 ) - - - OpenZeppelin Contracts (submodule) [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.6.1) - - - OpenZeppelin Contracts Upgradeable (submodule) [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/releases/tag/v5.6.1) +This repository is developed and tested with [Foundry](https://book.getfoundry.sh); a Hardhat config is also present for compilation and a small smoke test. Build settings (`foundry.toml` / `hardhat.config.js`): solc `v0.8.34`, EVM `Prague`, optimizer on (200 runs). - - CMTAT [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0) +### Main commands - - RuleEngine [v3.0.0-rc2](https://github.com/CMTA/RuleEngine/releases/tag/v3.0.0-rc2) +| Task | Command | +| --- | --- | +| Install / update submodules | `forge install` · `forge update` | +| Build | `forge build` | +| Contract sizes | `forge compile --sizes` | +| Run all tests | `forge test` | +| Run one test | `forge test --match-contract --match-test ` | +| Gas report | `forge test --gas-report` | +| Gas snapshot | `forge snapshot` (check only: `forge snapshot --check`) | +| Coverage | `forge coverage` | +| Format | `forge fmt` | +| Deploy a script | `forge script script/.s.sol --rpc-url --account ` | -### Toolchain installation - -This repository is primarily developed and tested with [Foundry](https://book.getfoundry.sh), a smart contract development toolchain. - -Hardhat configuration is also present to support contract compilation and a small smoke test with Hardhat. - -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) - -``` - forge build -``` - -Hardhat compilation (optional): - -```bash -npm run hardhat:compile -``` - -### Contract size - -```bash - forge compile --sizes -``` - -### Testing - -You can run the tests with - -```bash -forge test -``` - -Hardhat smoke test (optional): - -```bash -npm run hardhat:test:smoke -``` - -To run a specific test, use - -```bash -forge test --match-contract --match-test -``` - -- For `RuleConditionalTransferLight` fuzz/integration tests, note that mint and burn paths (`from == address(0)` or `to == address(0)`) are intentionally exempt from approval consumption. -- Ownable2Step variants also include dedicated tests for ownership transfer and manager-only functions (IdentityRegistry, MaxTotalSupply, SanctionsList). -- Coverage-focused tests also target deployment wrappers and operation-rule overloads (`created`, `destroyed`, spender-aware `transferred`) to improve line/function coverage in `src/rules/operation` and `src/rules/validation/deployment`. - -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). - -### Gas Benchmarks - -Gas usage is tracked in two complementary files: - -- **`.gas-snapshot`** — machine-generated file produced by `forge snapshot`. It records the gas cost of every test function and is checked into the repository so that gas regressions are visible in diffs. Regenerate it with: - - ```bash - forge snapshot - ``` - - To check for regressions against the committed snapshot without overwriting it: - - ```bash - forge snapshot --check - ``` - -- **`doc/GAS.md`** — human-readable summary of key operation costs (e.g. `addAddress`, `detectTransferRestriction`) with the date of the last measurement. Update it manually after running `forge snapshot` when behaviour or gas costs change. - -### Coverage - -![coverage](./doc/coverage/coverage.png) - -A code coverage is available in [index.html](./doc/coverage/coverage/index.html). - -* 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 --prefix "$PWD/" --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) - -### Other - -Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust. - -Foundry consists of: - -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. - -#### Documentation - -https://book.getfoundry.sh/ - - -#### Format - -```shell -$ forge fmt -``` - -#### Gas Snapshots - -```shell -$ forge snapshot -``` - -#### Anvil - -```shell -$ anvil -``` - -#### Deploy - -> **Warning — private key security** -> Passing `--private-key` directly on the command line is **not recommended** in production: the key is visible in your shell history and to any process that can read `/proc`. Prefer hardware wallets (`--ledger`, `--trezor`), encrypted keystores (`--account `), or environment-variable signers. See [Foundry best practices](https://www.getfoundry.sh/best-practices) for details. - -```shell -$ forge script script/DeployCMTATWithWhitelist.s.sol --rpc-url --private-key -$ forge script script/DeployCMTATWithBlacklist.s.sol --rpc-url --private-key -$ forge script script/DeployCMTATWithBlacklistAndSanctionsList.s.sol --rpc-url --private-key -``` - -#### Cast - -```shell -$ cast -``` - -#### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +Deployment scripts: `script/DeployCMTATWithWhitelist.s.sol`, `script/DeployCMTATWithBlacklist.s.sol`, `script/DeployCMTATWithBlacklistAndSanctionsList.s.sol`. +> **Deployment key security:** avoid passing `--private-key` on the command line (visible in shell history and to any process that can read `/proc`). Prefer hardware wallets (`--ledger`, `--trezor`) or encrypted keystores (`--account `). See [Foundry best practices](https://www.getfoundry.sh/best-practices). +For the full toolchain guide — dependency versions, Hardhat commands, HTML coverage generation, the gas-benchmark workflow, and the generic Forge / Cast / Anvil / Chisel reference — see **[doc/FOUNDRY.md](./doc/FOUNDRY.md)** and the [Foundry book](https://book.getfoundry.sh/). ## API @@ -1681,8 +1555,43 @@ Returns the number of approvals for the transfer hash. ### Automated Analysis -Latest tool outputs for this release cycle (including feedback documents) are available in [`doc/security/audits/tools/v0.3.0/`](./doc/security/audits/tools/v0.3.0/). -`v0.3.0` cleanup: removed unused `RuleConditionalTransferLight_TransferFailed` custom error after SafeERC20 migration. +See the consolidated [Audit & Security-Analysis Overview](./doc/security/audits/AUDIT_OVERVIEW.md) for the full index and triage. Latest tool outputs (including feedback documents) are in [`doc/security/audits/tools/v0.4.0/`](./doc/security/audits/tools/v0.4.0/). + +Commands used for `v0.4.0` (mocks excluded): + +```bash +slither . --checklist --filter-paths "node_modules,lib,test,forge-std,mocks" \ + > doc/security/audits/tools/v0.4.0/slither-report.md +aderyn -x mocks --output doc/security/audits/tools/v0.4.0/aderyn-report.md +``` + +#### Aderyn (v0.4.0) + +Static analysis with [Aderyn](https://github.com/Cyfrin/aderyn) 0.6.5. Full report and feedback in [`doc/security/audits/tools/v0.4.0/`](./doc/security/audits/tools/v0.4.0/). **No High/Medium issues; nothing to fix** — all 8 Low findings are by-design or false positives (see [feedback](./doc/security/audits/tools/v0.4.0/aderyn-report-feedback.md)). Instance counts rose vs `v0.3.0` only because `v0.4.0` adds the `RuleMintAllowance` and `RuleConditionalTransferLightMultiToken` families. + +| ID | Title | Instances | Verdict | +|---|---|---|---| +| L-1 | Centralization Risk | 62 | By design (regulated token issuer model) | +| L-2 | Unspecific Solidity Pragma | 63 | By design (`^0.8.20` library; project pins solc 0.8.34) | +| L-3 | Address State Variable Set Without Checks | 1 | False positive — zero-check enforced at public `setSanctionListOracle` | +| L-4 | PUSH0 Opcode | 63 | By design — project targets Prague EVM | +| L-5 | Modifier Invoked Only Once | 2 | By design — template method pattern | +| L-6 | Empty Block | 55 | By design — `_authorize*()` hooks + required interface no-ops | +| L-7 | Costly operations inside loop | 6 | By design — `EnumerableSet` requires one `SSTORE` per element | +| L-8 | Unchecked Return | 13 | Mixed — majority false positives; constructor `_grantRole` intentional | + +#### Slither (v0.4.0) + +Static analysis with [Slither](https://github.com/crytic/slither) 0.11.5. Full report and feedback in [`doc/security/audits/tools/v0.4.0/`](./doc/security/audits/tools/v0.4.0/). **Nothing to fix** — the two High `arbitrary-send-erc20` hits are false positives (approval-gated, allowance-checked compliance flow); see [feedback](./doc/security/audits/tools/v0.4.0/slither-report-feedback.md). + +| Category | Severity | Instances | Verdict | +|---|---|---|---| +| arbitrary-send-erc20 | High | 2 | False positive — `from` guarded by `onlyTransferApprover`, recorded approval, allowance check, bound token (light + multi-token) | +| unused-return | Medium | 6 | False positive — existence pre-checked at public layer before internal helper | +| calls-loop | Low | 16 | By design — wrapper must query each child rule; child rules are read-only | +| assembly | Informational | 2 | By design — memory-safe hash in `_transferHash` (light + multi-token) | +| naming-convention | Informational | 2 | By design — parameter names match ERC-2980 spec | +| unused-state | Informational | 8 | False positive — `RuleNFTAdapter` constants used in base dispatch (per-contract analysis limitation) | #### Aderyn (v0.3.0) diff --git a/doc/FOUNDRY.md b/doc/FOUNDRY.md new file mode 100644 index 0000000..3cb9751 --- /dev/null +++ b/doc/FOUNDRY.md @@ -0,0 +1,214 @@ +# Foundry Toolchain Guide + +Full development-toolchain reference for the **Rules** project. For a quick command summary, see the [Toolchains and Usage](../README.md#toolchains-and-usage) section of the README. + +## 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 + +- Library + + - Foundry [v1.5.0](https://github.com/foundry-rs/foundry) + + - Forge std [v1.12.0](https://github.com/foundry-rs/forge-std/releases/tag/v1.12.0) + + - OpenZeppelin Contracts (submodule) [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.6.1) + + - OpenZeppelin Contracts Upgradeable (submodule) [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/releases/tag/v5.6.1) + + - CMTAT [v3.3.0-rc1](https://github.com/CMTA/CMTAT/releases/tag/v3.3.0-rc1) + + - RuleEngine [v3.0.0-rc4](https://github.com/CMTA/RuleEngine/releases/tag/v3.0.0-rc4) + +## Toolchain installation + +This repository is primarily developed and tested with [Foundry](https://book.getfoundry.sh), a smart contract development toolchain. + +Hardhat configuration is also present to support contract compilation and a small smoke test with Hardhat. + +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) + +``` + forge build +``` + +Hardhat compilation (optional): + +```bash +npm run hardhat:compile +``` + +## Contract size + +```bash + forge compile --sizes +``` + +## Testing + +You can run the tests with + +```bash +forge test +``` + +Hardhat smoke test (optional): + +```bash +npm run hardhat:test:smoke +``` + +To run a specific test, use + +```bash +forge test --match-contract --match-test +``` + +- For `RuleConditionalTransferLight` fuzz/integration tests, note that mint and burn paths (`from == address(0)` or `to == address(0)`) are intentionally exempt from approval consumption. +- `RuleMintAllowance` integration tests cover single mints, cumulative `batchMint` allowance consumption, rollback on over-allowance batch mint, and advertised ERC-165 interface IDs. +- Ownable2Step variants also include dedicated tests for ownership transfer and manager-only functions (IdentityRegistry, MaxTotalSupply, SanctionsList). +- Coverage-focused tests also target deployment wrappers and operation-rule overloads (`created`, `destroyed`, spender-aware `transferred`) to improve line/function coverage in `src/rules/operation` and `src/rules/validation/deployment`. + +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). + +## Gas Benchmarks + +Gas usage is tracked in two complementary files: + +- **`.gas-snapshot`** — machine-generated file produced by `forge snapshot`. It records the gas cost of every test function and is checked into the repository so that gas regressions are visible in diffs. Regenerate it with: + + ```bash + forge snapshot + ``` + + To check for regressions against the committed snapshot without overwriting it: + + ```bash + forge snapshot --check + ``` + +- **`doc/GAS.md`** — human-readable summary of key operation costs (e.g. `addAddress`, `detectTransferRestriction`) with the date of the last measurement. Update it manually after running `forge snapshot` when behaviour or gas costs change. + +## Coverage + +![coverage](./coverage/coverage.png) + +A code coverage is available in [index.html](./coverage/coverage/index.html). + +* 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 --prefix "$PWD/" --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) + +## Other + +Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust. + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +### Documentation + +https://book.getfoundry.sh/ + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +> **Warning — private key security** +> Passing `--private-key` directly on the command line is **not recommended** in production: the key is visible in your shell history and to any process that can read `/proc`. Prefer hardware wallets (`--ledger`, `--trezor`), encrypted keystores (`--account `), or environment-variable signers. See [Foundry best practices](https://www.getfoundry.sh/best-practices) for details. + +```shell +$ forge script script/DeployCMTATWithWhitelist.s.sol --rpc-url --private-key +$ forge script script/DeployCMTATWithBlacklist.s.sol --rpc-url --private-key +$ forge script script/DeployCMTATWithBlacklistAndSanctionsList.s.sol --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/doc/img/readme-erc3643-integration.png b/doc/img/readme-erc3643-integration.png new file mode 100644 index 0000000..983005b Binary files /dev/null and b/doc/img/readme-erc3643-integration.png differ diff --git a/doc/img/readme-erc3643-integration.puml b/doc/img/readme-erc3643-integration.puml new file mode 100644 index 0000000..d4ba3cf --- /dev/null +++ b/doc/img/readme-erc3643-integration.puml @@ -0,0 +1,32 @@ +@startuml +title Using a rule with an ERC-3643 token (through a RuleEngine) + +participant "ERC-3643 token" as token +participant "RuleEngine\n(full ICompliance)" as engine +participant "Rule(s)" as rule + +== Transfer == +token -> engine : canTransfer(from, to, amount) read-only pre-check +engine -> rule : detectTransferRestriction(from, to, amount) +rule --> engine : code (0 = TRANSFER_OK) +engine --> token : allowed / blocked +token -> engine : transferred(from, to, amount) +engine -> rule : transferred(from, to, amount) + +== Mint == +token -> engine : created(to, amount) +engine -> engine : _transferred(address(0), to, amount) +engine -> rule : transferred(address(0), to, amount) + +== Burn == +token -> engine : destroyed(from, amount) +engine -> engine : _transferred(from, address(0), amount) +engine -> rule : transferred(from, address(0), amount) + +note over token, rule + A rule alone implements only canTransfer + transferred from ICompliance. + It does NOT implement created / destroyed, so it cannot back an + ERC-3643 token directly — mint/burn compliance would be missed. + Use the RuleEngine, which implements the whole ICompliance interface. +end note +@enduml diff --git a/doc/img/readme-erc721-erc1155-compliance.png b/doc/img/readme-erc721-erc1155-compliance.png new file mode 100644 index 0000000..674715f Binary files /dev/null and b/doc/img/readme-erc721-erc1155-compliance.png differ diff --git a/doc/img/readme-erc721-erc1155-compliance.puml b/doc/img/readme-erc721-erc1155-compliance.puml new file mode 100644 index 0000000..92e9f8d --- /dev/null +++ b/doc/img/readme-erc721-erc1155-compliance.puml @@ -0,0 +1,35 @@ +@startuml +title ERC-721 / ERC-1155 compliance (IERC7943NonFungibleComplianceExtend) + +actor "Operator / Holder" as caller +participant "ERC-721 / ERC-1155\ntoken" as token +participant "Validation rule\n(RuleNFTAdapter)" as rule + +caller -> token : safeTransferFrom(from, to, tokenId[, amount]) + +note over token, rule + A single transferred(...) call both validates AND reverts: + internally it runs detectTransferRestrictionFrom and + require(code == TRANSFER_OK). No separate pre-check is needed + in the transfer path. +end note + +token -> rule : transferred(spender, from, to, tokenId, value) (in the token's transfer hook) +rule -> rule : code = detectTransferRestrictionFrom(spender, from, to, value)\nrequire(code == TRANSFER_OK) +alt restricted (code > 0) + rule --> token : revert + token --> caller : revert (whole transfer rolls back) +else allowed (code == 0) + rule --> token : ok + token --> caller : Transfer succeeds +end + +note over token, rule + amount / value == 1 for ERC-721. RuleNFTAdapter carries tokenId + but delegates to the address-based checks (from / to / spender). + The read-only detectTransferRestriction* / canTransfer* overloads + stay available for off-chain or pre-transfer queries. + Only validation rules implement this interface; operation rules + (e.g. RuleConditionalTransferLight) and RuleMaxTotalSupply are ERC-20 only. +end note +@enduml diff --git a/doc/img/rule-blacklist-flow.png b/doc/img/rule-blacklist-flow.png new file mode 100644 index 0000000..c16c67e Binary files /dev/null and b/doc/img/rule-blacklist-flow.png differ diff --git a/doc/img/rule-blacklist-flow.puml b/doc/img/rule-blacklist-flow.puml new file mode 100644 index 0000000..ff1370c --- /dev/null +++ b/doc/img/rule-blacklist-flow.puml @@ -0,0 +1,38 @@ +@startuml +title RuleBlacklist — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleBlacklist\n(blocked addresses)" as rule + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + note right of rule + isBlacklisted(from)? + isBlacklisted(to)? + isBlacklisted(spender)? (transferFrom) + end note + alt from blacklisted + rule --> cmtat : 36 (CODE_ADDRESS_FROM_IS_BLACKLISTED) + else to blacklisted + rule --> cmtat : 37 (CODE_ADDRESS_TO_IS_BLACKLISTED) + else spender blacklisted + rule --> cmtat : 38 (CODE_ADDRESS_SPENDER_IS_BLACKLISTED) + else none blacklisted + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-conditional-transfer-flow.png b/doc/img/rule-conditional-transfer-flow.png new file mode 100644 index 0000000..7484f59 Binary files /dev/null and b/doc/img/rule-conditional-transfer-flow.png differ diff --git a/doc/img/rule-conditional-transfer-flow.puml b/doc/img/rule-conditional-transfer-flow.puml new file mode 100644 index 0000000..66fe565 --- /dev/null +++ b/doc/img/rule-conditional-transfer-flow.puml @@ -0,0 +1,49 @@ +@startuml +title RuleConditionalTransfer (Vinkulierung) — request/approve/transfer flow with a CMTAT token + +actor "Token holder" as holder +actor "Rule operator" as op +participant "CMTAT token\n(bound to the rule)" as cmtat +participant "RuleEngine" as engine +participant "RuleConditionalTransfer\nrequests[key(from,to,value)]" as rule + +== 1. Request (holder or operator) == +holder -> rule : createTransferRequest(to, value) +rule -> rule : status = WAIT (request created) + +== 2. Approval (operator) == +op -> rule : approveTransferRequest(from, to, value) +rule -> rule : status = APPROVED +note right of rule + Options (set by operator): + - AUTOMATIC_APPROVAL: WAIT is treated as + approved after timeLimitBeforeAutomaticApproval + - TIME_LIMIT: request/transfer deadlines + - Conditional whitelist: from & to whitelisted + are auto-authorized without a request +end note + +alt AUTOMATIC_TRANSFER enabled (and rule has ERC-20 allowance) + rule -> cmtat : transferFrom(from, to, value) + note right of rule : rule performs the transfer itself on approval +end + +== 3. Transfer via CMTAT (manual case) == +holder -> cmtat : transfer(to, value) + +group Validation + operation + cmtat -> engine : detectTransferRestriction(from, to, value) + engine -> rule : detectTransferRestriction(from, to, value) + alt no APPROVED request (or deadline passed) + rule --> cmtat : 46 (request not approved) + cmtat --> holder : revert CMTAT_InvalidTransfer(46) + else request APPROVED and within time limit + rule --> cmtat : 0 (TRANSFER_OK) + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(from, to, value) + engine -> rule : transferred(...) + rule -> rule : status = EXECUTED (state change) + cmtat --> holder : Transfer(from, to, value) + end +end +@enduml diff --git a/doc/img/rule-conditional-transfer-light-flow.png b/doc/img/rule-conditional-transfer-light-flow.png new file mode 100644 index 0000000..0fff01d Binary files /dev/null and b/doc/img/rule-conditional-transfer-light-flow.png differ diff --git a/doc/img/rule-conditional-transfer-light-flow.puml b/doc/img/rule-conditional-transfer-light-flow.puml new file mode 100644 index 0000000..0183cdb --- /dev/null +++ b/doc/img/rule-conditional-transfer-light-flow.puml @@ -0,0 +1,37 @@ +@startuml +title RuleConditionalTransferLight — transfer flow with a CMTAT token (operation rule) + +actor "Operator" as op +actor "Holder / Spender" as caller +participant "CMTAT token\n(bound to the rule)" as cmtat +participant "RuleEngine" as engine +participant "RuleConditionalTransferLight\napprovalCounts[hash(from,to,value)]" as rule + +== Phase 1: approval (off the transfer path) == +op -> rule : approveTransfer(from, to, value) +rule -> rule : approvalCounts[hash]++ \n(state change) + +== Phase 2: the transfer == +caller -> cmtat : transfer(to, value) / transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestriction[From](... from, to, value) + engine -> rule : detectTransferRestriction[From](...) + note right of rule : approvalCounts[hash(from,to,value)] > 0 ? + alt no approval recorded + rule --> cmtat : 46 (CODE_TRANSFER_REQUEST_NOT_APPROVED) + else approval exists + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(46) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) + rule -> rule : require(count != 0); approvalCounts[hash]-- \n(consume one approval) + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-conditional-transfer-light-multitoken-flow.png b/doc/img/rule-conditional-transfer-light-multitoken-flow.png new file mode 100644 index 0000000..9b08a08 Binary files /dev/null and b/doc/img/rule-conditional-transfer-light-multitoken-flow.png differ diff --git a/doc/img/rule-conditional-transfer-light-multitoken-flow.puml b/doc/img/rule-conditional-transfer-light-multitoken-flow.puml new file mode 100644 index 0000000..a1bdefd --- /dev/null +++ b/doc/img/rule-conditional-transfer-light-multitoken-flow.puml @@ -0,0 +1,42 @@ +@startuml +title RuleConditionalTransferLightMultiToken — transfer flow with a CMTAT token (operation rule) + +actor "Operator" as op +actor "Holder / Spender" as caller +participant "CMTAT token\n(one of several bound)" as cmtat +participant "RuleEngine" as engine +participant "RuleConditionalTransferLightMultiToken\napprovalCounts[hash(token,from,to,value)]" as rule + +== Phase 1: approval (token-scoped) == +op -> rule : approveTransfer(token, from, to, value) +rule -> rule : approvalCounts[hash(token,...)]++ \n(state change) +note right of rule + Approval key includes the token address, + so one token cannot consume another + token's approvals. +end note + +== Phase 2: the transfer == +caller -> cmtat : transfer(to, value) / transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestriction[From](... from, to, value) + engine -> rule : detectTransferRestriction[From](...) token = msg.sender context + note right of rule : approvalCounts[hash(token,from,to,value)] > 0 ? + alt no approval for (token, from, to, value) + rule --> cmtat : 46 (CODE_TRANSFER_REQUEST_NOT_APPROVED) + else approval exists + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(46) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) + rule -> rule : require(count != 0); approvalCounts[hash(token,...)]-- \n(consume one approval) + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-erc2980-flow.png b/doc/img/rule-erc2980-flow.png new file mode 100644 index 0000000..fa161b8 Binary files /dev/null and b/doc/img/rule-erc2980-flow.png differ diff --git a/doc/img/rule-erc2980-flow.puml b/doc/img/rule-erc2980-flow.puml new file mode 100644 index 0000000..fc03b4f --- /dev/null +++ b/doc/img/rule-erc2980-flow.puml @@ -0,0 +1,41 @@ +@startuml +title RuleERC2980 — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleERC2980\n(frozenlist + whitelist)" as rule + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + note right of rule + Frozenlist is checked FIRST and blocks + sender, recipient and spender. + Whitelist only gates the recipient + (senders may freely spend held tokens). + end note + alt from frozen + rule --> cmtat : 60 (CODE_ADDRESS_FROM_IS_FROZEN) + else to frozen + rule --> cmtat : 61 (CODE_ADDRESS_TO_IS_FROZEN) + else spender frozen + rule --> cmtat : 62 (CODE_ADDRESS_SPENDER_IS_FROZEN) + else to not whitelisted + rule --> cmtat : 63 (CODE_ADDRESS_TO_NOT_WHITELISTED) + else not frozen and recipient whitelisted + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-identity-registry-flow.png b/doc/img/rule-identity-registry-flow.png new file mode 100644 index 0000000..e252cc3 Binary files /dev/null and b/doc/img/rule-identity-registry-flow.png differ diff --git a/doc/img/rule-identity-registry-flow.puml b/doc/img/rule-identity-registry-flow.puml new file mode 100644 index 0000000..8f0c51d --- /dev/null +++ b/doc/img/rule-identity-registry-flow.puml @@ -0,0 +1,41 @@ +@startuml +title RuleIdentityRegistry — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleIdentityRegistry" as rule +participant "ERC-3643\nIdentity Registry" as reg + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + alt registry == address(0) + note right of rule : no registry configured -> rule passes + rule --> cmtat : 0 (TRANSFER_OK) + else registry configured + rule -> reg : isVerified(from) / isVerified(to) / isVerified(spender) + reg --> rule : true / false + alt from not verified + rule --> cmtat : 55 (CODE_ADDRESS_FROM_NOT_VERIFIED) + else to not verified + rule --> cmtat : 56 (CODE_ADDRESS_TO_NOT_VERIFIED) + else spender not verified + rule --> cmtat : 57 (CODE_ADDRESS_SPENDER_NOT_VERIFIED) + else all verified + rule --> cmtat : 0 (TRANSFER_OK) + end + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-max-total-supply-flow.png b/doc/img/rule-max-total-supply-flow.png new file mode 100644 index 0000000..ca2e5c9 Binary files /dev/null and b/doc/img/rule-max-total-supply-flow.png differ diff --git a/doc/img/rule-max-total-supply-flow.puml b/doc/img/rule-max-total-supply-flow.puml new file mode 100644 index 0000000..a13f6ac --- /dev/null +++ b/doc/img/rule-max-total-supply-flow.puml @@ -0,0 +1,34 @@ +@startuml +title RuleMaxTotalSupply — mint flow with a CMTAT token + +actor "Minter" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleMaxTotalSupply\n(max cap on totalSupply)" as rule + +caller -> cmtat : mint(to, value) (from == address(0)) + +group Validation (read-only) + cmtat -> engine : detectTransferRestriction(address(0), to, value) + engine -> rule : detectTransferRestriction(address(0), to, value) + note right of rule + Only mints (from == 0) are checked. + Transfers between holders and burns pass. + Reads token.totalSupply(). + end note + alt totalSupply() + value > maxTotalSupply + rule --> cmtat : 50 (CODE_MAX_TOTAL_SUPPLY_EXCEEDED) + else within cap + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(50) +else code == 0 + cmtat -> cmtat : _update: mint value to `to` + cmtat -> engine : transferred(operator, address(0), to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(address(0), to, value) +end +@enduml diff --git a/doc/img/rule-mint-allowance-flow.png b/doc/img/rule-mint-allowance-flow.png new file mode 100644 index 0000000..b4a091b Binary files /dev/null and b/doc/img/rule-mint-allowance-flow.png differ diff --git a/doc/img/rule-mint-allowance-flow.puml b/doc/img/rule-mint-allowance-flow.puml new file mode 100644 index 0000000..5e58762 --- /dev/null +++ b/doc/img/rule-mint-allowance-flow.puml @@ -0,0 +1,35 @@ +@startuml +title RuleMintAllowance — mint flow with a CMTAT token (operation rule) + +actor "Minter\n(spender)" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleMintAllowance\n(per-minter quota)" as rule + +caller -> cmtat : mint(to, value) (from == address(0)) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(minter, address(0), to, value) + engine -> rule : detectTransferRestrictionFrom(minter, address(0), to, value) + note right of rule + Only mints are gated. Needs the minter + (spender-aware path); the 3-arg + detectTransferRestriction always returns OK. + end note + alt allowance[minter] < value + rule --> cmtat : 70 (CODE_MINTER_ALLOWANCE_EXCEEDED) + else allowance[minter] >= value + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(70) +else code == 0 + cmtat -> cmtat : _update: mint value to `to` + cmtat -> engine : transferred(minter, address(0), to, value) + engine -> rule : transferred(minter, address(0), to, value) + rule -> rule : allowance[minter] -= value\n(state change) + cmtat --> caller : Transfer(address(0), to, value) +end +@enduml diff --git a/doc/img/rule-sanctionslist-flow.png b/doc/img/rule-sanctionslist-flow.png new file mode 100644 index 0000000..ea87e3d Binary files /dev/null and b/doc/img/rule-sanctionslist-flow.png differ diff --git a/doc/img/rule-sanctionslist-flow.puml b/doc/img/rule-sanctionslist-flow.puml new file mode 100644 index 0000000..d983be0 --- /dev/null +++ b/doc/img/rule-sanctionslist-flow.puml @@ -0,0 +1,41 @@ +@startuml +title RuleSanctionsList — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleSanctionsList" as rule +participant "Chainalysis oracle\n(SanctionsList)" as oracle + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + alt oracle == address(0) + note right of rule : no oracle configured -> rule passes + rule --> cmtat : 0 (TRANSFER_OK) + else oracle configured + rule -> oracle : isSanctioned(from) / isSanctioned(to) / isSanctioned(spender) + oracle --> rule : true / false + alt from sanctioned + rule --> cmtat : 30 (CODE_ADDRESS_FROM_IS_SANCTIONED) + else to sanctioned + rule --> cmtat : 31 (CODE_ADDRESS_TO_IS_SANCTIONED) + else spender sanctioned + rule --> cmtat : 32 (CODE_ADDRESS_SPENDER_IS_SANCTIONED) + else none sanctioned + rule --> cmtat : 0 (TRANSFER_OK) + end + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-spender-whitelist-flow.png b/doc/img/rule-spender-whitelist-flow.png new file mode 100644 index 0000000..72d1005 Binary files /dev/null and b/doc/img/rule-spender-whitelist-flow.png differ diff --git a/doc/img/rule-spender-whitelist-flow.puml b/doc/img/rule-spender-whitelist-flow.puml new file mode 100644 index 0000000..4bb71f0 --- /dev/null +++ b/doc/img/rule-spender-whitelist-flow.puml @@ -0,0 +1,35 @@ +@startuml +title RuleSpenderWhitelist — transfer flow with a CMTAT token + +actor "Caller" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleSpenderWhitelist\n(spender whitelist)" as rule + +alt direct transfer(to, value) (no spender) + caller -> cmtat : transfer(to, value) + cmtat -> engine : detectTransferRestriction(from, to, value) + engine -> rule : detectTransferRestriction(from, to, value) + note right of rule : direct transfers are always accepted + rule --> cmtat : 0 (TRANSFER_OK) +else transferFrom(from, to, value) (spender-initiated) + caller -> cmtat : transferFrom(from, to, value) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + note right of rule : isWhitelisted(spender)? + alt spender not whitelisted + rule --> cmtat : 66 (CODE_ADDRESS_SPENDER_NOT_WHITELISTED) + else spender whitelisted + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(66) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-whitelist-flow.png b/doc/img/rule-whitelist-flow.png new file mode 100644 index 0000000..3db0ed9 Binary files /dev/null and b/doc/img/rule-whitelist-flow.png differ diff --git a/doc/img/rule-whitelist-flow.puml b/doc/img/rule-whitelist-flow.puml new file mode 100644 index 0000000..4c2e44e --- /dev/null +++ b/doc/img/rule-whitelist-flow.puml @@ -0,0 +1,38 @@ +@startuml +title RuleWhitelist — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleWhitelist\n(from/to whitelist,\noptional checkSpender)" as rule + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + note right of rule + isWhitelisted(from)? + isWhitelisted(to)? + if checkSpender: isWhitelisted(spender)? + end note + alt from not whitelisted + rule --> cmtat : 21 (CODE_ADDRESS_FROM_NOT_WHITELISTED) + else to not whitelisted + rule --> cmtat : 22 (CODE_ADDRESS_TO_NOT_WHITELISTED) + else spender not whitelisted (checkSpender on) + rule --> cmtat : 23 (CODE_ADDRESS_SPENDER_NOT_WHITELISTED) + else all whitelisted + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-whitelist-wrapper-flow.png b/doc/img/rule-whitelist-wrapper-flow.png new file mode 100644 index 0000000..3d57484 Binary files /dev/null and b/doc/img/rule-whitelist-wrapper-flow.png differ diff --git a/doc/img/rule-whitelist-wrapper-flow.puml b/doc/img/rule-whitelist-wrapper-flow.puml new file mode 100644 index 0000000..0c200b1 --- /dev/null +++ b/doc/img/rule-whitelist-wrapper-flow.puml @@ -0,0 +1,44 @@ +@startuml +title RuleWhitelistWrapper — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleWhitelistWrapper\n(OR over child rules,\noptional checkSpender)" as wrap +participant "Child whitelist rules\n(IAddressList)" as child + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> wrap : detectTransferRestrictionFrom(spender, from, to, value) + note right of wrap + An address is whitelisted if ANY + child rule lists it (OR logic). + Iteration stops early once all + required addresses are resolved. + end note + loop over registered child rules until resolved + wrap -> child : isWhitelisted(from) / isWhitelisted(to) [/ spender] + child --> wrap : true / false + end + alt from listed in no child + wrap --> cmtat : 21 (CODE_ADDRESS_FROM_NOT_WHITELISTED) + else to listed in no child + wrap --> cmtat : 22 (CODE_ADDRESS_TO_NOT_WHITELISTED) + else spender listed in no child (checkSpender on) + wrap --> cmtat : 23 (CODE_ADDRESS_SPENDER_NOT_WHITELISTED) + else all resolved in some child + wrap --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> wrap : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/security/audits/AUDIT_OVERVIEW.md b/doc/security/audits/AUDIT_OVERVIEW.md new file mode 100644 index 0000000..fadf692 --- /dev/null +++ b/doc/security/audits/AUDIT_OVERVIEW.md @@ -0,0 +1,43 @@ +# Audit & Security-Analysis Overview + +> This is a security **overview** (analyses index + triage). It is **not** the vulnerability-reporting policy +> (that belongs in a root `SECURITY.md`). + +**Current package version:** `v0.4.0` +**Scope:** production contracts under `src/` — mocks/tests (`src/mocks`, `test/`) and dependencies (`lib/`) are excluded from static-analysis runs unless a run is explicitly marked *mocks included*. + +> ⚠️ This project has **not** undergone a formal third-party security audit. The analyses below are automated +> static analysis plus AI-assisted review, with the project team's triage. + +## Analyses + +| Date | Type | Tool / Source | Version | Reports | +|---|---|---|---|---| +| 2026-07-08 | Static analysis | Slither 0.11.5 | v0.4.0 | [report](./tools/v0.4.0/slither-report.md) · [feedback](./tools/v0.4.0/slither-report-feedback.md) | +| 2026-07-08 | Static analysis | Aderyn 0.6.5 | v0.4.0 | [report](./tools/v0.4.0/aderyn-report.md) · [feedback](./tools/v0.4.0/aderyn-report-feedback.md) | +| 2026-04-16 | Static analysis | Slither / Aderyn | v0.3.0 | [slither](./tools/v0.3.0/slither-report.md) · [aderyn](./tools/v0.3.0/aderyn-report.md) | +| 2026-03-16 | AI-assisted review | Wake Arena (Ackee) | v0.2.0 | [tools/v0.2.0](./tools/v0.2.0/) | + +## Static-analysis results (v0.4.0) + +| Tool | High | Medium | Low | Info | Relevant to fix? | +|---|---|---|---|---|---| +| Slither 0.11.5 | 2 | 6 | 16 | 12 | **No** — all false-positive or by-design ([feedback](./tools/v0.4.0/slither-report-feedback.md)) | +| Aderyn 0.6.5 | 0 | 0 | 8 findings | 0 | **No** — all Low, by-design or false-positive ([feedback](./tools/v0.4.0/aderyn-report-feedback.md)) | + +**Result: nothing to fix in `v0.4.0`.** The two High-severity Slither `arbitrary-send-erc20` hits are false positives — `approveAndTransferIfAllowed` (light + multi-token variants) is gated by `onlyTransferApprover`, a recorded approval, an allowance check, and a bound token. All other findings are accepted by design (centralization, unspecific pragma, PUSH0, template-method modifiers/empty blocks, `EnumerableSet` loop cost, spec-aligned naming) or tool limitations (`unused-return`/`unused-state`, per-contract analysis). Instance counts rose vs `v0.3.0` only because `v0.4.0` adds the `RuleMintAllowance` and `RuleConditionalTransferLightMultiToken` families; no new category appeared. + +## Substantive findings that were fixed (AI / manual review) + +From the Wake Arena AI review (v0.2.0) and internal `RuleMintAllowance` review: + +| Source | ID | Finding | Resolution | +|---|---|---|---| +| Wake Arena | H-1 | ConditionalTransferLight approvals not scoped by token | **Fixed** — single-token binding enforced in `bindToken`; `RuleConditionalTransferLight_TokenAlreadyBound` added. | +| Wake Arena | M-1 | Incomplete `supportsInterface` breaks ERC-165 discovery | **Fixed** — pre-computed interface IDs + `IERC7551Compliance`; full ERC-3643 `ICompliance` ID (`0x3144991c`) handled. | +| Wake Arena | I-1 | RuleERC2980 docs omit frozen spender on `transferFrom` | **Fixed (doc)** — README / AGENTS / CLAUDE updated. | +| Wake Arena | I-2 | `hasRole` admin implicitly passes all role checks | **Fixed (doc)** — documented intentional design + off-chain monitoring guidance. | +| Internal review | — | `RuleMintAllowance` allowance shared across bindings | **Fixed** — single-target binding enforced (`RuleMintAllowance_TokenAlreadyBound`). | +| Internal review | — | `SanctionListOracle` mock `removeFromSanctionsList` set `true` instead of `false` | **Fixed** — corrected to un-sanction; regression test added (mock/test-only). | + +See the per-version report directories under [`tools/`](./tools/) for the full outputs and triage. diff --git a/doc/security/audits/tools/v0.4.0/aderyn-report-feedback.md b/doc/security/audits/tools/v0.4.0/aderyn-report-feedback.md new file mode 100644 index 0000000..f49c27a --- /dev/null +++ b/doc/security/audits/tools/v0.4.0/aderyn-report-feedback.md @@ -0,0 +1,124 @@ +# Aderyn Report — Feedback + +Report version: `v0.4.0` +Aderyn report: [aderyn-report.md](./aderyn-report.md) +Tool: Aderyn 0.6.5 · Scope: production contracts only, 63 files (**mocks excluded**) +Feedback date: 2026-07-08 + +This document provides the project team's assessment of each finding reported by the Aderyn static analyser. Verdicts: + +| Verdict | Meaning | +|---|---| +| **Acknowledged** | Known, accepted by design; no change planned. | +| **By design** | Behaviour is intentional and required by the architecture. | +| **Fixed** | Resolved in the codebase. | +| **To fix** | Will be addressed in a future revision. | +| **False positive** | Tool mis-identification; no real issue exists. | + +All 8 findings are **Low** severity. No High or Medium issues were reported. + +--- + +## L-1: Centralization Risk — 62 instances + +**Verdict: By design** + +This library implements compliance rules for regulated security tokens (CMTAT / ERC-3643). Admin and operator roles are intentionally held by the issuer/compliance operator — an explicit trust assumption, not a bug. + +--- + +## L-2: Unspecific Solidity Pragma — 63 instances + +**Verdict: By design** + +The repository intentionally uses `pragma solidity ^0.8.20` to stay integrator-friendly, while the project itself is built deterministically via `foundry.toml` (`solc = 0.8.34`). + +--- + +## L-3: Address State Variable Set Without Checks — 1 instance + +**Verdict: False positive** + +Flagged location: `RuleSanctionsListBase._setSanctionListOracle` (`src/rules/validation/abstract/base/RuleSanctionsListBase.sol#L125`). + +The zero-address guard is enforced at the public boundary `setSanctionListOracle(...)` (`#L61-L62`, reverts with `RuleSanctionsList_OracleAddressZeroNotAllowed`). The internal setter accepts `address(0)` intentionally because `clearSanctionListOracle()` must disable the oracle. + +--- + +## L-4: PUSH0 Opcode — 63 instances + +**Verdict: By design — not applicable** + +The project targets `evm_version = "prague"` in `foundry.toml`; deployment targets are expected to support Shanghai+ opcodes including `PUSH0`. + +--- + +## L-5: Modifier Invoked Only Once — 2 instances + +**Verdict: By design (template method pattern)** + +Flagged modifiers are deliberate authorization wrappers over abstract `_authorize*` hooks. Inlining would weaken consistency across the AccessControl and Ownable variants. + +--- + +## L-6: Empty Block — 55 instances + +**Verdict: By design (template method pattern / interface compliance)** + +Most empty blocks are `_authorize*` hook implementations where the check is provided by modifiers (`onlyRole`, `onlyOwner`). Others are intentional no-ops required by shared interfaces in rules that are read-only for specific paths. + +--- + +## L-7: Costly operations inside loop — 6 instances + +**Verdict: By design — unavoidable** + +Flagged loops perform `EnumerableSet` insert/remove operations across batch APIs. These are inherently storage writes (`SSTORE`) per item, so linear gas growth is expected and unavoidable. + +--- + +## L-8: Unchecked Return — 13 instances + +**Verdict: Mixed — majority false positives** + +| Instance | Assessment | +|---|---| +| `_grantRole(DEFAULT_ADMIN_ROLE, admin)` in `AccessControlModuleStandalone` | **Acknowledged / low impact** — constructor path; a duplicate grant returns `false` and is not expected on fresh deployment. | +| `_addAddresses(...)` / `_removeAddresses(...)` batch helpers | **False positive** — `void` helpers, no return value to check. | +| `_listedAddresses.add/remove` in `RuleAddressSetInternal` single-item helpers | **False positive** — correctness guaranteed by outer pre-checks in public single-item methods. | +| `_whitelist.add/remove` and `_frozenlist.add/remove` in `RuleERC2980Internal` single-item helpers | **False positive** — same pre-check pattern. | +| batch `_add*Addresses` / `_remove*Addresses` helpers | **False positive** — no unchecked boolean return at the API boundary. | + +--- + +## Delta from v0.3.0 + +No new finding *categories*. Instance counts increased because `v0.4.0` adds the `RuleMintAllowance` and `RuleConditionalTransferLightMultiToken` contract families: + +| ID | v0.3.0 | v0.4.0 | +|---|---|---| +| L-1 Centralization Risk | 46 | 62 | +| L-2 Unspecific Pragma | 54 | 63 | +| L-3 Address Set Without Checks | 1 | 1 | +| L-4 PUSH0 Opcode | 54 | 63 | +| L-5 Modifier Invoked Once | 2 | 2 | +| L-6 Empty Block | 38 | 55 | +| L-7 Costly ops in loop | 6 | 6 | +| L-8 Unchecked Return | 13 | 13 | + +## Executive triage + +**Nothing to fix.** Every finding is Low severity and either by-design (centralization, pragma, PUSH0, template-method modifiers/empty blocks, EnumerableSet loop cost) or a false positive (L-3, and the majority of L-8). None is exploitable. + +## Summary + +| ID | Title | Instances | Verdict | +|---|---|---|---| +| L-1 | Centralization Risk | 62 | By design | +| L-2 | Unspecific Solidity Pragma | 63 | By design | +| L-3 | Address State Variable Set Without Checks | 1 | False positive | +| L-4 | PUSH0 Opcode | 63 | By design — not applicable | +| L-5 | Modifier Invoked Only Once | 2 | By design | +| L-6 | Empty Block | 55 | By design | +| L-7 | Costly operations inside loop | 6 | By design — unavoidable | +| L-8 | Unchecked Return | 13 | Mixed (majority false positives) | diff --git a/doc/security/audits/tools/v0.4.0/aderyn-report.md b/doc/security/audits/tools/v0.4.0/aderyn-report.md new file mode 100644 index 0000000..f356f74 --- /dev/null +++ b/doc/security/audits/tools/v0.4.0/aderyn-report.md @@ -0,0 +1,1816 @@ +# Aderyn Static Analysis — v0.4.0 + +**Tool:** Aderyn 0.6.5 · **Date:** 2026-07-08 · **Scope:** production contracts only (63 files) — **mocks EXCLUDED** +**Command:** + +```bash +aderyn -x mocks --output doc/security/audits/tools/v0.4.0/aderyn-report.md +``` + +**Severity tally:** `0 High · 8 Low (findings)` — no High/Medium. + +| ID | Title | Instances | Assessment | +|---|---|---|---| +| L-1 | Centralization Risk | 62 | **By design** — regulated-token issuer/compliance-operator model; explicit trust assumption. | +| L-2 | Unspecific Solidity Pragma | 63 | **By design** — `^0.8.20` for integrator-friendliness; the project pins `solc = 0.8.34` via `foundry.toml`. | +| L-3 | Address State Variable Set Without Checks | 1 | **False positive** — `RuleSanctionsListBase._setSanctionListOracle`; zero-check enforced at the public `setSanctionListOracle`, and `clearSanctionListOracle` needs `address(0)`. | +| L-4 | PUSH0 Opcode | 63 | **By design** — targets `evm_version = "prague"`; deployment targets support PUSH0. | +| L-5 | Modifier Invoked Only Once | 2 | **By design** — `_authorize*` template-method wrappers; inlining would break the AccessControl/Ownable abstraction. | +| L-6 | Empty Block | 55 | **By design** — `_authorize*` hooks gated by modifiers, plus required interface no-ops. | +| L-7 | Costly operations inside loop | 6 | **By design / unavoidable** — `EnumerableSet` batch ops are one `SSTORE` per element. | +| L-8 | Unchecked Return | 13 | **False positive (majority)** — `void`/pre-checked helpers; constructor `_grantRole` intentionally ignored. | + +**Bottom line: nothing to fix.** All findings are Low and either by-design or false positives — identical dispositions to `v0.3.0`. Instance counts rose (L-1 46→62, L-2/L-4 54→63, L-6 38→55) purely because `v0.4.0` adds the `RuleMintAllowance` and `RuleConditionalTransferLightMultiToken` contract families; no new *category* appeared. + +**See:** [feedback](./aderyn-report-feedback.md) · [audit overview](../../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: Address State Variable Set Without Checks](#l-3-address-state-variable-set-without-checks) + - [L-4: PUSH0 Opcode](#l-4-push0-opcode) + - [L-5: Modifier Invoked Only Once](#l-5-modifier-invoked-only-once) + - [L-6: Empty Block](#l-6-empty-block) + - [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 | 63 | +| Total nSLOC | 2843 | + + +## Files Details + +| Filepath | nSLOC | +| --- | --- | +| src/modules/AccessControlModuleStandalone.sol | 24 | +| src/modules/MetaTxModuleStandalone.sol | 6 | +| src/modules/Ownable2StepERC165Module.sol | 11 | +| src/modules/VersionModule.sol | 8 | +| src/rules/interfaces/IAddressList.sol | 15 | +| src/rules/interfaces/IERC2980.sol | 5 | +| src/rules/interfaces/IERC7943NonFungibleCompliance.sol | 19 | +| src/rules/interfaces/IIdentityRegistry.sol | 7 | +| src/rules/interfaces/ISanctionsList.sol | 4 | +| src/rules/interfaces/ITotalSupply.sol | 4 | +| src/rules/interfaces/ITransferContext.sol | 22 | +| src/rules/operation/RuleConditionalTransferLight.sol | 32 | +| src/rules/operation/RuleConditionalTransferLightMultiToken.sol | 34 | +| src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol | 33 | +| src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol | 30 | +| src/rules/operation/RuleMintAllowance.sol | 34 | +| src/rules/operation/RuleMintAllowanceOwnable2Step.sol | 27 | +| src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol | 57 | +| src/rules/operation/abstract/RuleConditionalTransferLightBase.sol | 123 | +| src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol | 18 | +| src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol | 194 | +| src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol | 19 | +| src/rules/operation/abstract/RuleMintAllowanceBase.sol | 137 | +| src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol | 14 | +| src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol | 66 | +| src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol | 39 | +| src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol | 7 | +| src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol | 14 | +| src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol | 15 | +| src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol | 79 | +| src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol | 30 | +| src/rules/validation/abstract/base/RuleBlacklistBase.sol | 100 | +| src/rules/validation/abstract/base/RuleERC2980Base.sol | 220 | +| src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol | 109 | +| src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol | 90 | +| src/rules/validation/abstract/base/RuleSanctionsListBase.sol | 108 | +| src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol | 52 | +| src/rules/validation/abstract/base/RuleWhitelistBase.sol | 70 | +| src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol | 145 | +| src/rules/validation/abstract/core/RuleNFTAdapter.sol | 111 | +| src/rules/validation/abstract/core/RuleTransferValidation.sol | 70 | +| src/rules/validation/abstract/core/RuleWhitelistShared.sol | 49 | +| src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol | 16 | +| src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol | 13 | +| src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol | 18 | +| src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol | 4 | +| src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol | 9 | +| src/rules/validation/deployment/RuleBlacklist.sol | 33 | +| src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol | 31 | +| src/rules/validation/deployment/RuleERC2980.sol | 33 | +| src/rules/validation/deployment/RuleERC2980Ownable2Step.sol | 35 | +| src/rules/validation/deployment/RuleIdentityRegistry.sol | 22 | +| src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol | 20 | +| src/rules/validation/deployment/RuleMaxTotalSupply.sol | 22 | +| src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol | 23 | +| src/rules/validation/deployment/RuleSanctionsList.sol | 34 | +| src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol | 35 | +| src/rules/validation/deployment/RuleSpenderWhitelist.sol | 34 | +| src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol | 35 | +| src/rules/validation/deployment/RuleWhitelist.sol | 35 | +| src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol | 35 | +| src/rules/validation/deployment/RuleWhitelistWrapper.sol | 65 | +| src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol | 40 | +| **Total** | **2843** | + + +## 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. + +
62 Found Instances + + +- Found in src/modules/AccessControlModuleStandalone.sol [Line: 13](../../../../../src/modules/AccessControlModuleStandalone.sol#L13) + + ```solidity + abstract contract AccessControlModuleStandalone is AccessControlEnumerable { + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 59](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L59) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 64](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L64) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 69](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L69) + + ```solidity + function _authorizeComplianceBindingChange(address) internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 51](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L51) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 56](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L56) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 22](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L22) + + ```solidity + Ownable2Step, + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 50](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L50) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 55](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L55) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 19](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L19) + + ```solidity + contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLightBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 57](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L57) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 62](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L62) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 67](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L67) + + ```solidity + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 60](../../../../../src/rules/operation/RuleMintAllowance.sol#L60) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 65](../../../../../src/rules/operation/RuleMintAllowance.sol#L65) + + ```solidity + function _authorizeSetMintAllowance() internal view virtual override onlyRole(ALLOWANCE_OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 75](../../../../../src/rules/operation/RuleMintAllowance.sol#L75) + + ```solidity + onlyRole(COMPLIANCE_MANAGER_ROLE) + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 19](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L19) + + ```solidity + contract RuleMintAllowanceOwnable2Step is RuleMintAllowanceBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 57](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L57) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 62](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L62) + + ```solidity + function _authorizeSetMintAllowance() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 67](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L67) + + ```solidity + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 55](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L55) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 60](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L60) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 15](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L15) + + ```solidity + contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 54](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L54) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 59](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L59) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 73](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L73) + + ```solidity + function _authorizeWhitelistAdd() internal view virtual override onlyRole(WHITELIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 78](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L78) + + ```solidity + function _authorizeWhitelistRemove() internal view virtual override onlyRole(WHITELIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 83](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L83) + + ```solidity + function _authorizeFrozenlistAdd() internal view virtual override onlyRole(FROZENLIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 88](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L88) + + ```solidity + function _authorizeFrozenlistRemove() internal view virtual override onlyRole(FROZENLIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 15](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L15) + + ```solidity + contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L57) + + ```solidity + function _authorizeWhitelistAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 62](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L62) + + ```solidity + function _authorizeWhitelistRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 67](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L67) + + ```solidity + function _authorizeFrozenlistAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 72](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L72) + + ```solidity + function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 56](../../../../../src/rules/validation/deployment/RuleIdentityRegistry.sol#L56) + + ```solidity + function _authorizeIdentityRegistryManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 14](../../../../../src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14) + + ```solidity + contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 53](../../../../../src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L53) + + ```solidity + function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 55](../../../../../src/rules/validation/deployment/RuleMaxTotalSupply.sol#L55) + + ```solidity + function _authorizeMaxTotalSupplyManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 14](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L14) + + ```solidity + contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L57) + + ```solidity + function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleSanctionsList.sol#L58) + + ```solidity + function _authorizeSanctionListManager() internal view virtual override onlyRole(SANCTIONLIST_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 17](../../../../../src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L17) + + ```solidity + contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 60](../../../../../src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L60) + + ```solidity + function _authorizeSanctionListManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L57) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 62](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L62) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 16](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L16) + + ```solidity + contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L58) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L63) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 65](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L65) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 70](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L70) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 75](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L75) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 15](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L15) + + ```solidity + contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L58) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L63) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 68](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L68) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 75](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L75) + + ```solidity + function _authorizeCheckSpenderManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 81](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L81) + + ```solidity + function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 86](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L86) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 16](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L16) + + ```solidity + contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L57) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L63) + + ```solidity + function _onlyRulesManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 68](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L68) + + ```solidity + function _onlyRulesLimitManager() internal view 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;` + +
63 Found Instances + + +- Found in src/modules/AccessControlModuleStandalone.sol [Line: 3](../../../../../src/modules/AccessControlModuleStandalone.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/MetaTxModuleStandalone.sol [Line: 3](../../../../../src/modules/MetaTxModuleStandalone.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/Ownable2StepERC165Module.sol [Line: 2](../../../../../src/modules/Ownable2StepERC165Module.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/VersionModule.sol [Line: 2](../../../../../src/modules/VersionModule.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IAddressList.sol [Line: 2](../../../../../src/rules/interfaces/IAddressList.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IERC2980.sol [Line: 2](../../../../../src/rules/interfaces/IERC2980.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IERC7943NonFungibleCompliance.sol [Line: 3](../../../../../src/rules/interfaces/IERC7943NonFungibleCompliance.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IIdentityRegistry.sol [Line: 3](../../../../../src/rules/interfaces/IIdentityRegistry.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ISanctionsList.sol [Line: 3](../../../../../src/rules/interfaces/ISanctionsList.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ITotalSupply.sol [Line: 2](../../../../../src/rules/interfaces/ITotalSupply.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ITransferContext.sol [Line: 2](../../../../../src/rules/interfaces/ITransferContext.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 2](../../../../../src/rules/operation/RuleMintAllowance.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleMintAllowanceBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleBlacklistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleBlacklistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleERC2980Base.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleSanctionsListBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleSanctionsListBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleWhitelistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleWhitelistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol [Line: 3](../../../../../src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleNFTAdapter.sol [Line: 2](../../../../../src/rules/validation/abstract/core/RuleNFTAdapter.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleTransferValidation.sol [Line: 3](../../../../../src/rules/validation/abstract/core/RuleTransferValidation.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleWhitelistShared.sol [Line: 2](../../../../../src/rules/validation/abstract/core/RuleWhitelistShared.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleIdentityRegistry.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupply.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSanctionsList.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +
+ + + +## L-3: Address State Variable Set Without Checks + +Check for `address(0)` when assigning values to address state variables. + +
1 Found Instances + + +- Found in src/rules/validation/abstract/base/RuleSanctionsListBase.sol [Line: 125](../../../../../src/rules/validation/abstract/base/RuleSanctionsListBase.sol#L125) + + ```solidity + sanctionsList = sanctionContractOracle_; + ``` + +
+ + + +## L-4: 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. + +
63 Found Instances + + +- Found in src/modules/AccessControlModuleStandalone.sol [Line: 3](../../../../../src/modules/AccessControlModuleStandalone.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/MetaTxModuleStandalone.sol [Line: 3](../../../../../src/modules/MetaTxModuleStandalone.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/Ownable2StepERC165Module.sol [Line: 2](../../../../../src/modules/Ownable2StepERC165Module.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/VersionModule.sol [Line: 2](../../../../../src/modules/VersionModule.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IAddressList.sol [Line: 2](../../../../../src/rules/interfaces/IAddressList.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IERC2980.sol [Line: 2](../../../../../src/rules/interfaces/IERC2980.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IERC7943NonFungibleCompliance.sol [Line: 3](../../../../../src/rules/interfaces/IERC7943NonFungibleCompliance.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IIdentityRegistry.sol [Line: 3](../../../../../src/rules/interfaces/IIdentityRegistry.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ISanctionsList.sol [Line: 3](../../../../../src/rules/interfaces/ISanctionsList.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ITotalSupply.sol [Line: 2](../../../../../src/rules/interfaces/ITotalSupply.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ITransferContext.sol [Line: 2](../../../../../src/rules/interfaces/ITransferContext.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 2](../../../../../src/rules/operation/RuleMintAllowance.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleMintAllowanceBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleBlacklistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleBlacklistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleERC2980Base.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleSanctionsListBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleSanctionsListBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleWhitelistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleWhitelistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol [Line: 3](../../../../../src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleNFTAdapter.sol [Line: 2](../../../../../src/rules/validation/abstract/core/RuleNFTAdapter.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleTransferValidation.sol [Line: 3](../../../../../src/rules/validation/abstract/core/RuleTransferValidation.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleWhitelistShared.sol [Line: 2](../../../../../src/rules/validation/abstract/core/RuleWhitelistShared.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleIdentityRegistry.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupply.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSanctionsList.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +
+ + + +## L-5: Modifier Invoked Only Once + +Consider removing the modifier or inlining the logic into the calling function. + +
2 Found Instances + + +- Found in src/rules/validation/abstract/base/RuleWhitelistBase.sol [Line: 72](../../../../../src/rules/validation/abstract/base/RuleWhitelistBase.sol#L72) + + ```solidity + modifier onlyCheckSpenderManager() { + ``` + +- Found in src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol [Line: 41](../../../../../src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L41) + + ```solidity + modifier onlyCheckSpenderManager() { + ``` + +
+ + + +## L-6: Empty Block + +Consider removing empty blocks. + +
55 Found Instances + + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 59](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L59) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 64](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L64) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 69](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L69) + + ```solidity + function _authorizeComplianceBindingChange(address) internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 51](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L51) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 56](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L56) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 50](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L50) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 55](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L55) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 57](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L57) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 62](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L62) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 67](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L67) + + ```solidity + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 60](../../../../../src/rules/operation/RuleMintAllowance.sol#L60) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 65](../../../../../src/rules/operation/RuleMintAllowance.sol#L65) + + ```solidity + function _authorizeSetMintAllowance() internal view virtual override onlyRole(ALLOWANCE_OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 70](../../../../../src/rules/operation/RuleMintAllowance.sol#L70) + + ```solidity + function _authorizeComplianceBindingChange(address) + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 57](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L57) + + ```solidity + function _onlyComplianceManager() internal virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 62](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L62) + + ```solidity + function _authorizeSetMintAllowance() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 67](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L67) + + ```solidity + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 63](../../../../../src/rules/operation/abstract/RuleMintAllowanceBase.sol#L63) + + ```solidity + function created(address, uint256) external virtual override onlyBoundToken {} + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 68](../../../../../src/rules/operation/abstract/RuleMintAllowanceBase.sol#L68) + + ```solidity + function destroyed(address, uint256) external virtual override onlyBoundToken {} + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 238](../../../../../src/rules/operation/abstract/RuleMintAllowanceBase.sol#L238) + + ```solidity + function _transferred(address, address, uint256) internal virtual { + ``` + +- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 47](../../../../../src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L47) + + ```solidity + function transferred(address, address, uint256) public view override(IERC3643IComplianceContract) {} + ``` + +- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 108](../../../../../src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L108) + + ```solidity + function _transferred(address, address, uint256) internal view virtual override { + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 55](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L55) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 60](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L60) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 54](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L54) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 59](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L59) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 73](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L73) + + ```solidity + function _authorizeWhitelistAdd() internal view virtual override onlyRole(WHITELIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 78](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L78) + + ```solidity + function _authorizeWhitelistRemove() internal view virtual override onlyRole(WHITELIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 83](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L83) + + ```solidity + function _authorizeFrozenlistAdd() internal view virtual override onlyRole(FROZENLIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 88](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L88) + + ```solidity + function _authorizeFrozenlistRemove() internal view virtual override onlyRole(FROZENLIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L57) + + ```solidity + function _authorizeWhitelistAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 62](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L62) + + ```solidity + function _authorizeWhitelistRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 67](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L67) + + ```solidity + function _authorizeFrozenlistAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 72](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L72) + + ```solidity + function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 56](../../../../../src/rules/validation/deployment/RuleIdentityRegistry.sol#L56) + + ```solidity + function _authorizeIdentityRegistryManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 53](../../../../../src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L53) + + ```solidity + function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 55](../../../../../src/rules/validation/deployment/RuleMaxTotalSupply.sol#L55) + + ```solidity + function _authorizeMaxTotalSupplyManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L57) + + ```solidity + function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleSanctionsList.sol#L58) + + ```solidity + function _authorizeSanctionListManager() internal view virtual override onlyRole(SANCTIONLIST_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 60](../../../../../src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L60) + + ```solidity + function _authorizeSanctionListManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L57) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 62](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L62) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L58) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L63) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 65](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L65) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 70](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L70) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 75](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L75) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L58) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L63) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 68](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L68) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 75](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L75) + + ```solidity + function _authorizeCheckSpenderManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 81](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L81) + + ```solidity + function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 86](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L86) + + ```solidity + function _onlyRulesLimitManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L57) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L63) + + ```solidity + function _onlyRulesManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 68](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L68) + + ```solidity + function _onlyRulesLimitManager() internal view virtual override onlyOwner {} + ``` + +
+ + + +## L-7: Costly operations inside loop + +Invoking `SSTORE` operations in loops may waste gas. Use a local variable to hold the loop computation result. + +
6 Found Instances + + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 41](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L41) + + ```solidity + for (uint256 i = 0; i < addressesToAdd.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 63](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L63) + + ```solidity + for (uint256 i = 0; i < addressesToRemove.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 47](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L47) + + ```solidity + for (uint256 i = 0; i < addressesToAdd.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 66](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L66) + + ```solidity + for (uint256 i = 0; i < addressesToRemove.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 105](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L105) + + ```solidity + for (uint256 i = 0; i < addressesToAdd.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 124](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L124) + + ```solidity + for (uint256 i = 0; i < addressesToRemove.length; ++i) { + ``` + +
+ + + +## L-8: Unchecked Return + +Function returns a value but it is ignored. Consider checking the return value. + +
13 Found Instances + + +- Found in src/modules/AccessControlModuleStandalone.sol [Line: 35](../../../../../src/modules/AccessControlModuleStandalone.sol#L35) + + ```solidity + _grantRole(DEFAULT_ADMIN_ROLE, admin); + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol [Line: 62](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol#L62) + + ```solidity + _addAddresses(targetAddresses); + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol [Line: 74](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol#L74) + + ```solidity + _removeAddresses(targetAddresses); + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 77](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L77) + + ```solidity + _listedAddresses.add(targetAddress); + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 85](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L85) + + ```solidity + _listedAddresses.remove(targetAddress); + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 80](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L80) + + ```solidity + _whitelist.add(targetAddress); + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 88](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L88) + + ```solidity + _whitelist.remove(targetAddress); + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 138](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L138) + + ```solidity + _frozenlist.add(targetAddress); + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 146](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L146) + + ```solidity + _frozenlist.remove(targetAddress); + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 87](../../../../../src/rules/validation/abstract/base/RuleERC2980Base.sol#L87) + + ```solidity + _addWhitelistAddresses(targetAddresses); + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 97](../../../../../src/rules/validation/abstract/base/RuleERC2980Base.sol#L97) + + ```solidity + _removeWhitelistAddresses(targetAddresses); + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 141](../../../../../src/rules/validation/abstract/base/RuleERC2980Base.sol#L141) + + ```solidity + _addFrozenlistAddresses(targetAddresses); + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 151](../../../../../src/rules/validation/abstract/base/RuleERC2980Base.sol#L151) + + ```solidity + _removeFrozenlistAddresses(targetAddresses); + ``` + +
+ + + diff --git a/doc/security/audits/tools/v0.4.0/slither-report-feedback.md b/doc/security/audits/tools/v0.4.0/slither-report-feedback.md new file mode 100644 index 0000000..3fa707e --- /dev/null +++ b/doc/security/audits/tools/v0.4.0/slither-report-feedback.md @@ -0,0 +1,92 @@ +# Slither Report — Feedback + +Report version: `v0.4.0` +Slither report: [slither-report.md](./slither-report.md) +Tool: Slither 0.11.5 · Scope: production contracts only (**mocks excluded**) +Feedback date: 2026-07-08 + +Verdicts: + +| Verdict | Meaning | +|---|---| +| **Acknowledged** | Known, accepted by design; no change planned. | +| **By design** | Behaviour is intentional and required by the architecture. | +| **Fixed** | Resolved in the codebase. | +| **To fix** | Will be addressed in a future revision. | +| **False positive** | Tool mis-identification; no real issue exists. | +| **Out of scope** | Reported location is in dependency code outside this repository. | + +--- + +## arbitrary-send-erc20 (High) — 2 instances + +**Verdict: False positive** + +Flagged functions: +- `RuleConditionalTransferLightBase.approveAndTransferIfAllowed(address,address,uint256)` (`src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L86-L101`) +- `RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed(address,address,address,uint256)` (`src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L121-L138`) + +`from` is a parameter, but neither is an arbitrary-drain primitive. In both: + +1. Access is restricted by `onlyTransferApprover`. +2. A transfer approval must have been recorded (`_approveTransfer(...)`) — the multi-token variant additionally requires `isTokenBound(token)`. +3. The contract checks `IERC20(token).allowance(from, address(this)) >= value` before transferring. +4. `SafeERC20.safeTransferFrom` only hardens ERC-20 return handling; it does not change the authorization model. + +The second instance is new in `v0.4.0` — it is the multi-token sibling of the `v0.3.0` finding and shares the identical guard chain. + +--- + +## unused-return (Medium) — 6 instances + +**Verdict: False positive** + +Flagged calls are `EnumerableSet.add/remove` in internal single-item helpers (`RuleAddressSetInternal`, `RuleERC2980Internal`). The public single-item entrypoints already perform presence/absence pre-checks, making the boolean return redundant in the internal helpers. + +--- + +## calls-loop (Low) — 16 instances + +**Verdict: By design** + +All instances stem from `RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(...)`, which must query each child whitelist rule to implement OR aggregation. The external calls in this loop are intrinsic to the wrapper design, and child rules are read-only. + +--- + +## assembly (Informational) — 2 instances + +**Verdict: By design** + +- `RuleConditionalTransferLightApprovalBase._transferHash(...)` (`#L125-L134`) +- `RuleConditionalTransferLightMultiTokenBase._transferHash(...)` (`#L334-L348`) + +Both use a small, memory-safe inline-assembly block to compute the transfer-tuple hash efficiently. Intentional and bounded. The second instance is new in `v0.4.0` (multi-token variant of the same pattern). + +--- + +## naming-convention (Informational) — 2 instances + +**Verdict: By design** + +`_operator` naming in `RuleERC2980Base.whitelist/frozenlist` mirrors the ERC-2980 interface. Keeping spec-aligned argument names is intentional. + +--- + +## unused-state (Informational) — 8 instances + +**Verdict: False positive** + +All instances are `RuleNFTAdapter` selector constants reported as unused in specific concrete contracts. They are part of inherited dispatch logic used across adapter paths; this is a per-contract inheritance-analysis limitation. + +--- + +## Delta from v0.3.0 + +- **arbitrary-send-erc20**: 1 → **2** (new: `RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed`, same guard chain). +- **assembly**: 1 → **2** (new: `RuleConditionalTransferLightMultiTokenBase._transferHash`, same memory-safe pattern). +- **unindexed-event-address** (2, Info, `lib/`): no longer reported — dependency paths excluded via `lib` filter. +- All other detectors and instance counts unchanged; all dispositions carried over. + +## Executive triage + +**Nothing to fix.** No finding is exploitable. The two High-severity `arbitrary-send-erc20` hits are false positives (authorized, approval-gated, allowance-checked compliance flow); everything else is by-design or a tool limitation. diff --git a/doc/security/audits/tools/v0.4.0/slither-report.md b/doc/security/audits/tools/v0.4.0/slither-report.md new file mode 100644 index 0000000..163b2ce --- /dev/null +++ b/doc/security/audits/tools/v0.4.0/slither-report.md @@ -0,0 +1,337 @@ +# Slither Static Analysis — v0.4.0 + +**Tool:** Slither 0.11.5 · **Date:** 2026-07-08 · **Scope:** production contracts only — **mocks EXCLUDED** +**Command:** + +```bash +slither . --checklist --filter-paths "node_modules,lib,test,forge-std,mocks" \ + > doc/security/audits/tools/v0.4.0/slither-report.md +``` + +**Severity tally:** `2 High · 6 Medium · 16 Low · 12 Info` + +| Detector | Severity | Instances | Assessment | +|---|---|---|---| +| arbitrary-send-erc20 | High | 2 | **False positive** — `approveAndTransferIfAllowed` in both `RuleConditionalTransferLightBase` and `RuleConditionalTransferLightMultiTokenBase` is gated by `onlyTransferApprover`, a recorded approval, an allowance check, and a bound token. | +| unused-return | Medium | 6 | **False positive** — `EnumerableSet.add/remove` booleans in internal single-item helpers; the public entrypoints pre-check presence. | +| calls-loop | Low | 16 | **By design** — `RuleWhitelistWrapperBase` must query each child rule to implement OR aggregation (children are read-only). | +| assembly | Info | 2 | **By design** — memory-safe hash assembly in `_transferHash` (light + multi-token variants). | +| naming-convention | Info | 2 | **By design** — `_operator` argument names mirror the ERC-2980 interface. | +| unused-state | Info | 8 | **False positive** — `RuleNFTAdapter` selector constants used via inherited dispatch (per-contract analysis limitation). | + +**Bottom line: nothing to fix.** Every finding is a false positive or accepted by design; identical dispositions to `v0.3.0`, with a 2nd instance each of `arbitrary-send-erc20` and `assembly` from the new `RuleConditionalTransferLightMultiToken` contract (same guarded/design patterns). + +**See:** [feedback](./slither-report-feedback.md) · [audit overview](../../AUDIT_OVERVIEW.md) + +--- + +**THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results. +Summary + - [arbitrary-send-erc20](#arbitrary-send-erc20) (2 results) (High) + - [unused-return](#unused-return) (6 results) (Medium) + - [calls-loop](#calls-loop) (16 results) (Low) + - [assembly](#assembly) (2 results) (Informational) + - [naming-convention](#naming-convention) (2 results) (Informational) + - [unused-state](#unused-state) (8 results) (Informational) +## arbitrary-send-erc20 +Impact: High +Confidence: High + - [ ] ID-0 +[RuleConditionalTransferLightBase.approveAndTransferIfAllowed(address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L86-L101) uses arbitrary from in transferFrom: [IERC20(token).safeTransferFrom(from,to,value)](src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L99) + +src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L86-L101 + + + - [ ] ID-1 +[RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed(address,address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L121-L138) uses arbitrary from in transferFrom: [IERC20(token).safeTransferFrom(from,to,value)](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L136) + +src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L121-L138 + + +## unused-return +Impact: Medium +Confidence: Medium + - [ ] ID-2 +[RuleAddressSetInternal._removeAddress(address)](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L84-L86) ignores return value by [_listedAddresses.remove(targetAddress)](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L85) + +src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L84-L86 + + + - [ ] ID-3 +[RuleAddressSetInternal._addAddress(address)](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L76-L78) ignores return value by [_listedAddresses.add(targetAddress)](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L77) + +src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L76-L78 + + + - [ ] ID-4 +[RuleERC2980Internal._removeFrozenlistAddress(address)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L145-L147) ignores return value by [_frozenlist.remove(targetAddress)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L146) + +src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L145-L147 + + + - [ ] ID-5 +[RuleERC2980Internal._addFrozenlistAddress(address)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L137-L139) ignores return value by [_frozenlist.add(targetAddress)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L138) + +src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L137-L139 + + + - [ ] ID-6 +[RuleERC2980Internal._removeWhitelistAddress(address)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L87-L89) ignores return value by [_whitelist.remove(targetAddress)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L88) + +src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L87-L89 + + + - [ ] ID-7 +[RuleERC2980Internal._addWhitelistAddress(address)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L79-L81) ignores return value by [_whitelist.add(targetAddress)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L80) + +src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L79-L81 + + +## calls-loop +Impact: Low +Confidence: Medium + - [ ] ID-8 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleNFTAdapter.canTransfer(address,address,uint256,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-9 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleWhitelistWrapperBase.isVerified(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-10 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleNFTAdapter.transferred(address,address,uint256,uint256) + RuleWhitelistWrapperBase._transferred(address,address,uint256) + RuleWhitelistShared._transferred(address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-11 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleWhitelistShared.transferred(address,address,address,uint256) + RuleWhitelistShared._transferredFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-12 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleNFTAdapter.transferred(ITransferContext.MultiTokenTransferContext) + RuleWhitelistShared._transferredFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-13 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleNFTAdapter.canTransferFrom(address,address,address,uint256,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-14 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleWhitelistShared.transferred(address,address,uint256) + RuleWhitelistWrapperBase._transferred(address,address,uint256) + RuleWhitelistShared._transferred(address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-15 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleNFTAdapter.transferred(ITransferContext.FungibleTransferContext) + RuleWhitelistShared._transferredFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-16 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleTransferValidation.canTransfer(address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-17 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleNFTAdapter.detectTransferRestrictionFrom(address,address,address,uint256,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-18 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleTransferValidation.canTransferFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-19 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleWhitelistWrapperHarnessInternal.exposedTransferredSpenderInternal(address,address,address,uint256) + RuleWhitelistWrapperBase._transferred(address,address,address,uint256) + RuleWhitelistShared._transferredFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-20 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleNFTAdapter.detectTransferRestriction(address,address,uint256,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-21 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleNFTAdapter.transferred(address,address,address,uint256,uint256) + RuleWhitelistShared._transferredFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-22 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleTransferValidation.detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + + - [ ] ID-23 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L227) + Calls stack containing the loop: + RuleTransferValidation.detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L216-L247 + + +## assembly +Impact: Informational +Confidence: High + - [ ] ID-24 +[RuleConditionalTransferLightApprovalBase._transferHash(address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L125-L134) uses assembly + - [INLINE ASM](src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L127-L133) + +src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L125-L134 + + + - [ ] ID-25 +[RuleConditionalTransferLightMultiTokenBase._transferHash(address,address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L334-L348) uses assembly + - [INLINE ASM](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L340-L347) + +src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L334-L348 + + +## naming-convention +Impact: Informational +Confidence: High + - [ ] ID-26 +Parameter [RuleERC2980Base.frozenlist(address)._operator](src/rules/validation/abstract/base/RuleERC2980Base.sol#L328) is not in mixedCase + +src/rules/validation/abstract/base/RuleERC2980Base.sol#L328 + + + - [ ] ID-27 +Parameter [RuleERC2980Base.whitelist(address)._operator](src/rules/validation/abstract/base/RuleERC2980Base.sol#L279) is not in mixedCase + +src/rules/validation/abstract/base/RuleERC2980Base.sol#L279 + + +## unused-state +Impact: Informational +Confidence: High + - [ ] ID-28 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_RULE_ENGINE](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L27) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L54) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L27 + + + - [ ] ID-29 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC7943](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L31-L32) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L54) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L31-L32 + + + - [ ] ID-30 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC7943_FROM](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L36-L37) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L54) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L36-L37 + + + - [ ] ID-31 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC3643](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L23) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L54) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L23 + + + - [ ] ID-32 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC7943](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L31-L32) is never used in [RuleIdentityRegistry](src/rules/validation/deployment/RuleIdentityRegistry.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L31-L32 + + + - [ ] ID-33 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_RULE_ENGINE](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L27) is never used in [RuleIdentityRegistry](src/rules/validation/deployment/RuleIdentityRegistry.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L27 + + + - [ ] ID-34 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC7943_FROM](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L36-L37) is never used in [RuleIdentityRegistry](src/rules/validation/deployment/RuleIdentityRegistry.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L36-L37 + + + - [ ] ID-35 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC3643](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L23) is never used in [RuleIdentityRegistry](src/rules/validation/deployment/RuleIdentityRegistry.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L23 + + diff --git a/doc/specification/RulesSpecificationv0.3.0.pdf b/doc/specification/RulesSpecificationv0.3.0.pdf new file mode 100644 index 0000000..53754ab Binary files /dev/null and b/doc/specification/RulesSpecificationv0.3.0.pdf differ diff --git a/doc/specification/RulesSpecificationv0.2.0.pdf b/doc/specification/archive/RulesSpecificationv0.2.0.pdf similarity index 100% rename from doc/specification/RulesSpecificationv0.2.0.pdf rename to doc/specification/archive/RulesSpecificationv0.2.0.pdf diff --git a/doc/specification/cover_page.odg b/doc/specification/cover_page.odg index 855eff6..4645f13 100644 Binary files a/doc/specification/cover_page.odg and b/doc/specification/cover_page.odg differ diff --git a/doc/specification/cover_page.pdf b/doc/specification/cover_page.pdf index 0b3519c..8dd7fe2 100644 Binary files a/doc/specification/cover_page.pdf and b/doc/specification/cover_page.pdf differ diff --git a/doc/surya/surya_graph/surya_graph_MockERC20WithTransferContext.sol.png b/doc/surya/surya_graph/surya_graph_MockERC20WithTransferContext.sol.png index e69de29..70b2150 100644 Binary files a/doc/surya/surya_graph/surya_graph_MockERC20WithTransferContext.sol.png and b/doc/surya/surya_graph/surya_graph_MockERC20WithTransferContext.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_MockERC721WithTransferContext.sol.png b/doc/surya/surya_graph/surya_graph_MockERC721WithTransferContext.sol.png index e69de29..b4e620e 100644 Binary files a/doc/surya/surya_graph/surya_graph_MockERC721WithTransferContext.sol.png and b/doc/surya/surya_graph/surya_graph_MockERC721WithTransferContext.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_Ownable2StepERC165Module.sol.png b/doc/surya/surya_graph/surya_graph_Ownable2StepERC165Module.sol.png new file mode 100644 index 0000000..aea364a Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_Ownable2StepERC165Module.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleBlacklist.sol.png b/doc/surya/surya_graph/surya_graph_RuleBlacklist.sol.png index e69de29..644c975 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleBlacklist.sol.png and b/doc/surya/surya_graph/surya_graph_RuleBlacklist.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleBlacklistOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleBlacklistOwnable2Step.sol.png index e69de29..36e969b 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleBlacklistOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleBlacklistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png index 184ce94..66c1899 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png new file mode 100644 index 0000000..4ef47d6 Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenBase.sol.png new file mode 100644 index 0000000..7e79f6d Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenBase.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png new file mode 100644 index 0000000..5b4fa6b Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png new file mode 100644 index 0000000..836581b Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png index 1138317..8aac685 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleERC2980.sol.png b/doc/surya/surya_graph/surya_graph_RuleERC2980.sol.png index e69de29..a620ceb 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleERC2980.sol.png and b/doc/surya/surya_graph/surya_graph_RuleERC2980.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleERC2980Ownable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleERC2980Ownable2Step.sol.png index e69de29..69fec14 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleERC2980Ownable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleERC2980Ownable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryOwnable2Step.sol.png index 2740710..c5964a6 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyOwnable2Step.sol.png index 478dd4c..50a285b 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png b/doc/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png new file mode 100644 index 0000000..b0c8706 Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleMintAllowanceBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceBase.sol.png new file mode 100644 index 0000000..90df11f Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceBase.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleMintAllowanceInvariantStorage.sol.png b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceInvariantStorage.sol.png new file mode 100644 index 0000000..5b4fa6b Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceInvariantStorage.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleMintAllowanceOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceOwnable2Step.sol.png new file mode 100644 index 0000000..8ed397f Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleSanctionsListOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleSanctionsListOwnable2Step.sol.png index efe30a2..f3f52a4 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleSanctionsListOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleSanctionsListOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelist.sol.png b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelist.sol.png index e69de29..4eadee9 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelist.sol.png and b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelist.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistBase.sol.png index 2095cd9..2db56c3 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistBase.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistOwnable2Step.sol.png index e69de29..d687e14 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelist.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelist.sol.png index e69de29..86a8825 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelist.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelist.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png index 49ddfce..0d7ea6b 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistOwnable2Step.sol.png index e69de29..87c918b 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapper.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapper.sol.png index 071894a..d521009 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapper.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapper.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png index 07a074e..3523933 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperOwnable2Step.sol.png index 9d1c456..719eecd 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_Ownable2StepERC165Module.sol.png b/doc/surya/surya_inheritance/surya_inheritance_Ownable2StepERC165Module.sol.png new file mode 100644 index 0000000..bde6d04 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_Ownable2StepERC165Module.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistOwnable2Step.sol.png index 131b819..8a9378e 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png index d56372a..dc9f809 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiToken.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiToken.sol.png new file mode 100644 index 0000000..e05b601 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiToken.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenBase.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenBase.sol.png new file mode 100644 index 0000000..b1e4857 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenBase.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png new file mode 100644 index 0000000..afe3a62 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png new file mode 100644 index 0000000..34cb43a Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightOwnable2Step.sol.png index 219317e..9eb4c54 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleERC2980Ownable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleERC2980Ownable2Step.sol.png index 2cec104..bbf3408 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleERC2980Ownable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleERC2980Ownable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleIdentityRegistryOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleIdentityRegistryOwnable2Step.sol.png index 68e2d20..4d69bb6 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleIdentityRegistryOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleIdentityRegistryOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyOwnable2Step.sol.png index 7e9a046..5868131 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowance.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowance.sol.png new file mode 100644 index 0000000..3c6c42c Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowance.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceBase.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceBase.sol.png new file mode 100644 index 0000000..3b91e2b Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceBase.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceInvariantStorage.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceInvariantStorage.sol.png new file mode 100644 index 0000000..bf91969 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceInvariantStorage.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceOwnable2Step.sol.png new file mode 100644 index 0000000..2213385 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleSanctionsListOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleSanctionsListOwnable2Step.sol.png index 6cc595f..cf44c9e 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleSanctionsListOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleSanctionsListOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleSpenderWhitelistOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleSpenderWhitelistOwnable2Step.sol.png index 7e43526..4f92c3f 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleSpenderWhitelistOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleSpenderWhitelistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistOwnable2Step.sol.png index 003c369..d3fc429 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapper.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapper.sol.png index c1f68fc..98f6cc6 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapper.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapper.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapperOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapperOwnable2Step.sol.png index c450eba..18dd771 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapperOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapperOwnable2Step.sol.png differ diff --git a/doc/surya/surya_report/surya_report_Ownable2StepERC165Module.sol.md b/doc/surya/surya_report/surya_report_Ownable2StepERC165Module.sol.md new file mode 100644 index 0000000..2383145 --- /dev/null +++ b/doc/surya/surya_report/surya_report_Ownable2StepERC165Module.sol.md @@ -0,0 +1,27 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./modules/Ownable2StepERC165Module.sol | 50ac2979efcf101b8db4fbc27720e9e92fedd47a | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **Ownable2StepERC165Module** | Implementation | ERC165 ||| +| └ | supportsInterface | Public ❗️ | |NO❗️ | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleBlacklistOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleBlacklistOwnable2Step.sol.md index 3b4d15f..8fadfa0 100644 --- a/doc/surya/surya_report/surya_report_RuleBlacklistOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleBlacklistOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleBlacklistOwnable2Step.sol | 7124f42b2a3b6352300ffdaba32b8f6ce31862bd | +| ./rules/validation/deployment/RuleBlacklistOwnable2Step.sol | d1d803fa9f1f3db0a21ef8cde1b9a96e910cd728 | ### Contracts Description Table @@ -15,10 +15,11 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleBlacklistOwnable2Step** | Implementation | RuleBlacklistBase, Ownable2Step ||| +| **RuleBlacklistOwnable2Step** | Implementation | RuleBlacklistBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleBlacklistBase Ownable | | └ | _authorizeAddressListAdd | Internal 🔒 | | onlyOwner | | └ | _authorizeAddressListRemove | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md index bc4c64c..d105630 100644 --- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/operation/RuleConditionalTransferLight.sol | 0296e0b6f9efba3f65c654982ee5e8b8b0d6b52c | +| ./rules/operation/RuleConditionalTransferLight.sol | 62dd5be103c29fa0ad3ec6ee371468a8e0edc01f | ### Contracts Description Table @@ -15,11 +15,12 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleConditionalTransferLight** | Implementation | AccessControlModuleStandalone, RuleConditionalTransferLightBase ||| +| **RuleConditionalTransferLight** | Implementation | AccessControlModuleStandalone, RuleConditionalTransferLightBase, ERC3643ComplianceRolesStorage ||| | └ | | Public ❗️ | 🛑 | AccessControlModuleStandalone | | └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _authorizeTransferApproval | Internal 🔒 | | onlyRole | | └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyRole | +| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyRole | ### Legend diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md index 2a94c95..2f3938f 100644 --- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/operation/abstract/RuleConditionalTransferLightBase.sol | 32e9a0425e0706764501785c3a4220b65ddeaec7 | +| ./rules/operation/abstract/RuleConditionalTransferLightBase.sol | ca4e40de3d7c746518faf45ef82d6c21c846e650 | ### Contracts Description Table diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiToken.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiToken.sol.md new file mode 100644 index 0000000..2d1dbe6 --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiToken.sol.md @@ -0,0 +1,30 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/RuleConditionalTransferLightMultiToken.sol | 5a26c4179ebee4b52842ca0073933366fcb4ce6c | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleConditionalTransferLightMultiToken** | Implementation | AccessControlModuleStandalone, RuleConditionalTransferLightMultiTokenBase, ERC3643ComplianceRolesStorage ||| +| └ | | Public ❗️ | 🛑 | AccessControlModuleStandalone | +| └ | supportsInterface | Public ❗️ | |NO❗️ | +| └ | _authorizeTransferApproval | Internal 🔒 | | onlyRole | +| └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyRole | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenBase.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenBase.sol.md new file mode 100644 index 0000000..a163edc --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenBase.sol.md @@ -0,0 +1,48 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol | 005bde36fdb674f7b60284457c9206700fe85387 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleConditionalTransferLightMultiTokenBase** | Implementation | VersionModule, ERC3643ComplianceModule, RuleConditionalTransferLightMultiTokenInvariantStorage, IRule ||| +| └ | _authorizeTransferApproval | Internal 🔒 | | | +| └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ | +| └ | messageForTransferRestriction | External ❗️ | |NO❗️ | +| └ | created | External ❗️ | 🛑 | onlyBoundToken | +| └ | destroyed | External ❗️ | 🛑 | onlyBoundToken | +| └ | approveTransfer | Public ❗️ | 🛑 | onlyTransferApprover | +| └ | cancelTransferApproval | Public ❗️ | 🛑 | onlyTransferApprover | +| └ | approvedCount | Public ❗️ | |NO❗️ | +| └ | approveAndTransferIfAllowed | Public ❗️ | 🛑 | onlyTransferApprover | +| └ | transferred | Public ❗️ | 🛑 | onlyTransferExecutor | +| └ | transferred | Public ❗️ | 🛑 | onlyTransferExecutor | +| └ | transferred | External ❗️ | 🛑 | onlyTransferExecutor | +| └ | detectTransferRestriction | Public ❗️ | |NO❗️ | +| └ | detectTransferRestrictionFrom | Public ❗️ | |NO❗️ | +| └ | canTransfer | Public ❗️ | |NO❗️ | +| └ | canTransferFrom | Public ❗️ | |NO❗️ | +| └ | _authorizeTransferExecution | Internal 🔒 | | | +| └ | _authorizeComplianceBindingChange | Internal 🔒 | 🛑 | | +| └ | _approveTransfer | Internal 🔒 | 🛑 | | +| └ | _cancelTransferApproval | Internal 🔒 | 🛑 | | +| └ | _transferred | Internal 🔒 | 🛑 | | +| └ | _transferHash | Internal 🔒 | | | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.md new file mode 100644 index 0000000..997b9a4 --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.md @@ -0,0 +1,26 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol | 8376ee8d2ce771fdb57e13ef3832258220f89496 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleConditionalTransferLightMultiTokenInvariantStorage** | Implementation | RuleSharedInvariantStorage ||| + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.md new file mode 100644 index 0000000..20808f5 --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.md @@ -0,0 +1,30 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol | 3da3c69fd927f5e21b6881156369ff3c7957af38 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleConditionalTransferLightMultiTokenOwnable2Step** | Implementation | RuleConditionalTransferLightMultiTokenBase, Ownable2Step, Ownable2StepERC165Module ||| +| └ | | Public ❗️ | 🛑 | Ownable | +| └ | supportsInterface | Public ❗️ | |NO❗️ | +| └ | _authorizeTransferApproval | Internal 🔒 | | onlyOwner | +| └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyOwner | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md index fdf137b..440233a 100644 --- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/operation/RuleConditionalTransferLightOwnable2Step.sol | 52c514a3996546640354595fd890941d0f8875ac | +| ./rules/operation/RuleConditionalTransferLightOwnable2Step.sol | e94c0dc5da33a08b1438eed005fffaf59d213def | ### Contracts Description Table @@ -15,11 +15,12 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleConditionalTransferLightOwnable2Step** | Implementation | RuleConditionalTransferLightBase, Ownable2Step ||| +| **RuleConditionalTransferLightOwnable2Step** | Implementation | RuleConditionalTransferLightBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | Ownable | | └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _authorizeTransferApproval | Internal 🔒 | | onlyOwner | | └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyOwner | +| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyOwner | ### Legend diff --git a/doc/surya/surya_report/surya_report_RuleERC2980Ownable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleERC2980Ownable2Step.sol.md index 8a62e10..5ea80fa 100644 --- a/doc/surya/surya_report/surya_report_RuleERC2980Ownable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleERC2980Ownable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleERC2980Ownable2Step.sol | 65d1a4469f2d80fda332499a277f75cfd96f07b3 | +| ./rules/validation/deployment/RuleERC2980Ownable2Step.sol | 059ea6668baf03d1ac9f3c7f3c85e2b09c01306f | ### Contracts Description Table @@ -15,12 +15,13 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleERC2980Ownable2Step** | Implementation | RuleERC2980Base, Ownable2Step ||| +| **RuleERC2980Ownable2Step** | Implementation | RuleERC2980Base, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleERC2980Base Ownable | | └ | _authorizeWhitelistAdd | Internal 🔒 | | onlyOwner | | └ | _authorizeWhitelistRemove | Internal 🔒 | | onlyOwner | | └ | _authorizeFrozenlistAdd | Internal 🔒 | | onlyOwner | | └ | _authorizeFrozenlistRemove | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleIdentityRegistryOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleIdentityRegistryOwnable2Step.sol.md index 5c2f7a8..49a8878 100644 --- a/doc/surya/surya_report/surya_report_RuleIdentityRegistryOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleIdentityRegistryOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol | f498c93b91bcdd36aa42817847953f3a308d51a6 | +| ./rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol | 99b6856a407f4a428a54b05ff817868f1d74e909 | ### Contracts Description Table @@ -15,9 +15,10 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleIdentityRegistryOwnable2Step** | Implementation | RuleIdentityRegistryBase, Ownable2Step ||| +| **RuleIdentityRegistryOwnable2Step** | Implementation | RuleIdentityRegistryBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleIdentityRegistryBase Ownable | | └ | _authorizeIdentityRegistryManager | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | ### Legend diff --git a/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyOwnable2Step.sol.md index 2d964cc..4445776 100644 --- a/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol | 6f8fc83269973f6497ca27721ae7b893cd6104e0 | +| ./rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol | f41457c8a1aefbb09ff45cbee3083f9111525686 | ### Contracts Description Table @@ -15,9 +15,10 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleMaxTotalSupplyOwnable2Step** | Implementation | RuleMaxTotalSupplyBase, Ownable2Step ||| +| **RuleMaxTotalSupplyOwnable2Step** | Implementation | RuleMaxTotalSupplyBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleMaxTotalSupplyBase Ownable | | └ | _authorizeMaxTotalSupplyManager | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | ### Legend diff --git a/doc/surya/surya_report/surya_report_RuleMintAllowance.sol.md b/doc/surya/surya_report/surya_report_RuleMintAllowance.sol.md new file mode 100644 index 0000000..9bcd911 --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleMintAllowance.sol.md @@ -0,0 +1,31 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/RuleMintAllowance.sol | 20af9da8f7f75bbedd817de955e768f9a716b54c | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleMintAllowance** | Implementation | AccessControlModuleStandalone, RuleMintAllowanceBase, ERC3643ComplianceRolesStorage ||| +| └ | | Public ❗️ | 🛑 | AccessControlModuleStandalone | +| └ | supportsInterface | Public ❗️ | |NO❗️ | +| └ | _authorizeSetMintAllowance | Internal 🔒 | | onlyRole | +| └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyRole | +| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyRole | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleMintAllowanceBase.sol.md b/doc/surya/surya_report/surya_report_RuleMintAllowanceBase.sol.md new file mode 100644 index 0000000..530992f --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleMintAllowanceBase.sol.md @@ -0,0 +1,45 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/abstract/RuleMintAllowanceBase.sol | 1676c17359978a8e901da2f022762462a44a7968 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleMintAllowanceBase** | Implementation | VersionModule, ERC3643ComplianceModule, RuleMintAllowanceInvariantStorage, IRule ||| +| └ | _authorizeSetMintAllowance | Internal 🔒 | | | +| └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ | +| └ | created | External ❗️ | 🛑 | onlyBoundToken | +| └ | destroyed | External ❗️ | 🛑 | onlyBoundToken | +| └ | setMintAllowance | Public ❗️ | 🛑 | onlyAllowanceOperator | +| └ | increaseMintAllowance | Public ❗️ | 🛑 | onlyAllowanceOperator | +| └ | decreaseMintAllowance | Public ❗️ | 🛑 | onlyAllowanceOperator | +| └ | bindToken | Public ❗️ | 🛑 | onlyComplianceManager | +| └ | messageForTransferRestriction | Public ❗️ | |NO❗️ | +| └ | transferred | Public ❗️ | 🛑 | onlyBoundToken | +| └ | transferred | Public ❗️ | 🛑 | onlyBoundToken | +| └ | detectTransferRestriction | Public ❗️ | |NO❗️ | +| └ | detectTransferRestrictionFrom | Public ❗️ | |NO❗️ | +| └ | canTransfer | Public ❗️ | |NO❗️ | +| └ | canTransferFrom | Public ❗️ | |NO❗️ | +| └ | _detectTransferRestrictionFrom | Internal 🔒 | | | +| └ | _transferred | Internal 🔒 | 🛑 | | +| └ | _transferredFrom | Internal 🔒 | 🛑 | | +| └ | _setMintAllowance | Internal 🔒 | 🛑 | | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleMintAllowanceInvariantStorage.sol.md b/doc/surya/surya_report/surya_report_RuleMintAllowanceInvariantStorage.sol.md new file mode 100644 index 0000000..8f8e894 --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleMintAllowanceInvariantStorage.sol.md @@ -0,0 +1,26 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol | 99b25d9540492defbc4168b3f07b47498a07f081 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleMintAllowanceInvariantStorage** | Implementation | RuleSharedInvariantStorage ||| + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleMintAllowanceOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleMintAllowanceOwnable2Step.sol.md new file mode 100644 index 0000000..171f23a --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleMintAllowanceOwnable2Step.sol.md @@ -0,0 +1,31 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/RuleMintAllowanceOwnable2Step.sol | 32837216a0005192e2bfbe7a91ab7bd782a32238 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleMintAllowanceOwnable2Step** | Implementation | RuleMintAllowanceBase, Ownable2Step, Ownable2StepERC165Module ||| +| └ | | Public ❗️ | 🛑 | Ownable | +| └ | supportsInterface | Public ❗️ | |NO❗️ | +| └ | _authorizeSetMintAllowance | Internal 🔒 | | onlyOwner | +| └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyOwner | +| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyOwner | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleSanctionsListOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleSanctionsListOwnable2Step.sol.md index 2ad4f13..e9cf3c0 100644 --- a/doc/surya/surya_report/surya_report_RuleSanctionsListOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleSanctionsListOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleSanctionsListOwnable2Step.sol | 8e57be4539873118b38e561331a5daa212bc68f3 | +| ./rules/validation/deployment/RuleSanctionsListOwnable2Step.sol | 0885c8f3fd637c7131ce04cc5a736b9acb60c616 | ### Contracts Description Table @@ -15,9 +15,10 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleSanctionsListOwnable2Step** | Implementation | RuleSanctionsListBase, Ownable2Step ||| +| **RuleSanctionsListOwnable2Step** | Implementation | RuleSanctionsListBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleSanctionsListBase Ownable | | └ | _authorizeSanctionListManager | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md b/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md index 6c40c5f..84c327d 100644 --- a/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md +++ b/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/abstract/base/RuleSpenderWhitelistBase.sol | 4dcfe0d0236ac5c0a1773ae39504f384433ad4c9 | +| ./rules/validation/abstract/base/RuleSpenderWhitelistBase.sol | 6a5f291ea67c01dc192a26ca6e51e0034aca9621 | ### Contracts Description Table diff --git a/doc/surya/surya_report/surya_report_RuleSpenderWhitelistOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleSpenderWhitelistOwnable2Step.sol.md index 8ab7606..5006e91 100644 --- a/doc/surya/surya_report/surya_report_RuleSpenderWhitelistOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleSpenderWhitelistOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol | 3a1a7f3c6cc386f57c7479adf4c1b5d13854a37e | +| ./rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol | f45ba40d563a6f073f666cd1cd3866efab959081 | ### Contracts Description Table @@ -15,10 +15,11 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleSpenderWhitelistOwnable2Step** | Implementation | RuleSpenderWhitelistBase, Ownable2Step ||| +| **RuleSpenderWhitelistOwnable2Step** | Implementation | RuleSpenderWhitelistBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleSpenderWhitelistBase Ownable | | └ | _authorizeAddressListAdd | Internal 🔒 | | onlyOwner | | └ | _authorizeAddressListRemove | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md index 1718595..af2048c 100644 --- a/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md +++ b/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/abstract/base/RuleWhitelistBase.sol | 3dc6bbac3c9d6bbbb012578f4d0ecd1fd790f44c | +| ./rules/validation/abstract/base/RuleWhitelistBase.sol | c03f8b0e8af1c93b60cbde46d512b75eb501c805 | ### Contracts Description Table diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistOwnable2Step.sol.md index 346c5a3..0c142c3 100644 --- a/doc/surya/surya_report/surya_report_RuleWhitelistOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleWhitelistOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleWhitelistOwnable2Step.sol | a8f5ed118679dd2e32abc7e51cd7e9c165879376 | +| ./rules/validation/deployment/RuleWhitelistOwnable2Step.sol | 6c451cec10719b78b486fff1254daaf78d3c3a25 | ### Contracts Description Table @@ -15,11 +15,12 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleWhitelistOwnable2Step** | Implementation | RuleWhitelistBase, Ownable2Step ||| +| **RuleWhitelistOwnable2Step** | Implementation | RuleWhitelistBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleWhitelistBase Ownable | | └ | _authorizeAddressListAdd | Internal 🔒 | | onlyOwner | | └ | _authorizeAddressListRemove | Internal 🔒 | | onlyOwner | | └ | _authorizeCheckSpenderManager | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistWrapper.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistWrapper.sol.md index 92397aa..f69c876 100644 --- a/doc/surya/surya_report/surya_report_RuleWhitelistWrapper.sol.md +++ b/doc/surya/surya_report/surya_report_RuleWhitelistWrapper.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleWhitelistWrapper.sol | 3294eb2b561b5055686ff1eb82b1f88494d1d9c6 | +| ./rules/validation/deployment/RuleWhitelistWrapper.sol | a2aa47367533733b60d7aca73921f7e3546424b7 | ### Contracts Description Table @@ -15,12 +15,13 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleWhitelistWrapper** | Implementation | RuleWhitelistWrapperBase, AccessControlModuleStandalone ||| +| **RuleWhitelistWrapper** | Implementation | RuleWhitelistWrapperBase, AccessControlModuleStandalone, RulesManagementModuleRolesStorage ||| | └ | | Public ❗️ | 🛑 | RuleWhitelistWrapperBase AccessControlModuleStandalone | | └ | hasRole | Public ❗️ | |NO❗️ | | └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _authorizeCheckSpenderManager | Internal 🔒 | 🛑 | onlyRole | | └ | _onlyRulesManager | Internal 🔒 | 🛑 | onlyRole | +| └ | _onlyRulesLimitManager | Internal 🔒 | 🛑 | onlyRole | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md index 948fa65..ddbc88a 100644 --- a/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md +++ b/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/abstract/base/RuleWhitelistWrapperBase.sol | ae2d97261413a3a126dde152a60bef8627ad1299 | +| ./rules/validation/abstract/base/RuleWhitelistWrapperBase.sol | f345ef26fdc628576e5f4497642bd782d5f6d709 | ### Contracts Description Table diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistWrapperOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistWrapperOwnable2Step.sol.md index 352de2c..dbd4144 100644 --- a/doc/surya/surya_report/surya_report_RuleWhitelistWrapperOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleWhitelistWrapperOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol | 5ec7ee305486a7edfdc2dc045d717fc8ecb2b577 | +| ./rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol | daca724ea3684221c6205d7c4784c48e5ed73074 | ### Contracts Description Table @@ -15,10 +15,12 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleWhitelistWrapperOwnable2Step** | Implementation | RuleWhitelistWrapperBase, Ownable2Step ||| +| **RuleWhitelistWrapperOwnable2Step** | Implementation | RuleWhitelistWrapperBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleWhitelistWrapperBase Ownable | | └ | _authorizeCheckSpenderManager | Internal 🔒 | | onlyOwner | | └ | _onlyRulesManager | Internal 🔒 | | onlyOwner | +| └ | _onlyRulesLimitManager | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_VersionModule.sol.md b/doc/surya/surya_report/surya_report_VersionModule.sol.md index e3a4204..0cf6879 100644 --- a/doc/surya/surya_report/surya_report_VersionModule.sol.md +++ b/doc/surya/surya_report/surya_report_VersionModule.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/VersionModule.sol | 419acdca74542295a72abbf1fd6c29fdfbfe4f70 | +| ./modules/VersionModule.sol | 3351ae4920df4e78c4551e06c79154d8f8b70814 | ### Contracts Description Table diff --git a/doc/technical/RuleBlacklist.md b/doc/technical/RuleBlacklist.md index bace50e..da320e1 100644 --- a/doc/technical/RuleBlacklist.md +++ b/doc/technical/RuleBlacklist.md @@ -16,6 +16,14 @@ A significant portion of the address-list management code is shared with the whi ![surya_inheritance_RuleBlacklist](../surya/surya_inheritance/surya_inheritance_RuleBlacklist.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer. + +![RuleBlacklist flow with a CMTAT token](../img/rule-blacklist-flow.png) + +_Diagram source: doc/img/rule-blacklist-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleConditionalTransfer.md b/doc/technical/RuleConditionalTransfer.md index fcea617..c692247 100644 --- a/doc/technical/RuleConditionalTransfer.md +++ b/doc/technical/RuleConditionalTransfer.md @@ -69,11 +69,19 @@ To perform the transfer, the token holder has to `approve` the rule to spend tok ### Graph -![surya_graph_Blacklist](../surya/surya_graph/surya_graph_RuleConditionalTransfer.sol.png) +![surya_graph_RuleConditionalTransferLight](../surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png) ### Inheritance -![surya_inheritance_RuleWhitelistWrapper.sol](../surya/surya_inheritance/surya_inheritance_RuleConditionalTransfer.sol.png) +![surya_inheritance_RuleConditionalTransferLight](../surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png) + +### Flow with a CMTAT token + +The sequence below shows the full *Vinkulierung* workflow: a holder requests a transfer, an operator approves it, and the CMTAT token (with this rule configured in its RuleEngine) validates the approved request and marks it executed. The optional `AUTOMATIC_TRANSFER` path where the rule performs the transfer on approval is also shown. + +![RuleConditionalTransfer flow with a CMTAT token](../img/rule-conditional-transfer-flow.png) + +_Diagram source: doc/img/rule-conditional-transfer-flow.puml._ ### Workflow @@ -167,7 +175,7 @@ The default admin is the address put in argument(`admin`) inside the constructor ### Graph -![surya_graph_Whitelist](../surya/surya_graph/surya_graph_RuleConditionalTransfer.sol.png) +![surya_graph_RuleConditionalTransferLight](../surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png) diff --git a/doc/technical/RuleConditionalTransferLight.md b/doc/technical/RuleConditionalTransferLight.md index 2c338b2..a9d5411 100644 --- a/doc/technical/RuleConditionalTransferLight.md +++ b/doc/technical/RuleConditionalTransferLight.md @@ -18,6 +18,14 @@ Mints (`from == address(0)`) and burns (`to == address(0)`) are **exempt**: they ![surya_inheritance_RuleConditionalTransferLight](../surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png) +### Flow with a CMTAT token + +The sequence below shows the two-phase flow: an operator first approves a `(from, to, value)` transfer, then the CMTAT token (with this rule configured in its RuleEngine) validates and consumes that approval during the transfer. + +![RuleConditionalTransferLight flow with a CMTAT token](../img/rule-conditional-transfer-light-flow.png) + +_Diagram source: doc/img/rule-conditional-transfer-light-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleConditionalTransferLightMultiToken.md b/doc/technical/RuleConditionalTransferLightMultiToken.md new file mode 100644 index 0000000..3cd15f4 --- /dev/null +++ b/doc/technical/RuleConditionalTransferLightMultiToken.md @@ -0,0 +1,71 @@ +# Rule Conditional Transfer Light MultiToken + +[TOC] + +`RuleConditionalTransferLightMultiToken` is an operation rule that requires explicit operator approval before each transfer, with approvals scoped per token. + +Approval key: + +- `keccak256(token, from, to, value)` + +This prevents approval reuse across tokens when the rule receives token-specific caller context (for example, direct token callbacks to the rule). + +## Schema + +### Graph + +![surya_graph_RuleConditionalTransferLightMultiToken](../surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png) + +### Inheritance + +![surya_inheritance_RuleConditionalTransferLightMultiToken](../surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiToken.sol.png) + +### Flow with a CMTAT token + +The sequence below shows the two-phase flow with token-scoped approvals: an operator approves a `(token, from, to, value)` transfer, then the CMTAT token (with this rule configured in its RuleEngine) validates and consumes that approval. Approvals of one token cannot be spent by another. + +![RuleConditionalTransferLightMultiToken flow with a CMTAT token](../img/rule-conditional-transfer-light-multitoken-flow.png) + +_Diagram source: doc/img/rule-conditional-transfer-light-multitoken-flow.puml._ + +## Restriction codes + +| Constant | Code | Meaning | +| --- | --- | --- | +| `CODE_TRANSFER_REQUEST_NOT_APPROVED` | 46 | No approval exists for this `(token, from, to, value)` tuple | + +## Access control + +| Role | Description | +| --- | --- | +| `DEFAULT_ADMIN_ROLE` | Manages all roles (AccessControl variant) | +| `OPERATOR_ROLE` | Approve/cancel approvals and call `approveAndTransferIfAllowed` | +| `COMPLIANCE_MANAGER_ROLE` | Bind/unbind token contracts | + +## Methods + +### `approveTransfer(address token, address from, address to, uint256 value)` + +Approves one transfer for a specific token key. + +### `cancelTransferApproval(address token, address from, address to, uint256 value)` + +Removes one approval for a specific token key. Reverts if none exists. + +### `approvedCount(address token, address from, address to, uint256 value) -> uint256` + +Returns the remaining count for a specific token key. + +### `approveAndTransferIfAllowed(address token, address from, address to, uint256 value) -> bool` + +Approves and executes `safeTransferFrom` on the specified token, requiring allowance for this rule as spender. + +### `transferred(...)` + +Only bound tokens can call transfer execution hooks. Approval consumption uses the caller token (`msg.sender`) as the token key. + +## Notes + +- Mints and burns are exempt from approval consumption (`from == address(0)` or `to == address(0)`). +- This rule is ERC-20 operation-focused, like `RuleConditionalTransferLight`. +- In a shared `RuleEngine` topology, rule calls are made by the `RuleEngine` address, so `msg.sender` is the engine (not the token). In that case, token-scoped approval keys are not observable through current ERC-3643 / RuleEngine function signatures. diff --git a/doc/technical/RuleERC2980.md b/doc/technical/RuleERC2980.md index 4bf1ffb..2888210 100644 --- a/doc/technical/RuleERC2980.md +++ b/doc/technical/RuleERC2980.md @@ -18,6 +18,14 @@ This rule implements the [ERC-2980](https://eips.ethereum.org/EIPS/eip-2980) Swi ![surya_inheritance_RuleERC2980](../surya/surya_inheritance/surya_inheritance_RuleERC2980.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer. The frozenlist is evaluated before the recipient whitelist. + +![RuleERC2980 flow with a CMTAT token](../img/rule-erc2980-flow.png) + +_Diagram source: doc/img/rule-erc2980-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleIdentityRegistry.md b/doc/technical/RuleIdentityRegistry.md index 1dd07af..b324aa0 100644 --- a/doc/technical/RuleIdentityRegistry.md +++ b/doc/technical/RuleIdentityRegistry.md @@ -27,6 +27,14 @@ If no identity registry is configured (`address(0)`), all transfers pass this ru ![surya_inheritance_RuleIdentityRegistry](../surya/surya_inheritance/surya_inheritance_RuleIdentityRegistry.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer, including the ERC-3643 identity registry `isVerified` lookups and the no-registry pass-through case. + +![RuleIdentityRegistry flow with a CMTAT token](../img/rule-identity-registry-flow.png) + +_Diagram source: doc/img/rule-identity-registry-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleMaxTotalSupply.md b/doc/technical/RuleMaxTotalSupply.md index 21220e7..0cc5603 100644 --- a/doc/technical/RuleMaxTotalSupply.md +++ b/doc/technical/RuleMaxTotalSupply.md @@ -28,6 +28,14 @@ Both the cap and the token contract address can be updated by the admin after de ![surya_inheritance_RuleMaxTotalSupply](../surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupply.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a mint. Only mints (`from == address(0)`) are gated; transfers and burns pass. + +![RuleMaxTotalSupply flow with a CMTAT token](../img/rule-max-total-supply-flow.png) + +_Diagram source: doc/img/rule-max-total-supply-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleMintAllowance.md b/doc/technical/RuleMintAllowance.md new file mode 100644 index 0000000..8dab379 --- /dev/null +++ b/doc/technical/RuleMintAllowance.md @@ -0,0 +1,104 @@ +# Rule Mint Allowance + +[TOC] + +This rule enforces a per-minter mint quota. An operator assigns each minter address a maximum number of tokens it is allowed to mint. Every successful mint reduces the minter's remaining allowance. The operator can adjust the allowance at any time by setting an absolute value or by incrementing/decrementing the current one. + +It is an **operation rule**: it modifies state during the transfer call (deducting from the minter's allowance), unlike validation rules which are read-only. + +Regular transfers and burns are **not restricted** by this rule — it only acts on minting operations (`from == address(0)`). + +`detectTransferRestriction(from, to, value)` (the 3-arg form without a spender) always returns `TRANSFER_OK` because the minter's identity is not available in that call path. Use `detectTransferRestrictionFrom(minter, address(0), to, amount)` to query a minter's allowance. + +> Compatibility warning: this rule does **not** enforce mint allowances for a token that only calls the standard ERC-3643 3-arg compliance functions. It requires the spender-aware RuleEngine/CMTAT v3.3+ path (`detectTransferRestrictionFrom` and `transferred(spender, from, to, value)`) so the minter address is available. + +For that reason, `RuleMintAllowance` does not advertise the full ERC-3643 `ICompliance` interface through ERC-165. It still implements the inherited 3-arg callbacks required by the rule interface, but they are not sufficient to enforce mint quotas. + +## Schema + +### Graph + +![surya_graph_RuleMintAllowance](../surya/surya_graph/surya_graph_RuleMintAllowance.sol.png) + +### Inheritance + +![surya_inheritance_RuleMintAllowance](../surya/surya_inheritance/surya_inheritance_RuleMintAllowance.sol.png) + +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a mint. As an operation rule, it decrements the minter's allowance in the `transferred` callback after balances are updated. + +![RuleMintAllowance flow with a CMTAT token](../img/rule-mint-allowance-flow.png) + +_Diagram source: doc/img/rule-mint-allowance-flow.puml._ + +## Restriction codes + +| Constant | Code | Meaning | +| --- | --- | --- | +| `CODE_MINTER_ALLOWANCE_EXCEEDED` | 70 | Minter's remaining allowance is less than the requested mint amount | + +## Access Control + +| Role | Description | +| --- | --- | +| `DEFAULT_ADMIN_ROLE` | Manages all roles; implicitly holds all roles below | +| `ALLOWANCE_OPERATOR_ROLE` | May set, increase, and decrease per-minter allowances | +| `COMPLIANCE_MANAGER_ROLE` | May bind and unbind the rule to a RuleEngine (`bindToken`, `unbindToken`) | + +The state-modifying `transferred()` functions are restricted to the bound entity only. In the standard CMTAT + RuleEngine deployment, bind the rule to the **RuleEngine address** (not the token address), because the RuleEngine is the direct caller of the rule's `transferred()`. The rule targets exactly one bound entity at a time; attempting to bind a second RuleEngine/token reverts with `RuleMintAllowance_TokenAlreadyBound`. To migrate, call `unbindToken` first. + +## Methods + +### `setMintAllowance(address minter, uint256 amount)` + +Sets the `minter`'s allowance to an absolute `amount`, overwriting any previous value. Restricted to `ALLOWANCE_OPERATOR_ROLE`. Emits `MintAllowanceSet`. + +### `increaseMintAllowance(address minter, uint256 amount)` + +Adds `amount` to `minter`'s current allowance. Restricted to `ALLOWANCE_OPERATOR_ROLE`. Emits `MintAllowanceIncreased`. + +### `decreaseMintAllowance(address minter, uint256 amount)` + +Subtracts `amount` from `minter`'s current allowance. Reverts with `RuleMintAllowance_DecreaseBelowZero` if `amount` exceeds the current allowance. Restricted to `ALLOWANCE_OPERATOR_ROLE`. Emits `MintAllowanceDecreased`. + +### `mintAllowance(address minter) → uint256` + +Returns the remaining mint allowance for `minter`. Default is `0`. + +### `bindToken(address token)` / `unbindToken(address token)` + +Binds or unbinds the caller address. Only the bound address is authorised to call `transferred`. In practice, bind the RuleEngine address. Restricted to `COMPLIANCE_MANAGER_ROLE`. A second `bindToken` call reverts until the current binding is removed. + +## Workflow + +1. Deploy `RuleMintAllowance` (or `RuleMintAllowanceOwnable2Step`). +2. Add the rule to the RuleEngine with `ruleEngine.addRule(ruleMintAllowance)`. +3. Bind the rule to the RuleEngine: `ruleMintAllowance.bindToken(address(ruleEngine))`. +4. Set the CMTAT's RuleEngine: `cmtat.setRuleEngine(ruleEngine)`. +5. For each minter, call `setMintAllowance(minterAddress, quota)`. +6. Minters can now mint tokens up to their assigned quota. + +## Allowance deduction + +When CMTAT v3.3+ calls `ruleEngine.transferred(minter, address(0), recipient, amount)`, the rule receives `transferred(minter, address(0), recipient, amount)` and deducts `amount` from `mintAllowance[minter]`. If `amount > mintAllowance[minter]`, the call reverts with `RuleMintAllowance_AllowanceExceeded`. + +## Multiple minters + +Each minter has an independent allowance within the single bound RuleEngine/token. Multiple minters can share a single `RuleMintAllowance` instance, but that instance intentionally targets only one bound caller at a time so allowance state is not shared across multiple RuleEngines/tokens. + +## Notes + +### 3-arg `transferred` path + +When CMTAT calls `ruleEngine.transferred(from, to, value)` without a spender (CMTAT v3.2 and earlier, a pure ERC-3643 token, or when spender is `address(0)`), the rule receives the 3-arg call and performs **no deduction**. The minter's quota is only consumed via the 4-arg path where the minter's address is passed as `spender`. + +Because of this, `RuleMintAllowance` must not be used as a standalone compliance contract for a pure ERC-3643 token. It is intended for the CMTAT/RuleEngine spender-aware integration path. + +### Burns not restricted + +Burns (`to == address(0)`) are not tracked by this rule. Minters do not recover allowance when tokens are burned. + +## Usage scenario + +An issuer deploys `RuleMintAllowance` and grants `ALLOWANCE_OPERATOR_ROLE` to a compliance officer. The officer assigns `setMintAllowance(alice, 100_000e18)` — Alice may mint up to 100 000 tokens. Each `cmtat.mint(recipient, amount)` call by Alice reduces her quota. Once exhausted, further mints by Alice revert. The officer can call `increaseMintAllowance(alice, 50_000e18)` to extend Alice's quota or `setMintAllowance(alice, 0)` to revoke it entirely. diff --git a/doc/technical/RuleSanctionList.md b/doc/technical/RuleSanctionList.md index 27c49bc..20f7995 100644 --- a/doc/technical/RuleSanctionList.md +++ b/doc/technical/RuleSanctionList.md @@ -22,6 +22,14 @@ The oracle can be updated with `setSanctionListOracle` or disabled with `clearSa ![surya_inheritance_RuleSanctionsList](../surya/surya_inheritance/surya_inheritance_RuleSanctionsList.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer, including the Chainalysis oracle lookup and the no-oracle pass-through case. + +![RuleSanctionsList flow with a CMTAT token](../img/rule-sanctionslist-flow.png) + +_Diagram source: doc/img/rule-sanctionslist-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleSpenderWhitelist.md b/doc/technical/RuleSpenderWhitelist.md index bf3281f..045969d 100644 --- a/doc/technical/RuleSpenderWhitelist.md +++ b/doc/technical/RuleSpenderWhitelist.md @@ -23,6 +23,14 @@ This rule restricts only spender-initiated transfers (`transferFrom`): the spend ![surya_inheritance_RuleSpenderWhitelist](../surya/surya_inheritance/surya_inheritance_RuleSpenderWhitelist.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer. Direct `transfer` calls always pass; only `transferFrom` spenders are gated. + +![RuleSpenderWhitelist flow with a CMTAT token](../img/rule-spender-whitelist-flow.png) + +_Diagram source: doc/img/rule-spender-whitelist-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleWhitelist.md b/doc/technical/RuleWhitelist.md index 7f0bfd4..de35943 100644 --- a/doc/technical/RuleWhitelist.md +++ b/doc/technical/RuleWhitelist.md @@ -29,6 +29,14 @@ When `checkSpender` is `true`, the spender in a `transferFrom` call must also be ![surya_inheritance_RuleWhitelist](../surya/surya_inheritance/surya_inheritance_RuleWhitelist.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer. + +![RuleWhitelist flow with a CMTAT token](../img/rule-whitelist-flow.png) + +_Diagram source: doc/img/rule-whitelist-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleWhitelistWrapper.md b/doc/technical/RuleWhitelistWrapper.md index dc02b31..a7bfa2a 100644 --- a/doc/technical/RuleWhitelistWrapper.md +++ b/doc/technical/RuleWhitelistWrapper.md @@ -20,6 +20,14 @@ Each child rule must implement `IAddressList`. The wrapper iterates through all ![surya_inheritance_RuleWhitelistWrapper](../surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapper.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the wrapper aggregates its child whitelist rules when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer. + +![RuleWhitelistWrapper flow with a CMTAT token](../img/rule-whitelist-wrapper-flow.png) + +_Diagram source: doc/img/rule-whitelist-wrapper-flow.puml._ + ## Configuration ### Constructor parameters diff --git a/lib/CMTAT b/lib/CMTAT index 49544f4..580d477 160000 --- a/lib/CMTAT +++ b/lib/CMTAT @@ -1 +1 @@ -Subproject commit 49544f4de1993008acfc9e848d0bf03bd31d8579 +Subproject commit 580d4776e4cbb857b2da7d83fd79144ae7e47557 diff --git a/lib/RuleEngine b/lib/RuleEngine index ec4a24a..66fcf2a 160000 --- a/lib/RuleEngine +++ b/lib/RuleEngine @@ -1 +1 @@ -Subproject commit ec4a24a96ca30e2ef8f79a06e49846a431e9b4b1 +Subproject commit 66fcf2aafebd1f9d9de8a81dec92b88da071c9b3 diff --git a/script/DeployCMTATWithBlacklist.s.sol b/script/DeployCMTATWithBlacklist.s.sol index 67b819a..5d446bc 100644 --- a/script/DeployCMTATWithBlacklist.s.sol +++ b/script/DeployCMTATWithBlacklist.s.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.20; import {Script} from "forge-std/Script.sol"; -import {ICMTATConstructor, CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {ICMTATConstructor, CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; contract DeployCMTATWithBlacklist is Script { - function deploy(address admin, address forwarder) public returns (CMTATStandalone token, RuleBlacklist rule) { + function deploy(address admin, address forwarder) public returns (CMTATStandardStandalone token, RuleBlacklist rule) { ICMTATConstructor.ERC20Attributes memory erc20Attributes = ICMTATConstructor.ERC20Attributes("CMTA Token", "CMTAT", 0); ICMTATConstructor.ExtraInformationAttributes memory extraInformationAttributes = @@ -21,7 +21,7 @@ contract DeployCMTATWithBlacklist is Script { ); ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(address(0))); - token = new CMTATStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); + token = new CMTATStandardStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); rule = new RuleBlacklist(admin, address(0)); token.setRuleEngine(IRuleEngine(address(rule))); @@ -32,7 +32,7 @@ contract DeployCMTATWithBlacklist is Script { } } - function run() external returns (CMTATStandalone token, RuleBlacklist rule) { + function run() external returns (CMTATStandardStandalone token, RuleBlacklist rule) { vm.startBroadcast(); (token, rule) = deploy(msg.sender, address(0)); vm.stopBroadcast(); diff --git a/script/DeployCMTATWithBlacklistAndSanctionsList.s.sol b/script/DeployCMTATWithBlacklistAndSanctionsList.s.sol index 6579cf6..3515894 100644 --- a/script/DeployCMTATWithBlacklistAndSanctionsList.s.sol +++ b/script/DeployCMTATWithBlacklistAndSanctionsList.s.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import {Script} from "forge-std/Script.sol"; -import {ICMTATConstructor, CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {ICMTATConstructor, CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; @@ -16,7 +16,7 @@ import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; * a blacklist (RuleBlacklist) and a sanctions screening (RuleSanctionsList). * * Deployment order: - * 1. CMTATStandalone — token contract (deployer as temporary admin) + * 1. CMTATStandardStandalone — token contract (deployer as temporary admin) * 2. RuleBlacklist — blocks blacklisted senders / recipients * 3. RuleSanctionsList — blocks sanctioned addresses via Chainalysis oracle * 4. RuleEngine — aggregates both rules; token bound at construction @@ -27,7 +27,7 @@ contract DeployCMTATWithBlacklistAndSanctionsList is Script { function deploy(address admin, address forwarder, ISanctionsList sanctionsOracle) public returns ( - CMTATStandalone token, + CMTATStandardStandalone token, RuleEngine ruleEngine, RuleBlacklist ruleBlacklist, RuleSanctionsList ruleSanctionsList @@ -46,7 +46,7 @@ contract DeployCMTATWithBlacklistAndSanctionsList is Script { ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(address(0))); // Deploy CMTAT with the deployer as temporary admin so we can configure it. - token = new CMTATStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); + token = new CMTATStandardStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); // Deploy rules; each rule is owned directly by the intended admin. ruleBlacklist = new RuleBlacklist(admin, address(0)); @@ -75,7 +75,7 @@ contract DeployCMTATWithBlacklistAndSanctionsList is Script { function run() external returns ( - CMTATStandalone token, + CMTATStandardStandalone token, RuleEngine ruleEngine, RuleBlacklist ruleBlacklist, RuleSanctionsList ruleSanctionsList diff --git a/script/DeployCMTATWithWhitelist.s.sol b/script/DeployCMTATWithWhitelist.s.sol index e01f4fd..63fb6cc 100644 --- a/script/DeployCMTATWithWhitelist.s.sol +++ b/script/DeployCMTATWithWhitelist.s.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import {Script} from "forge-std/Script.sol"; -import {ICMTATConstructor, CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {ICMTATConstructor, CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; @@ -10,7 +10,7 @@ import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; contract DeployCMTATWithWhitelist is Script { function deploy(address admin, address forwarder, bool checkSpender) public - returns (CMTATStandalone token, RuleWhitelist rule) + returns (CMTATStandardStandalone token, RuleWhitelist rule) { ICMTATConstructor.ERC20Attributes memory erc20Attributes = ICMTATConstructor.ERC20Attributes("CMTA Token", "CMTAT", 0); @@ -24,7 +24,7 @@ contract DeployCMTATWithWhitelist is Script { ); ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(address(0))); - token = new CMTATStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); + token = new CMTATStandardStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); rule = new RuleWhitelist(admin, address(0), checkSpender, false); token.setRuleEngine(IRuleEngine(address(rule))); @@ -35,7 +35,7 @@ contract DeployCMTATWithWhitelist is Script { } } - function run() external returns (CMTATStandalone token, RuleWhitelist rule) { + function run() external returns (CMTATStandardStandalone token, RuleWhitelist rule) { vm.startBroadcast(); (token, rule) = deploy(msg.sender, address(0), false); vm.stopBroadcast(); diff --git a/src/mocks/IERC3643ComplianceFull.sol b/src/mocks/IERC3643ComplianceFull.sol index 44a1be7..77f43e4 100644 --- a/src/mocks/IERC3643ComplianceFull.sol +++ b/src/mocks/IERC3643ComplianceFull.sol @@ -21,15 +21,56 @@ pragma solidity ^0.8.20; * Computed value: `type(IERC3643ComplianceFull).interfaceId == 0x3144991c` */ interface IERC3643ComplianceFull { - // From IERC3643ComplianceRead - function canTransfer(address from, address to, uint256 value) external view returns (bool isValid); // From IERC3643IComplianceContract + /** + * @notice Notifies the compliance contract that a transfer has occurred. + * @param from The address tokens were transferred from. + * @param to The address tokens were transferred to. + * @param value The amount transferred. + */ function transferred(address from, address to, uint256 value) external; // From IERC3643Compliance (directly defined) + /** + * @notice Binds a token to the compliance contract. + * @param token The token address to bind. + */ function bindToken(address token) external; + /** + * @notice Unbinds a token from the compliance contract. + * @param token The token address to unbind. + */ function unbindToken(address token) external; - function isTokenBound(address token) external view returns (bool isBound); - function getTokenBound() external view returns (address token); + /** + * @notice Notifies the compliance contract that tokens have been created (minted). + * @param to The address that received the newly created tokens. + * @param value The amount created. + */ function created(address to, uint256 value) external; + /** + * @notice Notifies the compliance contract that tokens have been destroyed (burned). + * @param from The address whose tokens were destroyed. + * @param value The amount destroyed. + */ function destroyed(address from, uint256 value) external; + + // From IERC3643ComplianceRead + /** + * @notice Checks whether a transfer is compliant. + * @param from The address tokens would be transferred from. + * @param to The address tokens would be transferred to. + * @param value The amount that would be transferred. + * @return isValid True if the transfer is compliant. + */ + function canTransfer(address from, address to, uint256 value) external view returns (bool isValid); + /** + * @notice Returns whether a token is bound to the compliance contract. + * @param token The token address to query. + * @return isBound True if the token is bound. + */ + function isTokenBound(address token) external view returns (bool isBound); + /** + * @notice Returns the token currently bound to the compliance contract. + * @return token The bound token address. + */ + function getTokenBound() external view returns (address token); } diff --git a/src/mocks/IdentityRegistryMock.sol b/src/mocks/IdentityRegistryMock.sol index 936a26f..dd8ceb1 100644 --- a/src/mocks/IdentityRegistryMock.sol +++ b/src/mocks/IdentityRegistryMock.sol @@ -3,17 +3,34 @@ pragma solidity ^0.8.20; import {IIdentityRegistryVerified} from "src/rules/interfaces/IIdentityRegistry.sol"; +/** + * @title IdentityRegistryMock — test double for an ERC-3643 identity registry + * @notice Stores per-address verification status for use in tests. + */ contract IdentityRegistryMock is IIdentityRegistryVerified { + /** + * @notice Verification status keyed by address. + */ mapping(address => bool) private verified; /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the verification status of an address. + * @param user The address to update. + * @param verified_ The verification status to assign. + */ function setVerified(address user, bool verified_) external { verified[user] = verified_; } + /** + * @notice Returns whether an address is verified. + * @param user The address to query. + * @return True if the address is verified. + */ function isVerified(address user) external view returns (bool) { return verified[user]; } diff --git a/src/mocks/MockERC20TransferFromFalse.sol b/src/mocks/MockERC20TransferFromFalse.sol index 96469fe..5662812 100644 --- a/src/mocks/MockERC20TransferFromFalse.sol +++ b/src/mocks/MockERC20TransferFromFalse.sol @@ -1,21 +1,45 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title MockERC20TransferFromFalse — ERC20-like mock whose transferFrom always fails + * @notice Test double that tracks allowances but returns false from transferFrom, + * simulating a token whose transfer silently fails. + */ contract MockERC20TransferFromFalse { + /** + * @notice Allowance amounts keyed by owner then spender. + */ mapping(address => mapping(address => uint256)) private _allowances; /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the allowance granted by an owner to a spender. + * @param owner The address granting the allowance. + * @param spender The address being allowed to spend. + * @param value The allowance amount to set. + */ function setAllowance(address owner, address spender, uint256 value) external { _allowances[owner][spender] = value; } + /** + * @notice Returns the allowance granted by an owner to a spender. + * @param owner The address that granted the allowance. + * @param spender The address allowed to spend. + * @return The current allowance amount. + */ function allowance(address owner, address spender) external view returns (uint256) { return _allowances[owner][spender]; } + /** + * @notice Mock transferFrom that always fails by returning false. + * @return Always false. + */ function transferFrom(address, address, uint256) external pure returns (bool) { return false; } diff --git a/src/mocks/MockERC20WithTransferContext.sol b/src/mocks/MockERC20WithTransferContext.sol index 72ee311..ab1e124 100644 --- a/src/mocks/MockERC20WithTransferContext.sol +++ b/src/mocks/MockERC20WithTransferContext.sol @@ -4,27 +4,57 @@ pragma solidity ^0.8.20; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ITransferContext} from "src/rules/interfaces/ITransferContext.sol"; +/** + * @title MockERC20WithTransferContext — ERC20 mock that notifies a transfer-context rule + * @notice Test double that forwards fungible/multi-token transfer context to a + * configured ITransferContext rule after each transfer. + */ contract MockERC20WithTransferContext is ERC20 { + /** + * @notice The transfer-context rule notified on each transfer. + */ ITransferContext public rule; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the mock ERC20 token. + * @param name_ The token name. + * @param symbol_ The token symbol. + */ constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the transfer-context rule to notify. + * @param rule_ The rule address (set to zero to disable notifications). + */ function setRule(address rule_) external { rule = ITransferContext(rule_); } + /** + * @notice Mints tokens to an address. + * @param to The recipient of the minted tokens. + * @param value The amount to mint. + */ function mint(address to, uint256 value) external { _mint(to, value); } + /** + * @notice Transfers tokens from the caller and notifies the rule with the chosen context. + * @param to The recipient of the transfer. + * @param value The amount to transfer. + * @param useFungibleContext If true, notify with a fungible context; otherwise a multi-token context. + * @param tokenId The token id used when notifying with a multi-token context. + * @return Always true on success. + */ function transferWithContext(address to, uint256 value, bool useFungibleContext, uint256 tokenId) external returns (bool) @@ -38,6 +68,15 @@ contract MockERC20WithTransferContext is ERC20 { return true; } + /** + * @notice Transfers tokens via allowance and notifies the rule with the chosen context. + * @param from The address tokens are transferred from. + * @param to The recipient of the transfer. + * @param value The amount to transfer. + * @param useFungibleContext If true, notify with a fungible context; otherwise a multi-token context. + * @param tokenId The token id used when notifying with a multi-token context. + * @return Always true on success. + */ function transferFromWithContext(address from, address to, uint256 value, bool useFungibleContext, uint256 tokenId) external returns (bool) @@ -58,12 +97,18 @@ contract MockERC20WithTransferContext is ERC20 { PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc ERC20 + */ function transfer(address to, uint256 value) public virtual override returns (bool) { bool success = super.transfer(to, value); _notifyFungible(_msgSender(), _msgSender(), to, value); return success; } + /** + * @inheritdoc ERC20 + */ function transferFrom(address from, address to, uint256 value) public virtual override returns (bool) { address sender = _msgSender(); bool success = super.transferFrom(from, to, value); @@ -75,6 +120,14 @@ contract MockERC20WithTransferContext is ERC20 { INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Builds a fungible transfer context and forwards it to the configured rule. + * @dev No-op when no rule is set. + * @param sender The message sender that initiated the transfer. + * @param from The address tokens were transferred from. + * @param to The address tokens were transferred to. + * @param value The amount transferred. + */ function _notifyFungible(address sender, address from, address to, uint256 value) internal { if (address(rule) == address(0)) { return; @@ -91,6 +144,15 @@ contract MockERC20WithTransferContext is ERC20 { rule.transferred(ctx); } + /** + * @notice Builds a multi-token transfer context and forwards it to the configured rule. + * @dev No-op when no rule is set. + * @param sender The message sender that initiated the transfer. + * @param from The address tokens were transferred from. + * @param to The address tokens were transferred to. + * @param value The amount transferred. + * @param tokenId The token id associated with the transfer. + */ function _notifyMultiToken(address sender, address from, address to, uint256 value, uint256 tokenId) internal { if (address(rule) == address(0)) { return; diff --git a/src/mocks/MockERC721WithTransferContext.sol b/src/mocks/MockERC721WithTransferContext.sol index c6f9879..920315e 100644 --- a/src/mocks/MockERC721WithTransferContext.sol +++ b/src/mocks/MockERC721WithTransferContext.sol @@ -4,23 +4,45 @@ pragma solidity ^0.8.20; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {ITransferContext} from "src/rules/interfaces/ITransferContext.sol"; +/** + * @title MockERC721WithTransferContext — ERC721 mock that notifies a transfer-context rule + * @notice Test double that forwards a multi-token transfer context to a configured + * ITransferContext rule after each transferFrom. + */ contract MockERC721WithTransferContext is ERC721 { + /** + * @notice The transfer-context rule notified on each transfer. + */ ITransferContext public rule; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the mock ERC721 token. + * @param name_ The token name. + * @param symbol_ The token symbol. + */ constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the transfer-context rule to notify. + * @param rule_ The rule address (set to zero to disable notifications). + */ function setRule(address rule_) external { rule = ITransferContext(rule_); } + /** + * @notice Mints a token to an address. + * @param to The recipient of the minted token. + * @param tokenId The id of the token to mint. + */ function mint(address to, uint256 tokenId) external { _mint(to, tokenId); } @@ -29,6 +51,9 @@ contract MockERC721WithTransferContext is ERC721 { PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc ERC721 + */ function transferFrom(address from, address to, uint256 tokenId) public virtual override { address sender = _msgSender(); super.transferFrom(from, to, tokenId); @@ -39,6 +64,14 @@ contract MockERC721WithTransferContext is ERC721 { INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Builds a multi-token transfer context and forwards it to the configured rule. + * @dev No-op when no rule is set; value is fixed to 1 for a single NFT. + * @param sender The message sender that initiated the transfer. + * @param from The address the token was transferred from. + * @param to The address the token was transferred to. + * @param tokenId The id of the transferred token. + */ function _notifyRule(address sender, address from, address to, uint256 tokenId) internal { if (address(rule) == address(0)) { return; diff --git a/src/mocks/SanctionListOracle.sol b/src/mocks/SanctionListOracle.sol index 41d586d..0973beb 100644 --- a/src/mocks/SanctionListOracle.sol +++ b/src/mocks/SanctionListOracle.sol @@ -3,21 +3,41 @@ pragma solidity ^0.8.20; import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; +/** + * @title SanctionListOracle — test double for a Chainalysis-style sanctions oracle + * @notice Stores per-address sanctioned status for use in tests. + */ contract SanctionListOracle is ISanctionsList { + /** + * @notice Sanctioned status keyed by address. + */ mapping(address => bool) private sanctionedAddresses; /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Marks an address as sanctioned. + * @param newSanction The address to sanction. + */ function addToSanctionsList(address newSanction) public { sanctionedAddresses[newSanction] = true; } + /** + * @notice Removes an address from the sanctions list. + * @param removeSanction The address to un-sanction. + */ function removeFromSanctionsList(address removeSanction) public { - sanctionedAddresses[removeSanction] = true; + sanctionedAddresses[removeSanction] = false; } + /** + * @notice Returns whether an address is sanctioned. + * @param addr The address to query. + * @return True if the address is sanctioned. + */ function isSanctioned(address addr) public view returns (bool) { return sanctionedAddresses[addr]; } diff --git a/src/mocks/TotalSupplyMock.sol b/src/mocks/TotalSupplyMock.sol index 3b72179..7fd9f84 100644 --- a/src/mocks/TotalSupplyMock.sol +++ b/src/mocks/TotalSupplyMock.sol @@ -1,17 +1,32 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title TotalSupplyMock — test double exposing a settable total supply + * @notice Stores a total supply value that tests can set and read back. + */ contract TotalSupplyMock { + /** + * @notice The stored total supply value. + */ uint256 private _totalSupply; /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the total supply value. + * @param newTotalSupply The new total supply to store. + */ function setTotalSupply(uint256 newTotalSupply) external { _totalSupply = newTotalSupply; } + /** + * @notice Returns the stored total supply. + * @return The current total supply value. + */ function totalSupply() external view returns (uint256) { return _totalSupply; } diff --git a/src/mocks/harness/DeploymentCoverageHarnesses.sol b/src/mocks/harness/DeploymentCoverageHarnesses.sol index 3ebbc24..b16fed4 100644 --- a/src/mocks/harness/DeploymentCoverageHarnesses.sol +++ b/src/mocks/harness/DeploymentCoverageHarnesses.sol @@ -12,27 +12,49 @@ import {RuleWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleWhi import {RuleWhitelistWrapperOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol"; import {RuleERC2980Ownable2Step} from "src/rules/validation/deployment/RuleERC2980Ownable2Step.sol"; +/** + * @title RuleBlacklistHarness — test harness exposing RuleBlacklist internals + */ contract RuleBlacklistHarness is RuleBlacklist { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleBlacklist constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + */ constructor(address admin, address forwarderIrrevocable) RuleBlacklist(admin, forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleWhitelistHarness — test harness exposing RuleWhitelist internals + */ contract RuleWhitelistHarness is RuleWhitelist { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleWhitelist constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param checkSpender_ Whether the spender is also checked against the whitelist + * @param allowMintBurn Whether mint and burn operations bypass the whitelist + */ constructor(address admin, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) RuleWhitelist(admin, forwarderIrrevocable, checkSpender_, allowMintBurn) {} @@ -41,16 +63,29 @@ contract RuleWhitelistHarness is RuleWhitelist { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleWhitelistWrapperHarness — test harness exposing RuleWhitelistWrapper internals + */ contract RuleWhitelistWrapperHarness is RuleWhitelistWrapper { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleWhitelistWrapper constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param checkSpender_ Whether the spender is also checked against the whitelist + */ constructor(address admin, address forwarderIrrevocable, bool checkSpender_) RuleWhitelistWrapper(admin, forwarderIrrevocable, checkSpender_) {} @@ -59,16 +94,29 @@ contract RuleWhitelistWrapperHarness is RuleWhitelistWrapper { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleERC2980Harness — test harness exposing RuleERC2980 internals + */ contract RuleERC2980Harness is RuleERC2980 { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleERC2980 constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param allowBurn Whether burn operations are permitted + */ constructor(address admin, address forwarderIrrevocable, bool allowBurn) RuleERC2980(admin, forwarderIrrevocable, allowBurn) {} @@ -77,16 +125,29 @@ contract RuleERC2980Harness is RuleERC2980 { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleSanctionsListHarness — test harness exposing RuleSanctionsList internals + */ contract RuleSanctionsListHarness is RuleSanctionsList { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleSanctionsList constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param sanctionContractOracle_ Chainalysis sanctions oracle used to screen addresses + */ constructor(address admin, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) RuleSanctionsList(admin, forwarderIrrevocable, sanctionContractOracle_) {} @@ -95,32 +156,58 @@ contract RuleSanctionsListHarness is RuleSanctionsList { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleBlacklistOwnable2StepHarness — test harness exposing RuleBlacklistOwnable2Step internals + */ contract RuleBlacklistOwnable2StepHarness is RuleBlacklistOwnable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleBlacklistOwnable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + */ constructor(address owner, address forwarderIrrevocable) RuleBlacklistOwnable2Step(owner, forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleWhitelistOwnable2StepHarness — test harness exposing RuleWhitelistOwnable2Step internals + */ contract RuleWhitelistOwnable2StepHarness is RuleWhitelistOwnable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleWhitelistOwnable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param checkSpender_ Whether the spender is also checked against the whitelist + * @param allowMintBurn Whether mint and burn operations bypass the whitelist + */ constructor(address owner, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) RuleWhitelistOwnable2Step(owner, forwarderIrrevocable, checkSpender_, allowMintBurn) {} @@ -129,16 +216,29 @@ contract RuleWhitelistOwnable2StepHarness is RuleWhitelistOwnable2Step { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleWhitelistWrapperOwnable2StepHarness — test harness exposing RuleWhitelistWrapperOwnable2Step internals + */ contract RuleWhitelistWrapperOwnable2StepHarness is RuleWhitelistWrapperOwnable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleWhitelistWrapperOwnable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param checkSpender_ Whether the spender is also checked against the whitelist + */ constructor(address owner, address forwarderIrrevocable, bool checkSpender_) RuleWhitelistWrapperOwnable2Step(owner, forwarderIrrevocable, checkSpender_) {} @@ -147,16 +247,29 @@ contract RuleWhitelistWrapperOwnable2StepHarness is RuleWhitelistWrapperOwnable2 EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleERC2980Ownable2StepHarness — test harness exposing RuleERC2980Ownable2Step internals + */ contract RuleERC2980Ownable2StepHarness is RuleERC2980Ownable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleERC2980Ownable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param allowBurn Whether burn operations are permitted + */ constructor(address owner, address forwarderIrrevocable, bool allowBurn) RuleERC2980Ownable2Step(owner, forwarderIrrevocable, allowBurn) {} @@ -165,6 +278,10 @@ contract RuleERC2980Ownable2StepHarness is RuleERC2980Ownable2Step { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } diff --git a/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol b/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol index dedfac7..a32c6ef 100644 --- a/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol +++ b/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol @@ -4,11 +4,20 @@ pragma solidity ^0.8.20; import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; import {RuleSanctionsListOwnable2Step} from "src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol"; +/** + * @title RuleSanctionsListOwnable2StepHarness — test harness exposing RuleSanctionsListOwnable2Step internals + */ contract RuleSanctionsListOwnable2StepHarness is RuleSanctionsListOwnable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleSanctionsListOwnable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param sanctionContractOracle_ Chainalysis sanctions oracle used to screen addresses + */ constructor(address owner, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) RuleSanctionsListOwnable2Step(owner, forwarderIrrevocable, sanctionContractOracle_) {} @@ -17,14 +26,26 @@ contract RuleSanctionsListOwnable2StepHarness is RuleSanctionsListOwnable2Step { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the internal `_msgSender()` resolved sender + * @return Address returned by `_msgSender()` + */ function exposedMsgSender() external view returns (address) { return _msgSender(); } + /** + * @notice Exposes the internal `_msgData()` calldata buffer + * @return Calldata bytes returned by `_msgData()` + */ function exposedMsgData() external view returns (bytes memory) { return _msgData(); } + /** + * @notice Exposes the internal `_contextSuffixLength()` value + * @return Length in bytes of the ERC-2771 context suffix + */ function exposedContextSuffixLength() external view returns (uint256) { return _contextSuffixLength(); } diff --git a/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol b/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol index 9aa9d82..9414e75 100644 --- a/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol +++ b/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol @@ -4,35 +4,63 @@ pragma solidity ^0.8.20; import {RuleSpenderWhitelist} from "src/rules/validation/deployment/RuleSpenderWhitelist.sol"; import {RuleSpenderWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol"; +/** + * @title RuleSpenderWhitelistHarness — test harness exposing RuleSpenderWhitelist internals + */ contract RuleSpenderWhitelistHarness is RuleSpenderWhitelist { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleSpenderWhitelist constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + */ constructor(address admin, address forwarderIrrevocable) RuleSpenderWhitelist(admin, forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the internal `_msgSender()` resolved sender + * @return Address returned by `_msgSender()` + */ function exposedMsgSender() external view returns (address) { return _msgSender(); } + /** + * @notice Exposes the internal `_msgData()` calldata buffer + * @return Calldata bytes returned by `_msgData()` + */ function exposedMsgData() external view returns (bytes memory) { return _msgData(); } + /** + * @notice Exposes the internal `_contextSuffixLength()` value + * @return Length in bytes of the ERC-2771 context suffix + */ function exposedContextSuffixLength() external view returns (uint256) { return _contextSuffixLength(); } } +/** + * @title RuleSpenderWhitelistOwnable2StepHarness — test harness exposing RuleSpenderWhitelistOwnable2Step internals + */ contract RuleSpenderWhitelistOwnable2StepHarness is RuleSpenderWhitelistOwnable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleSpenderWhitelistOwnable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + */ constructor(address owner, address forwarderIrrevocable) RuleSpenderWhitelistOwnable2Step(owner, forwarderIrrevocable) {} @@ -41,14 +69,26 @@ contract RuleSpenderWhitelistOwnable2StepHarness is RuleSpenderWhitelistOwnable2 EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the internal `_msgSender()` resolved sender + * @return Address returned by `_msgSender()` + */ function exposedMsgSender() external view returns (address) { return _msgSender(); } + /** + * @notice Exposes the internal `_msgData()` calldata buffer + * @return Calldata bytes returned by `_msgData()` + */ function exposedMsgData() external view returns (bytes memory) { return _msgData(); } + /** + * @notice Exposes the internal `_contextSuffixLength()` value + * @return Length in bytes of the ERC-2771 context suffix + */ function exposedContextSuffixLength() external view returns (uint256) { return _contextSuffixLength(); } diff --git a/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol b/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol index c02598b..b2f6a70 100644 --- a/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol +++ b/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol @@ -3,11 +3,20 @@ pragma solidity ^0.8.20; import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol"; +/** + * @title RuleWhitelistWrapperHarnessInternal — test harness exposing RuleWhitelistWrapper internal transfer hook + */ contract RuleWhitelistWrapperHarnessInternal is RuleWhitelistWrapper { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleWhitelistWrapper constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param checkSpender_ Whether the spender is also checked against the whitelist + */ constructor(address admin, address forwarderIrrevocable, bool checkSpender_) RuleWhitelistWrapper(admin, forwarderIrrevocable, checkSpender_) {} @@ -16,6 +25,13 @@ contract RuleWhitelistWrapperHarnessInternal is RuleWhitelistWrapper { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the internal `_transferred()` post-transfer hook + * @param spender Address executing the transfer (relevant for `transferFrom`) + * @param from Source address of the transfer + * @param to Destination address of the transfer + * @param value Amount transferred + */ function exposedTransferredSpenderInternal(address spender, address from, address to, uint256 value) external view { _transferred(spender, from, to, value); } diff --git a/src/modules/AccessControlModuleStandalone.sol b/src/modules/AccessControlModuleStandalone.sol index 9c757f4..0fa19db 100644 --- a/src/modules/AccessControlModuleStandalone.sol +++ b/src/modules/AccessControlModuleStandalone.sol @@ -7,6 +7,9 @@ import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +/** + * @title AccessControlModuleStandalone — base RBAC module where the default admin implicitly holds all roles. + */ abstract contract AccessControlModuleStandalone is AccessControlEnumerable { error AccessControlModuleStandalone_AddressZeroNotAllowed(); @@ -37,7 +40,8 @@ abstract contract AccessControlModuleStandalone is AccessControlEnumerable { //////////////////////////////////////////////////////////////*/ /** - * @dev Returns `true` if `account` has been granted `role`. + * @inheritdoc IAccessControl + * @dev The default admin is treated as holding every role. */ function hasRole(bytes32 role, address account) public diff --git a/src/modules/MetaTxModuleStandalone.sol b/src/modules/MetaTxModuleStandalone.sol index 9902688..12ea2f1 100644 --- a/src/modules/MetaTxModuleStandalone.sol +++ b/src/modules/MetaTxModuleStandalone.sol @@ -8,6 +8,10 @@ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol" * @dev Meta transaction (gasless) module. */ abstract contract MetaTxModuleStandalone is ERC2771Context { + /** + * @notice Configures the trusted ERC-2771 forwarder used to recover the meta-transaction sender. + * @param trustedForwarder The address allowed to forward meta-transactions on behalf of users. + */ constructor(address trustedForwarder) ERC2771Context(trustedForwarder) { // Nothing to do } diff --git a/src/modules/Ownable2StepERC165Module.sol b/src/modules/Ownable2StepERC165Module.sol new file mode 100644 index 0000000..0c6dcc0 --- /dev/null +++ b/src/modules/Ownable2StepERC165Module.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {OwnableInterfaceId} from "RuleEngine/modules/library/OwnableInterfaceId.sol"; +import {Ownable2StepInterfaceId} from "RuleEngine/modules/library/Ownable2StepInterfaceId.sol"; + +/** + * @title Ownable2StepERC165Module + * @notice Shared ERC-165 advertisement for Ownable2Step deployments. + */ +abstract contract Ownable2StepERC165Module is ERC165 { + /** + * @inheritdoc ERC165 + * @dev Also advertises support for the IERC173 and IOwnable2Step interfaces. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == OwnableInterfaceId.IERC173_INTERFACE_ID + || interfaceId == Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID + || ERC165.supportsInterface(interfaceId); + } +} diff --git a/src/modules/VersionModule.sol b/src/modules/VersionModule.sol index 8da4a78..675192f 100644 --- a/src/modules/VersionModule.sol +++ b/src/modules/VersionModule.sol @@ -8,13 +8,18 @@ import {IERC3643Version} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol * @notice Exposes the contract version as required by ERC-3643. */ abstract contract VersionModule is IERC3643Version { - string private constant VERSION = "0.3.0"; + /** + * @notice The contract version string returned by {version}. + */ + string private constant VERSION = "0.4.0"; /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ - /// @inheritdoc IERC3643Version + /** + * @inheritdoc IERC3643Version + */ function version() public view virtual override returns (string memory version_) { return VERSION; } diff --git a/src/rules/interfaces/IAddressList.sol b/src/rules/interfaces/IAddressList.sol index a2e7173..36e4b79 100644 --- a/src/rules/interfaces/IAddressList.sol +++ b/src/rules/interfaces/IAddressList.sol @@ -3,22 +3,33 @@ pragma solidity ^0.8.20; import {IIdentityRegistryContains} from "./IIdentityRegistry.sol"; +/** + * @title IAddressList — interface for managing and querying a set of addresses. + */ interface IAddressList is IIdentityRegistryContains { /* ============ Events ============ */ - /// @notice Emitted when multiple addresses are added. - /// @param targetAddresses The array of added addresses. + /** + * @notice Emitted when multiple addresses are added. + * @param targetAddresses The array of added addresses. + */ event AddAddresses(address[] targetAddresses); - /// @notice Emitted when multiple addresses are removed. - /// @param targetAddresses The array of removed addresses. + /** + * @notice Emitted when multiple addresses are removed. + * @param targetAddresses The array of removed addresses. + */ event RemoveAddresses(address[] targetAddresses); - /// @notice Emitted when a single address is added. - /// @param targetAddress The added address. + /** + * @notice Emitted when a single address is added. + * @param targetAddress The added address. + */ event AddAddress(address indexed targetAddress); - /// @notice Emitted when a single address is removed. - /// @param targetAddress The removed address. + /** + * @notice Emitted when a single address is removed. + * @param targetAddress The removed address. + */ event RemoveAddress(address indexed targetAddress); /* ============ Write ============ */ diff --git a/src/rules/interfaces/IERC2980.sol b/src/rules/interfaces/IERC2980.sol index adf2f51..f5ca814 100644 --- a/src/rules/interfaces/IERC2980.sol +++ b/src/rules/interfaces/IERC2980.sol @@ -19,12 +19,16 @@ interface IERC2980 { /** * @dev Returns true if the address is in the frozenlist. * Frozen addresses cannot send or receive tokens. + * @param _operator The address to check. + * @return True if the address is in the frozenlist, otherwise false. */ function frozenlist(address _operator) external view returns (bool); /** * @dev Returns true if the address is in the whitelist. * Only whitelisted addresses can receive tokens. + * @param _operator The address to check. + * @return True if the address is in the whitelist, otherwise false. */ function whitelist(address _operator) external view returns (bool); } diff --git a/src/rules/interfaces/IERC7943NonFungibleCompliance.sol b/src/rules/interfaces/IERC7943NonFungibleCompliance.sol index 05cd3f9..fc66f10 100644 --- a/src/rules/interfaces/IERC7943NonFungibleCompliance.sol +++ b/src/rules/interfaces/IERC7943NonFungibleCompliance.sol @@ -42,43 +42,6 @@ interface IERC7943NonFungibleCompliance { * For ERC-721, `amount/value` MUST be set to `1`. */ interface IERC7943NonFungibleComplianceExtend is IERC7943NonFungibleCompliance { - /** - * @notice Returns a transfer-restriction code describing why a transfer is blocked. - * @dev - * - MUST NOT modify state. - * - MUST return `0` when the transfer is allowed. - * - Non-zero codes SHOULD follow the ERC-1404 or RuleEngine restriction-code conventions. - * - * @param from The address currently holding the token. - * @param to The address intended to receive the token. - * @param tokenId The ERC-721/1155 token ID being checked. - * @param amount The amount being transferred (always `1` for ERC-721). - * @return code A restriction code: `0` for success, otherwise an implementation-defined error code. - */ - function detectTransferRestriction(address from, address to, uint256 tokenId, uint256 amount) - external - view - returns (uint8 code); - - /** - * @notice Returns a transfer-restriction code for transfers triggered by a spender. - * @dev - * Similar to `detectTransferRestriction`, but includes the spender performing the transfer. - * - MUST NOT modify state. - * - MUST return `0` when transfer is allowed. - * - * @param spender The caller executing the transfer (owner, operator, or approved address). - * @param from The current owner of the token. - * @param to The intended recipient. - * @param tokenId The token ID being checked. - * @param value The amount being transferred (always `1` for ERC-721). - * @return code A restriction code: `0` for allowed, otherwise a non-zero restriction identifier. - */ - function detectTransferRestrictionFrom(address spender, address from, address to, uint256 tokenId, uint256 value) - external - view - returns (uint8 code); - /** * @notice Determines whether a spender-initiated transfer is allowed. * @dev @@ -124,4 +87,41 @@ interface IERC7943NonFungibleComplianceExtend is IERC7943NonFungibleCompliance { * @param value The transfer amount (always `1` for ERC-721). */ function transferred(address spender, address from, address to, uint256 tokenId, uint256 value) external; + + /** + * @notice Returns a transfer-restriction code describing why a transfer is blocked. + * @dev + * - MUST NOT modify state. + * - MUST return `0` when the transfer is allowed. + * - Non-zero codes SHOULD follow the ERC-1404 or RuleEngine restriction-code conventions. + * + * @param from The address currently holding the token. + * @param to The address intended to receive the token. + * @param tokenId The ERC-721/1155 token ID being checked. + * @param amount The amount being transferred (always `1` for ERC-721). + * @return code A restriction code: `0` for success, otherwise an implementation-defined error code. + */ + function detectTransferRestriction(address from, address to, uint256 tokenId, uint256 amount) + external + view + returns (uint8 code); + + /** + * @notice Returns a transfer-restriction code for transfers triggered by a spender. + * @dev + * Similar to `detectTransferRestriction`, but includes the spender performing the transfer. + * - MUST NOT modify state. + * - MUST return `0` when transfer is allowed. + * + * @param spender The caller executing the transfer (owner, operator, or approved address). + * @param from The current owner of the token. + * @param to The intended recipient. + * @param tokenId The token ID being checked. + * @param value The amount being transferred (always `1` for ERC-721). + * @return code A restriction code: `0` for allowed, otherwise a non-zero restriction identifier. + */ + function detectTransferRestrictionFrom(address spender, address from, address to, uint256 tokenId, uint256 value) + external + view + returns (uint8 code); } diff --git a/src/rules/interfaces/IIdentityRegistry.sol b/src/rules/interfaces/IIdentityRegistry.sol index 86d11d3..6743189 100644 --- a/src/rules/interfaces/IIdentityRegistry.sol +++ b/src/rules/interfaces/IIdentityRegistry.sol @@ -2,12 +2,28 @@ pragma solidity ^0.8.20; +/** + * @title IIdentityRegistryVerified — identity registry verification query. + */ interface IIdentityRegistryVerified { // registry consultation + /** + * @notice Returns whether the given address has a verified identity in the registry. + * @param _userAddress The address to check. + * @return True if the address is verified, otherwise false. + */ function isVerified(address _userAddress) external view returns (bool); } +/** + * @title IIdentityRegistryContains — identity registry membership query. + */ interface IIdentityRegistryContains { // registry consultation + /** + * @notice Returns whether the given address is present in the registry. + * @param _userAddress The address to check. + * @return True if the address is contained in the registry, otherwise false. + */ function contains(address _userAddress) external view returns (bool); } diff --git a/src/rules/interfaces/ISanctionsList.sol b/src/rules/interfaces/ISanctionsList.sol index 8a5775f..312fbc8 100644 --- a/src/rules/interfaces/ISanctionsList.sol +++ b/src/rules/interfaces/ISanctionsList.sol @@ -2,6 +2,14 @@ pragma solidity ^0.8.20; +/** + * @title ISanctionsList — sanctions oracle membership query. + */ interface ISanctionsList { + /** + * @notice Returns whether the given address is sanctioned. + * @param addr The address to check. + * @return True if the address is sanctioned, otherwise false. + */ function isSanctioned(address addr) external view returns (bool); } diff --git a/src/rules/interfaces/ITotalSupply.sol b/src/rules/interfaces/ITotalSupply.sol index ff5fbed..16125b5 100644 --- a/src/rules/interfaces/ITotalSupply.sol +++ b/src/rules/interfaces/ITotalSupply.sol @@ -1,6 +1,13 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title ITotalSupply — total supply query. + */ interface ITotalSupply { + /** + * @notice Returns the current total supply of the token. + * @return The total supply. + */ function totalSupply() external view returns (uint256); } diff --git a/src/rules/interfaces/ITransferContext.sol b/src/rules/interfaces/ITransferContext.sol index c0fb158..e2e33cf 100644 --- a/src/rules/interfaces/ITransferContext.sol +++ b/src/rules/interfaces/ITransferContext.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title ITransferContext — transfer context structs and post-transfer hooks. + */ interface ITransferContext { /** * @notice Transfer context for unified rule entrypoints. @@ -41,7 +44,15 @@ interface ITransferContext { bytes data; } + /** + * @notice Notifies the rule of an executed multi-token transfer. + * @param ctx The multi-token transfer context describing the transfer. + */ function transferred(MultiTokenTransferContext calldata ctx) external; + /** + * @notice Notifies the rule of an executed fungible transfer. + * @param ctx The fungible transfer context describing the transfer. + */ function transferred(FungibleTransferContext calldata ctx) external; } diff --git a/src/rules/operation/RuleConditionalTransferLight.sol b/src/rules/operation/RuleConditionalTransferLight.sol index 787427a..27c12b0 100644 --- a/src/rules/operation/RuleConditionalTransferLight.sol +++ b/src/rules/operation/RuleConditionalTransferLight.sol @@ -10,13 +10,14 @@ import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.s import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol"; import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol"; import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransferLightBase.sol"; +import {ERC3643ComplianceRolesStorage} from "RuleEngine/modules/library/ERC3643ComplianceRolesStorage.sol"; /** * @title ConditionalTransferLight * @dev Requires operator approval for each transfer. Same transfer (from, to, value) * can be approved multiple times to allow repeated transfers. */ -contract RuleConditionalTransferLight is AccessControlModuleStandalone, RuleConditionalTransferLightBase { +contract RuleConditionalTransferLight is AccessControlModuleStandalone, RuleConditionalTransferLightBase, ERC3643ComplianceRolesStorage { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -30,6 +31,9 @@ contract RuleConditionalTransferLight is AccessControlModuleStandalone, RuleCond PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc IERC165 + */ function supportsInterface(bytes4 interfaceId) public view @@ -49,7 +53,19 @@ contract RuleConditionalTransferLight is AccessControlModuleStandalone, RuleCond ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + */ + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + + /** + * @notice Reverts unless the caller holds `OPERATOR_ROLE`. + */ function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} - function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + /** + * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + */ + function _authorizeComplianceBindingChange(address) internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) + {} } diff --git a/src/rules/operation/RuleConditionalTransferLightMultiToken.sol b/src/rules/operation/RuleConditionalTransferLightMultiToken.sol new file mode 100644 index 0000000..e61dcf8 --- /dev/null +++ b/src/rules/operation/RuleConditionalTransferLightMultiToken.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol"; +import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol"; +import {RuleConditionalTransferLightMultiTokenBase} from "./abstract/RuleConditionalTransferLightMultiTokenBase.sol"; +import {ERC3643ComplianceRolesStorage} from "RuleEngine/modules/library/ERC3643ComplianceRolesStorage.sol"; + +/** + * @title RuleConditionalTransferLightMultiToken + * @notice AccessControl variant of the multi-token conditional transfer rule. + * `OPERATOR_ROLE` approves transfers; `COMPLIANCE_MANAGER_ROLE` manages compliance bindings. + */ +contract RuleConditionalTransferLightMultiToken is + AccessControlModuleStandalone, + RuleConditionalTransferLightMultiTokenBase, + ERC3643ComplianceRolesStorage +{ + /** + * @param admin Address of the contract admin. + */ + constructor(address admin) AccessControlModuleStandalone(admin) {} + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AccessControlEnumerable, IERC165) + returns (bool) + { + return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID + || interfaceId == type(IERC7551Compliance).interfaceId + || interfaceId == type(IERC3643ComplianceFull).interfaceId + || AccessControlEnumerable.supportsInterface(interfaceId); + } + + /** + * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + */ + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + + /** + * @notice Reverts unless the caller holds `OPERATOR_ROLE`. + */ + function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} +} diff --git a/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol b/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol new file mode 100644 index 0000000..53a07d0 --- /dev/null +++ b/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol"; +import {RuleConditionalTransferLightMultiTokenBase} from "./abstract/RuleConditionalTransferLightMultiTokenBase.sol"; +import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol"; + +/** + * @title RuleConditionalTransferLightMultiTokenOwnable2Step + * @notice Ownable2Step variant of the multi-token conditional transfer rule. + * The owner approves transfers and manages compliance bindings. + */ +contract RuleConditionalTransferLightMultiTokenOwnable2Step is + RuleConditionalTransferLightMultiTokenBase, + Ownable2Step, + Ownable2StepERC165Module +{ + /** + * @param owner Address of the contract owner. + */ + constructor(address owner) Ownable(owner) {} + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + override(Ownable2StepERC165Module, IERC165) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID + || interfaceId == type(IERC7551Compliance).interfaceId + || interfaceId == type(IERC3643ComplianceFull).interfaceId; + } + + /** + * @notice Reverts unless the caller is the owner. + */ + function _onlyComplianceManager() internal virtual override onlyOwner {} + + /** + * @notice Reverts unless the caller is the owner. + */ + function _authorizeTransferApproval() internal view virtual override onlyOwner {} +} diff --git a/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol b/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol index 0ba8e65..a746e86 100644 --- a/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol +++ b/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol @@ -3,31 +3,43 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; -import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol"; import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransferLightBase.sol"; +import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol"; /** * @title RuleConditionalTransferLightOwnable2Step * @notice Ownable2Step variant of RuleConditionalTransferLight. */ -contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLightBase, Ownable2Step { +contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLightBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @param owner Address of the contract owner. + */ constructor(address owner) Ownable(owner) {} /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ - function supportsInterface(bytes4 interfaceId) public view override returns (bool) { - return interfaceId == type(IERC165).interfaceId + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + override(Ownable2StepERC165Module, IERC165) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) || interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID @@ -39,7 +51,18 @@ contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLigh ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Reverts unless the caller is the owner. + */ + function _onlyComplianceManager() internal virtual override onlyOwner {} + + /** + * @notice Reverts unless the caller is the owner. + */ function _authorizeTransferApproval() internal view virtual override onlyOwner {} - function _onlyComplianceManager() internal virtual override onlyOwner {} + /** + * @notice Reverts unless the caller is the owner. + */ + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} } diff --git a/src/rules/operation/RuleMintAllowance.sol b/src/rules/operation/RuleMintAllowance.sol new file mode 100644 index 0000000..73d6a6a --- /dev/null +++ b/src/rules/operation/RuleMintAllowance.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol"; +import {RuleMintAllowanceBase} from "./abstract/RuleMintAllowanceBase.sol"; +import {ERC3643ComplianceRolesStorage} from "RuleEngine/modules/library/ERC3643ComplianceRolesStorage.sol"; + +/** + * @title RuleMintAllowance + * @notice AccessControl variant of RuleMintAllowance. + * `DEFAULT_ADMIN_ROLE` implicitly holds all roles. + * `ALLOWANCE_OPERATOR_ROLE` can set, increase, and decrease per-minter allowances. + * `COMPLIANCE_MANAGER_ROLE` can bind/unbind the rule to a RuleEngine. + */ +contract RuleMintAllowance is AccessControlModuleStandalone, RuleMintAllowanceBase, ERC3643ComplianceRolesStorage { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /** + * @param admin Address of the contract admin. + */ + constructor(address admin) AccessControlModuleStandalone(admin) {} + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AccessControlEnumerable, IERC165) + returns (bool) + { + // Do not advertise full ERC-3643 ICompliance: its 3-arg mint callback + // cannot identify the minter, so quota enforcement needs the spender-aware path. + return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId + || AccessControlEnumerable.supportsInterface(interfaceId); + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + */ + function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + + /** + * @notice Reverts unless the caller holds `ALLOWANCE_OPERATOR_ROLE`. + */ + function _authorizeSetMintAllowance() internal view virtual override onlyRole(ALLOWANCE_OPERATOR_ROLE) {} + + /** + * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + */ + function _authorizeComplianceBindingChange(address) + internal + view + virtual + override + onlyRole(COMPLIANCE_MANAGER_ROLE) + {} +} diff --git a/src/rules/operation/RuleMintAllowanceOwnable2Step.sol b/src/rules/operation/RuleMintAllowanceOwnable2Step.sol new file mode 100644 index 0000000..171cdbd --- /dev/null +++ b/src/rules/operation/RuleMintAllowanceOwnable2Step.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {RuleMintAllowanceBase} from "./abstract/RuleMintAllowanceBase.sol"; +import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol"; + +/** + * @title RuleMintAllowanceOwnable2Step + * @notice Ownable2Step variant of RuleMintAllowance. + * The owner manages all allowances and compliance bindings. + */ +contract RuleMintAllowanceOwnable2Step is RuleMintAllowanceBase, Ownable2Step, Ownable2StepERC165Module { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /** + * @param owner Address of the contract owner. + */ + constructor(address owner) Ownable(owner) {} + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + override(Ownable2StepERC165Module, IERC165) + returns (bool) + { + // Do not advertise full ERC-3643 ICompliance: its 3-arg mint callback + // cannot identify the minter, so quota enforcement needs the spender-aware path. + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId; + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Reverts unless the caller is the owner. + */ + function _onlyComplianceManager() internal virtual override onlyOwner {} + + /** + * @notice Reverts unless the caller is the owner. + */ + function _authorizeSetMintAllowance() internal view virtual override onlyOwner {} + + /** + * @notice Reverts unless the caller is the owner. + */ + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} +} diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol index 83b4b3b..f23bb3a 100644 --- a/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol +++ b/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol @@ -10,7 +10,9 @@ import {RuleConditionalTransferLightInvariantStorage} from "./RuleConditionalTra * No knowledge of token binding or compliance interfaces. */ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTransferLightInvariantStorage { - // Mapping from transfer hash to approval count + /** + * @notice Number of outstanding approvals for each transfer hash (mapping from transfer hash to approval count) + */ mapping(bytes32 => uint256) public approvalCounts; /*////////////////////////////////////////////////////////////// @@ -27,14 +29,14 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra _; } - function _authorizeTransferApproval() internal view virtual; - - function _authorizeTransferExecution() internal view virtual; - /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Consumes one approval for the transfer described by `ctx`. + * @param ctx The fungible transfer context (from, to, value). + */ function transferred(ITransferContext.FungibleTransferContext calldata ctx) external onlyTransferExecutor { _transferredFromContext(ctx); } @@ -43,12 +45,24 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Records a new approval for the given transfer, incrementing its approval count. + * @param from The sender of the transfer to approve. + * @param to The recipient of the transfer to approve. + * @param value The amount of the transfer to approve. + */ function approveTransfer(address from, address to, uint256 value) public onlyTransferApprover { bytes32 transferHash = _transferHash(from, to, value); approvalCounts[transferHash] += 1; emit TransferApproved(from, to, value, approvalCounts[transferHash]); } + /** + * @notice Cancels one outstanding approval for the given transfer; reverts if none exists. + * @param from The sender of the transfer whose approval is cancelled. + * @param to The recipient of the transfer whose approval is cancelled. + * @param value The amount of the transfer whose approval is cancelled. + */ function cancelTransferApproval(address from, address to, uint256 value) public onlyTransferApprover { bytes32 transferHash = _transferHash(from, to, value); uint256 count = approvalCounts[transferHash]; @@ -57,6 +71,13 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra emit TransferApprovalCancelled(from, to, value, approvalCounts[transferHash]); } + /** + * @notice Returns the number of outstanding approvals for the given transfer. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + * @return The current approval count for the transfer. + */ function approvedCount(address from, address to, uint256 value) public view returns (uint256) { bytes32 transferHash = _transferHash(from, to, value); return approvalCounts[transferHash]; @@ -66,10 +87,21 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Consumes one approval for the transfer described by `ctx`. + * @param ctx The fungible transfer context (from, to, value). + */ function _transferredFromContext(ITransferContext.FungibleTransferContext calldata ctx) internal virtual { _transferred(ctx.from, ctx.to, ctx.value); } + /** + * @notice Consumes one approval for the given transfer; reverts if none exists. + * @dev No-op when either endpoint is the zero address (mint/burn). + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + */ function _transferred(address from, address to, uint256 value) internal virtual { if (from == address(0) || to == address(0)) { return; @@ -83,6 +115,13 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra emit TransferExecuted(from, to, value, approvalCounts[transferHash]); } + /** + * @notice Computes the storage key identifying a (from, to, value) transfer. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + * @return hash The keccak256 hash uniquely identifying the transfer. + */ function _transferHash(address from, address to, uint256 value) internal pure virtual returns (bytes32 hash) { // Linter suggestion (`asm-keccak256`): hash packed values in assembly to avoid abi.encodePacked overhead. assembly ("memory-safe") { @@ -93,4 +132,14 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra hash := keccak256(ptr, 0x60) } } + + /** + * @notice Authorizes the caller to approve or cancel transfers; reverts if unauthorized. + */ + function _authorizeTransferApproval() internal view virtual; + + /** + * @notice Authorizes the caller to execute (consume) approved transfers; reverts if unauthorized. + */ + function _authorizeTransferExecution() internal view virtual; } diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol index 7b2bfca..5e96ba6 100644 --- a/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol +++ b/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol @@ -29,10 +29,34 @@ abstract contract RuleConditionalTransferLightBase is EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Compliance hook invoked when tokens are created (minted); consumes an approval if applicable. + * @param to The recipient of the created tokens. + * @param value The amount of tokens created. + */ + function created(address to, uint256 value) external onlyBoundToken { + _transferred(address(0), to, value); + } + + /** + * @notice Compliance hook invoked when tokens are destroyed (burned); consumes an approval if applicable. + * @param from The holder whose tokens are destroyed. + * @param value The amount of tokens destroyed. + */ + function destroyed(address from, uint256 value) external onlyBoundToken { + _transferred(from, address(0), value); + } + + /** + * @inheritdoc IRule + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) external pure @@ -45,14 +69,6 @@ abstract contract RuleConditionalTransferLightBase is return TEXT_CODE_NOT_FOUND; } - function created(address to, uint256 value) external onlyBoundToken { - _transferred(address(0), to, value); - } - - function destroyed(address from, uint256 value) external onlyBoundToken { - _transferred(from, address(0), value); - } - /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -62,6 +78,10 @@ abstract contract RuleConditionalTransferLightBase is * @dev Requires `from` to have approved this contract on the token. * @dev This function is only safe for tokens that call back `transferred()` during transfer. * @dev CEI is intentionally inverted so the approval exists for the callback. + * @param from The holder to transfer tokens from. + * @param to The recipient of the transfer. + * @param value The amount to transfer. + * @return True when the transfer succeeds. */ function approveAndTransferIfAllowed(address from, address to, uint256 value) public @@ -80,6 +100,9 @@ abstract contract RuleConditionalTransferLightBase is return true; } + /** + * @inheritdoc IERC3643IComplianceContract + */ function transferred(address from, address to, uint256 value) public override(IERC3643IComplianceContract) @@ -88,6 +111,9 @@ abstract contract RuleConditionalTransferLightBase is _transferred(from, to, value); } + /** + * @inheritdoc IRuleEngine + */ function transferred( address, /* spender */ @@ -110,12 +136,16 @@ abstract contract RuleConditionalTransferLightBase is * from the previous token remain in storage and can be consumed after rebinding. * The operator who controls rebinding also controls approvals, so the trust * model is preserved, but integrators should be aware of this behavior. + * @param token The token to bind to this rule. */ function bindToken(address token) public override onlyComplianceManager { require(getTokenBound() == address(0), RuleConditionalTransferLight_TokenAlreadyBound()); _bindToken(token); } + /** + * @inheritdoc IERC1404 + */ function detectTransferRestriction(address from, address to, uint256 value) public view @@ -132,6 +162,9 @@ abstract contract RuleConditionalTransferLightBase is return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @inheritdoc IERC1404Extend + */ function detectTransferRestrictionFrom( address, /* spender */ @@ -147,6 +180,9 @@ abstract contract RuleConditionalTransferLightBase is return detectTransferRestriction(from, to, value); } + /** + * @inheritdoc IERC3643ComplianceRead + */ function canTransfer(address from, address to, uint256 value) public view @@ -156,6 +192,9 @@ abstract contract RuleConditionalTransferLightBase is return detectTransferRestriction(from, to, value) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @inheritdoc IERC7551Compliance + */ function canTransferFrom(address spender, address from, address to, uint256 value) public view @@ -170,6 +209,9 @@ abstract contract RuleConditionalTransferLightBase is ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Authorizes transfer execution: only the bound token may call the execution hooks. + */ function _authorizeTransferExecution() internal view override { require(isTokenBound(_msgSender()), RuleConditionalTransferLight_TransferExecutorUnauthorized(_msgSender())); } diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol b/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol index 61aed67..c163d61 100644 --- a/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol +++ b/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol @@ -3,18 +3,51 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "../../validation/abstract/invariant/RuleSharedInvariantStorage.sol"; +/** + * @title RuleConditionalTransferLightInvariantStorage — constants, events and errors for the conditional-transfer rule + */ abstract contract RuleConditionalTransferLightInvariantStorage is RuleSharedInvariantStorage { /* ============ Role ============ */ + /** + * @notice Role allowed to approve, cancel and execute conditional transfers + */ bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); /* ============ State variables ============ */ + /** + * @notice Human-readable message returned when a transfer has not been approved + */ string constant TEXT_TRANSFER_REQUEST_NOT_APPROVED = "ConditionalTransferLight: The request is not approved"; // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when a transfer request has not been approved + */ uint8 public constant CODE_TRANSFER_REQUEST_NOT_APPROVED = 46; /* ============ Events ============ */ + /** + * @notice Emitted when a transfer is approved + * @param from The sender of the approved transfer + * @param to The recipient of the approved transfer + * @param value The amount of the approved transfer + * @param count The approval count for this transfer after the approval + */ event TransferApproved(address indexed from, address indexed to, uint256 value, uint256 count); + /** + * @notice Emitted when an approved transfer is executed + * @param from The sender of the executed transfer + * @param to The recipient of the executed transfer + * @param value The amount of the executed transfer + * @param remaining The approval count remaining for this transfer after execution + */ event TransferExecuted(address indexed from, address indexed to, uint256 value, uint256 remaining); + /** + * @notice Emitted when a transfer approval is cancelled + * @param from The sender of the cancelled transfer approval + * @param to The recipient of the cancelled transfer approval + * @param value The amount of the cancelled transfer approval + * @param remaining The approval count remaining for this transfer after cancellation + */ event TransferApprovalCancelled(address indexed from, address indexed to, uint256 value, uint256 remaining); /* ============ Custom error ============ */ diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol new file mode 100644 index 0000000..095f530 --- /dev/null +++ b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; +import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IRule} from "RuleEngine/interfaces/IRule.sol"; +import {ERC3643ComplianceModule} from "RuleEngine/modules/ERC3643ComplianceModule.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {VersionModule} from "../../../modules/VersionModule.sol"; +import {RuleConditionalTransferLightMultiTokenInvariantStorage} from "./RuleConditionalTransferLightMultiTokenInvariantStorage.sol"; +import {ITransferContext} from "../../interfaces/ITransferContext.sol"; + +/** + * @title RuleConditionalTransferLightMultiTokenBase — conditional-transfer rule wiring per-token approval state into the compliance interfaces + */ +abstract contract RuleConditionalTransferLightMultiTokenBase is + VersionModule, + ERC3643ComplianceModule, + RuleConditionalTransferLightMultiTokenInvariantStorage, + IRule +{ + using SafeERC20 for IERC20; + + /** + * @notice Number of outstanding approvals for each per-token transfer hash + */ + mapping(bytes32 => uint256) public approvalCounts; + + modifier onlyTransferApprover() { + _authorizeTransferApproval(); + _; + } + + modifier onlyTransferExecutor() { + _authorizeTransferExecution(); + _; + } + + /** + * @notice Compliance hook invoked when tokens are created (minted); consumes an approval if applicable. + * @param to The recipient of the created tokens. + * @param value The amount of tokens created. + */ + function created(address to, uint256 value) external onlyBoundToken { + _transferred(_msgSender(), address(0), to, value); + } + + /** + * @notice Compliance hook invoked when tokens are destroyed (burned); consumes an approval if applicable. + * @param from The holder whose tokens are destroyed. + * @param value The amount of tokens destroyed. + */ + function destroyed(address from, uint256 value) external onlyBoundToken { + _transferred(_msgSender(), from, address(0), value); + } + + /** + * @notice Consumes one approval for the transfer described by `ctx`, using the caller as the token. + * @param ctx The fungible transfer context (from, to, value). + */ + function transferred(ITransferContext.FungibleTransferContext calldata ctx) external onlyTransferExecutor { + _transferred(_msgSender(), ctx.from, ctx.to, ctx.value); + } + + /** + * @inheritdoc IRule + */ + function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { + return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; + } + + /** + * @inheritdoc IERC1404 + */ + function messageForTransferRestriction(uint8 restrictionCode) + external + pure + override(IERC1404) + returns (string memory) + { + if (restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED) { + return TEXT_TRANSFER_REQUEST_NOT_APPROVED; + } + return TEXT_CODE_NOT_FOUND; + } + + /** + * @notice Records a new approval for the given per-token transfer. + * @param token The token the transfer applies to. + * @param from The sender of the transfer to approve. + * @param to The recipient of the transfer to approve. + * @param value The amount of the transfer to approve. + */ + function approveTransfer(address token, address from, address to, uint256 value) public onlyTransferApprover { + _approveTransfer(token, from, to, value); + } + + /** + * @notice Cancels one outstanding approval for the given per-token transfer. + * @param token The token the transfer applies to. + * @param from The sender of the transfer whose approval is cancelled. + * @param to The recipient of the transfer whose approval is cancelled. + * @param value The amount of the transfer whose approval is cancelled. + */ + function cancelTransferApproval(address token, address from, address to, uint256 value) public onlyTransferApprover { + _cancelTransferApproval(token, from, to, value); + } + + /** + * @notice Approves and performs a transferFrom of `token` using this rule as spender. + * @dev Requires `from` to have approved this contract on `token`; the token must be bound. + * @param token The token to transfer. + * @param from The holder to transfer tokens from. + * @param to The recipient of the transfer. + * @param value The amount to transfer. + * @return True when the transfer succeeds. + */ + function approveAndTransferIfAllowed(address token, address from, address to, uint256 value) + public + onlyTransferApprover + returns (bool) + { + require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken()); + + _approveTransfer(token, from, to, value); + + uint256 allowed = IERC20(token).allowance(from, address(this)); + require( + allowed >= value, + RuleConditionalTransferLightMultiToken_InsufficientAllowance(token, from, allowed, value) + ); + + IERC20(token).safeTransferFrom(from, to, value); + return true; + } + + /** + * @inheritdoc IERC3643IComplianceContract + */ + function transferred(address from, address to, uint256 value) + public + override(IERC3643IComplianceContract) + onlyTransferExecutor + { + _transferred(_msgSender(), from, to, value); + } + + /** + * @inheritdoc IRuleEngine + */ + function transferred( + address, + /* spender */ + address from, + address to, + uint256 value + ) + public + override(IRuleEngine) + onlyTransferExecutor + { + _transferred(_msgSender(), from, to, value); + } + + /** + * @notice Returns the number of outstanding approvals for the given per-token transfer. + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + * @return The current approval count for the transfer. + */ + function approvedCount(address token, address from, address to, uint256 value) public view returns (uint256) { + bytes32 transferHash = _transferHash(token, from, to, value); + return approvalCounts[transferHash]; + } + + /** + * @inheritdoc IERC1404 + */ + function detectTransferRestriction(address from, address to, uint256 value) + public + view + override(IERC1404) + returns (uint8) + { + if (from == address(0) || to == address(0)) { + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + address token = _msgSender(); + if (!isTokenBound(token)) { + return CODE_TRANSFER_REQUEST_NOT_APPROVED; + } + + bytes32 transferHash = _transferHash(token, from, to, value); + if (approvalCounts[transferHash] == 0) { + return CODE_TRANSFER_REQUEST_NOT_APPROVED; + } + + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @inheritdoc IERC1404Extend + */ + function detectTransferRestrictionFrom( + address, + /* spender */ + address from, + address to, + uint256 value + ) + public + view + override(IERC1404Extend) + returns (uint8) + { + return detectTransferRestriction(from, to, value); + } + + /** + * @inheritdoc IERC3643ComplianceRead + */ + function canTransfer(address from, address to, uint256 value) + public + view + override(IERC3643ComplianceRead) + returns (bool) + { + return detectTransferRestriction(from, to, value) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @inheritdoc IERC7551Compliance + */ + function canTransferFrom(address spender, address from, address to, uint256 value) + public + view + override(IERC7551Compliance) + returns (bool) + { + return detectTransferRestrictionFrom(spender, from, to, value) + == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @notice Authorizes changes to compliance binding: restricted to the compliance manager. + */ + function _authorizeComplianceBindingChange(address) internal virtual override { + _onlyComplianceManager(); + } + + /** + * @notice Records a new approval for the given per-token transfer; reverts if the token is not bound. + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + */ + function _approveTransfer(address token, address from, address to, uint256 value) internal virtual { + require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken()); + bytes32 transferHash = _transferHash(token, from, to, value); + approvalCounts[transferHash] += 1; + emit TransferApproved(token, from, to, value, approvalCounts[transferHash]); + } + + /** + * @notice Cancels one outstanding approval for the given per-token transfer; reverts if none exists. + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + */ + function _cancelTransferApproval(address token, address from, address to, uint256 value) internal virtual { + require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken()); + bytes32 transferHash = _transferHash(token, from, to, value); + uint256 count = approvalCounts[transferHash]; + + require(count != 0, RuleConditionalTransferLightMultiToken_TransferApprovalNotFound()); + + approvalCounts[transferHash] = count - 1; + emit TransferApprovalCancelled(token, from, to, value, approvalCounts[transferHash]); + } + + /** + * @notice Consumes one approval for the given per-token transfer; reverts if none exists. + * @dev No-op when either endpoint is the zero address (mint/burn). + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + */ + function _transferred(address token, address from, address to, uint256 value) internal virtual { + if (from == address(0) || to == address(0)) { + return; + } + + bytes32 transferHash = _transferHash(token, from, to, value); + uint256 count = approvalCounts[transferHash]; + + require(count != 0, RuleConditionalTransferLightMultiToken_TransferNotApproved()); + + approvalCounts[transferHash] = count - 1; + emit TransferExecuted(token, from, to, value, approvalCounts[transferHash]); + } + + /** + * @notice Authorizes transfer execution: only a bound token may call the execution hooks. + */ + function _authorizeTransferExecution() internal view virtual { + require( + isTokenBound(_msgSender()), + RuleConditionalTransferLightMultiToken_TransferExecutorUnauthorized(_msgSender()) + ); + } + + /** + * @notice Authorizes the caller to approve or cancel transfers; reverts if unauthorized. + */ + function _authorizeTransferApproval() internal view virtual; + + /** + * @notice Computes the storage key identifying a (token, from, to, value) transfer. + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + * @return hash The keccak256 hash uniquely identifying the transfer. + */ + function _transferHash(address token, address from, address to, uint256 value) + internal + pure + virtual + returns (bytes32 hash) + { + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(ptr, shl(96, token)) + mstore(add(ptr, 0x20), shl(96, from)) + mstore(add(ptr, 0x40), shl(96, to)) + mstore(add(ptr, 0x60), value) + hash := keccak256(ptr, 0x80) + } + } +} diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol new file mode 100644 index 0000000..6dec2cb --- /dev/null +++ b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {RuleSharedInvariantStorage} from "../../validation/abstract/invariant/RuleSharedInvariantStorage.sol"; + +/** + * @title RuleConditionalTransferLightMultiTokenInvariantStorage — constants, events and errors for the multi-token conditional-transfer rule + */ +abstract contract RuleConditionalTransferLightMultiTokenInvariantStorage is RuleSharedInvariantStorage { + /* ============ Role ============ */ + /** + * @notice Role allowed to approve, cancel and execute conditional transfers + */ + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + /* ============ State variables ============ */ + /** + * @notice Human-readable message returned when a transfer has not been approved + */ + string constant TEXT_TRANSFER_REQUEST_NOT_APPROVED = "ConditionalTransferLightMultiToken: The request is not approved"; + /** + * @notice Restriction code returned when a transfer request has not been approved + */ + uint8 public constant CODE_TRANSFER_REQUEST_NOT_APPROVED = 46; + + /* ============ Events ============ */ + /** + * @notice Emitted when a transfer is approved for a given token + * @param token The token the approval applies to + * @param from The sender of the approved transfer + * @param to The recipient of the approved transfer + * @param value The amount of the approved transfer + * @param count The approval count for this transfer after the approval + */ + event TransferApproved(address indexed token, address indexed from, address indexed to, uint256 value, uint256 count); + /** + * @notice Emitted when an approved transfer is executed for a given token + * @param token The token the transfer applies to + * @param from The sender of the executed transfer + * @param to The recipient of the executed transfer + * @param value The amount of the executed transfer + * @param remaining The approval count remaining for this transfer after execution + */ + event TransferExecuted(address indexed token, address indexed from, address indexed to, uint256 value, uint256 remaining); + /** + * @notice Emitted when a transfer approval is cancelled for a given token + * @param token The token the approval applies to + * @param from The sender of the cancelled transfer approval + * @param to The recipient of the cancelled transfer approval + * @param value The amount of the cancelled transfer approval + * @param remaining The approval count remaining for this transfer after cancellation + */ + event TransferApprovalCancelled( + address indexed token, address indexed from, address indexed to, uint256 value, uint256 remaining + ); + + /* ============ Custom error ============ */ + error RuleConditionalTransferLightMultiToken_TransferExecutorUnauthorized(address account); + error RuleConditionalTransferLightMultiToken_InsufficientAllowance( + address token, address owner, uint256 allowance, uint256 required + ); + error RuleConditionalTransferLightMultiToken_InvalidToken(); + error RuleConditionalTransferLightMultiToken_TransferNotApproved(); + error RuleConditionalTransferLightMultiToken_TransferApprovalNotFound(); +} diff --git a/src/rules/operation/abstract/RuleMintAllowanceBase.sol b/src/rules/operation/abstract/RuleMintAllowanceBase.sol new file mode 100644 index 0000000..7a89bd6 --- /dev/null +++ b/src/rules/operation/abstract/RuleMintAllowanceBase.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; +import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IRule} from "RuleEngine/interfaces/IRule.sol"; +import {ERC3643ComplianceModule} from "RuleEngine/modules/ERC3643ComplianceModule.sol"; +import {VersionModule} from "../../../modules/VersionModule.sol"; +import {RuleMintAllowanceInvariantStorage} from "./RuleMintAllowanceInvariantStorage.sol"; + +/** + * @title RuleMintAllowanceBase + * @notice Core logic for per-minter mint quota enforcement. + * @dev Operators set the number of tokens each minter address is allowed to mint. + * Each mint reduces the minter's allowance. Allowances can be set to an absolute + * value or adjusted incrementally via `increaseMintAllowance`/`decreaseMintAllowance`. + * + * The rule tracks mints via the 4-arg `transferred(spender, from=0, to, value)` path + * introduced in CMTAT v3.3. The 3-arg `transferred(from=0, to, value)` path has no + * minter identity and performs no deduction. + * + * `detectTransferRestriction(from, to, value)` always returns TRANSFER_OK because + * the minter identity is unavailable in the 3-arg call; use + * `detectTransferRestrictionFrom(minter, address(0), to, amount)` to query allowance. + * + * Callers of the state-modifying `transferred()` functions must be bound via + * `bindToken(ruleEngineAddress)` before minting starts. In a standard CMTAT + + * RuleEngine setup the RuleEngine address is the entity to bind. + */ +abstract contract RuleMintAllowanceBase is + VersionModule, + ERC3643ComplianceModule, + RuleMintAllowanceInvariantStorage, + IRule +{ + /*////////////////////////////////////////////////////////////// + STATE + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Remaining mint allowance for each minter address, in token base units + */ + mapping(address minter => uint256 allowance) public mintAllowance; + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + modifier onlyAllowanceOperator() { + _authorizeSetMintAllowance(); + _; + } + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Compliance hook invoked when tokens are created (minted); no-op for this rule. + */ + function created(address, uint256) external virtual override onlyBoundToken {} + + /** + * @notice Compliance hook invoked when tokens are destroyed (burned); no-op for this rule. + */ + function destroyed(address, uint256) external virtual override onlyBoundToken {} + + /** + * @inheritdoc IRule + */ + function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { + return restrictionCode == CODE_MINTER_ALLOWANCE_EXCEEDED; + } + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Sets `minter`'s allowance to an absolute `amount`. + * @param minter The minter whose allowance is set. + * @param amount The absolute allowance to assign. + */ + function setMintAllowance(address minter, uint256 amount) public virtual onlyAllowanceOperator { + _setMintAllowance(minter, amount); + } + + /** + * @notice Increases `minter`'s allowance by `amount`. + * @param minter The minter whose allowance is increased. + * @param amount The amount to add to the allowance. + */ + function increaseMintAllowance(address minter, uint256 amount) public virtual onlyAllowanceOperator { + uint256 newAllowance = mintAllowance[minter] + amount; + mintAllowance[minter] = newAllowance; + emit MintAllowanceIncreased(minter, amount, newAllowance); + } + + /** + * @notice Decreases `minter`'s allowance by `amount`. Reverts if the reduction + * would underflow (i.e. `amount > current allowance`). + * @param minter The minter whose allowance is decreased. + * @param amount The amount to subtract from the allowance. + */ + function decreaseMintAllowance(address minter, uint256 amount) public virtual onlyAllowanceOperator { + uint256 current = mintAllowance[minter]; + require(amount <= current, RuleMintAllowance_DecreaseBelowZero(minter, current, amount)); + uint256 newAllowance = current - amount; + mintAllowance[minter] = newAllowance; + emit MintAllowanceDecreased(minter, amount, newAllowance); + } + + /** + * @notice Binds a caller to this rule. Reverts if a caller is already bound. + * @dev Enforces single-target binding to prevent one allowance state from being + * shared across several RuleEngines/tokens. To migrate, call `unbindToken` + * first, then bind the new caller. + * @param token The caller (RuleEngine/token) to bind to this rule. + */ + function bindToken(address token) public virtual override onlyComplianceManager { + require(getTokenBound() == address(0), RuleMintAllowance_TokenAlreadyBound()); + _bindToken(token); + } + + /** + * @dev 3-arg path: no minter identity available; performs no deduction. + * Mints always arrive via the 4-arg path in CMTAT v3.3+. + * @param from The sender address (address(0) for mints). + * @param to The recipient address. + * @param value The amount transferred. + */ + function transferred(address from, address to, uint256 value) + public + virtual + override(IERC3643IComplianceContract) + onlyBoundToken + { + _transferred(from, to, value); + } + + /** + * @dev 4-arg path: `spender` is the minter when `from == address(0)`. + * Deducts `value` from `mintAllowance[spender]`; reverts if insufficient. + * @param spender The minter identity when `from == address(0)`. + * @param from The sender address (address(0) for mints). + * @param to The recipient address. + * @param value The amount transferred. + */ + function transferred(address spender, address from, address to, uint256 value) + public + virtual + override(IRuleEngine) + onlyBoundToken + { + _transferredFrom(spender, from, to, value); + } + + /** + * @inheritdoc IERC1404 + */ + function messageForTransferRestriction(uint8 restrictionCode) + public + pure + override(IERC1404) + returns (string memory) + { + if (restrictionCode == CODE_MINTER_ALLOWANCE_EXCEEDED) { + return TEXT_MINTER_ALLOWANCE_EXCEEDED; + } + return TEXT_CODE_NOT_FOUND; + } + + /** + * @dev Always returns TRANSFER_OK: the minter address is not available in the + * 3-arg call. Call `detectTransferRestrictionFrom` to check a minter's quota. + * @return The restriction code, always TRANSFER_OK. + */ + function detectTransferRestriction(address, address, uint256) + public + view + virtual + override(IERC1404) + returns (uint8) + { + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @inheritdoc IERC1404Extend + */ + function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + public + view + virtual + override(IERC1404Extend) + returns (uint8) + { + return _detectTransferRestrictionFrom(spender, from, to, value); + } + + /** + * @dev Always returns true: use `canTransferFrom` to check mint allowance. + * @return Always true. + */ + function canTransfer(address, address, uint256) + public + view + virtual + override(IERC3643ComplianceRead) + returns (bool) + { + return true; + } + + /** + * @inheritdoc IERC7551Compliance + */ + function canTransferFrom(address spender, address from, address to, uint256 value) + public + view + virtual + override(IERC7551Compliance) + returns (bool) + { + return detectTransferRestrictionFrom(spender, from, to, value) + == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice 3-arg path helper: regular transfers carry no minter identity and are not tracked. + */ + function _transferred(address, address, uint256) internal virtual { + // 3-arg path: no minter identity; regular transfers are not tracked by this rule. + } + + /** + * @notice Deducts a mint from the minter's allowance on the 4-arg path. + * @dev No-op unless `from == address(0)` (a mint). Reverts if `value` exceeds + * the minter's remaining allowance. + * @param spender The minter identity. + * @param from The sender address (must be address(0) to deduct). + * @param value The amount minted. + */ + function _transferredFrom(address spender, address from, address, uint256 value) internal virtual { + if (from != address(0)) { + return; + } + uint256 current = mintAllowance[spender]; + require(value <= current, RuleMintAllowance_AllowanceExceeded(address(this), spender, current, value)); + uint256 remaining = current - value; + mintAllowance[spender] = remaining; + emit MintAllowanceConsumed(spender, value, remaining); + } + + /** + * @notice Sets a minter's allowance to an absolute value and emits the event. + * @param minter The minter whose allowance is set. + * @param amount The absolute allowance to assign. + */ + function _setMintAllowance(address minter, uint256 amount) internal virtual { + mintAllowance[minter] = amount; + emit MintAllowanceSet(minter, amount); + } + + /** + * @notice Computes whether a prospective mint would exceed the minter's allowance. + * @dev Only mints (`from == address(0)`) are checked; other transfers return TRANSFER_OK. + * @param spender The minter identity. + * @param from The sender address (checked only when address(0)). + * @param value The amount to be minted. + * @return The restriction code (CODE_MINTER_ALLOWANCE_EXCEEDED or TRANSFER_OK). + */ + function _detectTransferRestrictionFrom(address spender, address from, address, uint256 value) + internal + view + virtual + returns (uint8) + { + if (from == address(0) && mintAllowance[spender] < value) { + return CODE_MINTER_ALLOWANCE_EXCEEDED; + } + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @notice Authorizes the caller to set/adjust mint allowances; reverts if unauthorized. + */ + function _authorizeSetMintAllowance() internal view virtual; +} diff --git a/src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol b/src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol new file mode 100644 index 0000000..249a555 --- /dev/null +++ b/src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {RuleSharedInvariantStorage} from "../../validation/abstract/invariant/RuleSharedInvariantStorage.sol"; + +/** + * @title RuleMintAllowanceInvariantStorage — constants, events and errors for the mint-allowance rule + */ +abstract contract RuleMintAllowanceInvariantStorage is RuleSharedInvariantStorage { + /* ============ Role ============ */ + /** + * @notice Role allowed to set, increase and decrease minter allowances + */ + bytes32 public constant ALLOWANCE_OPERATOR_ROLE = keccak256("ALLOWANCE_OPERATOR_ROLE"); + + /* ============ State variables ============ */ + /** + * @notice Human-readable message returned when a minter's allowance is exceeded + */ + string constant TEXT_MINTER_ALLOWANCE_EXCEEDED = "MintAllowance: minter allowance exceeded"; + // It is very important that each rule uses a unique code + /** + * @notice Restriction code returned when a minter's allowance is exceeded + */ + uint8 public constant CODE_MINTER_ALLOWANCE_EXCEEDED = 70; + + /* ============ Events ============ */ + /** + * @notice Emitted when a minter's allowance is set to an absolute value + * @param minter The minter whose allowance was set + * @param newAllowance The allowance after the update + */ + event MintAllowanceSet(address indexed minter, uint256 newAllowance); + /** + * @notice Emitted when a minter's allowance is increased + * @param minter The minter whose allowance was increased + * @param addedAmount The amount added to the allowance + * @param newAllowance The allowance after the increase + */ + event MintAllowanceIncreased(address indexed minter, uint256 addedAmount, uint256 newAllowance); + /** + * @notice Emitted when a minter's allowance is decreased + * @param minter The minter whose allowance was decreased + * @param reducedAmount The amount subtracted from the allowance + * @param newAllowance The allowance after the decrease + */ + event MintAllowanceDecreased(address indexed minter, uint256 reducedAmount, uint256 newAllowance); + /** + * @notice Emitted when a mint consumes part of a minter's allowance + * @param minter The minter whose allowance was consumed + * @param consumed The amount deducted by the mint + * @param remaining The allowance remaining after consumption + */ + event MintAllowanceConsumed(address indexed minter, uint256 consumed, uint256 remaining); + + /* ============ Custom error ============ */ + error RuleMintAllowance_AllowanceExceeded(address rule, address minter, uint256 allowance, uint256 amount); + error RuleMintAllowance_DecreaseBelowZero(address minter, uint256 currentAllowance, uint256 reductionAmount); + error RuleMintAllowance_TokenAlreadyBound(); +} diff --git a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol index a183d2e..ada0412 100644 --- a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol +++ b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol @@ -47,10 +47,6 @@ abstract contract RuleAddressSet is _; } - function _authorizeAddressListAdd() internal view virtual; - - function _authorizeAddressListRemove() internal view virtual; - /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -147,17 +143,33 @@ abstract contract RuleAddressSet is INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ - /// @inheritdoc ERC2771Context + /** + * @notice Authorizes the caller to add addresses to the set; reverts if unauthorized. + */ + function _authorizeAddressListAdd() internal view virtual; + + /** + * @notice Authorizes the caller to remove addresses from the set; reverts if unauthorized. + */ + function _authorizeAddressListRemove() internal view virtual; + + /** + * @inheritdoc ERC2771Context + */ function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { return ERC2771Context._msgSender(); } - /// @inheritdoc ERC2771Context + /** + * @inheritdoc ERC2771Context + */ function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { return ERC2771Context._msgData(); } - /// @inheritdoc ERC2771Context + /** + * @inheritdoc ERC2771Context + */ function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); } diff --git a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol index 7fae61d..1eb5987 100644 --- a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol +++ b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol @@ -19,7 +19,9 @@ abstract contract RuleAddressSetInternal { STATE VARIABLES //////////////////////////////////////////////////////////////*/ - /// @dev Storage for all listed addresses. + /** + * @dev Storage for all listed addresses. + */ EnumerableSet.AddressSet private _listedAddresses; /*////////////////////////////////////////////////////////////// diff --git a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol index d31478e..a2b3a69 100644 --- a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol +++ b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol @@ -2,15 +2,28 @@ pragma solidity ^0.8.20; +/** + * @title RuleAddressSetInvariantStorage — roles and errors for the address-set rule. + */ abstract contract RuleAddressSetInvariantStorage { /* ============ Role ============ */ + /** + * @notice Role allowed to remove addresses from the set. + */ bytes32 public constant ADDRESS_LIST_REMOVE_ROLE = keccak256("ADDRESS_LIST_REMOVE_ROLE"); + /** + * @notice Role allowed to add addresses to the set. + */ bytes32 public constant ADDRESS_LIST_ADD_ROLE = keccak256("ADDRESS_LIST_ADD_ROLE"); /* ============ Custom errors ============ */ - /// @notice Thrown when trying to add an address that is already listed. + /** + * @notice Thrown when trying to add an address that is already listed. + */ error RuleAddressSet_AddressAlreadyListed(); - /// @notice Thrown when trying to remove an address that is not listed. + /** + * @notice Thrown when trying to remove an address that is not listed. + */ error RuleAddressSet_AddressNotFound(); } diff --git a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol index a356e7b..9b0ca6f 100644 --- a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol +++ b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol @@ -4,16 +4,37 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "../../invariant/RuleSharedInvariantStorage.sol"; +/** + * @title RuleBlacklistInvariantStorage — constants and errors for the blacklist rule. + */ abstract contract RuleBlacklistInvariantStorage is RuleSharedInvariantStorage { /* ============ String message ============ */ + /** + * @notice Restriction message returned when the sender is blacklisted. + */ string constant TEXT_ADDRESS_FROM_IS_BLACKLISTED = "The sender is blacklisted"; + /** + * @notice Restriction message returned when the recipient is blacklisted. + */ string constant TEXT_ADDRESS_TO_IS_BLACKLISTED = "The recipient is blacklisted"; + /** + * @notice Restriction 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 returned when the sender is blacklisted. + */ uint8 public constant CODE_ADDRESS_FROM_IS_BLACKLISTED = 36; + /** + * @notice Restriction code returned when the recipient is blacklisted. + */ uint8 public constant CODE_ADDRESS_TO_IS_BLACKLISTED = 37; + /** + * @notice Restriction code returned when the spender is blacklisted. + */ uint8 public constant CODE_ADDRESS_SPENDER_IS_BLACKLISTED = 38; error RuleBlacklist_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); diff --git a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol index 59fc65f..6461803 100644 --- a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol +++ b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol @@ -4,20 +4,44 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "../../invariant/RuleSharedInvariantStorage.sol"; +/** + * @title RuleWhitelistInvariantStorage — constants and events for the whitelist rule. + */ abstract contract RuleWhitelistInvariantStorage is RuleSharedInvariantStorage { /* ============ String message ============ */ + /** + * @notice Restriction message returned when the sender is not whitelisted. + */ string constant TEXT_ADDRESS_FROM_NOT_WHITELISTED = "The sender is not in the whitelist"; + /** + * @notice Restriction message returned when the recipient is not whitelisted. + */ string constant TEXT_ADDRESS_TO_NOT_WHITELISTED = "The recipient is not in the whitelist"; + /** + * @notice Restriction 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 returned when the sender is not whitelisted. + */ uint8 public constant CODE_ADDRESS_FROM_NOT_WHITELISTED = 21; + /** + * @notice Restriction code returned when the recipient is not whitelisted. + */ uint8 public constant CODE_ADDRESS_TO_NOT_WHITELISTED = 22; + /** + * @notice Restriction code returned when the spender is not whitelisted. + */ uint8 public constant CODE_ADDRESS_SPENDER_NOT_WHITELISTED = 23; /* ============ Events ============ */ - /// @dev Emitted when the `checkSpender` flag is updated. + /** + * @notice Emitted when the `checkSpender` flag is updated. + * @param newValue New value of the `checkSpender` flag. + */ event CheckSpenderUpdated(bool newValue); error RuleWhitelist_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); diff --git a/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol b/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol index 2f5890a..8c1e69d 100644 --- a/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol +++ b/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol @@ -20,16 +20,26 @@ abstract contract RuleERC2980Internal { STATE VARIABLES //////////////////////////////////////////////////////////////*/ - /// @dev Addresses allowed to receive tokens. + /** + * @dev Addresses allowed to receive tokens. + */ EnumerableSet.AddressSet private _whitelist; - /// @dev Addresses completely blocked from sending and receiving tokens. + /** + * @dev Addresses completely blocked from sending and receiving tokens. + */ EnumerableSet.AddressSet private _frozenlist; /*////////////////////////////////////////////////////////////// WHITELIST — INTERNAL //////////////////////////////////////////////////////////////*/ + /** + * @notice Adds multiple addresses to the whitelist, skipping any already present. + * @param addressesToAdd Addresses to add to the whitelist. + * @return added Number of addresses newly added. + * @return skipped Number of addresses that were already whitelisted. + */ function _addWhitelistAddresses(address[] calldata addressesToAdd) internal returns (uint256 added, uint256 skipped) @@ -43,6 +53,12 @@ abstract contract RuleERC2980Internal { } } + /** + * @notice Removes multiple addresses from the whitelist, skipping any that are absent. + * @param addressesToRemove Addresses to remove from the whitelist. + * @return removed Number of addresses actually removed. + * @return skipped Number of addresses that were not whitelisted. + */ function _removeWhitelistAddresses(address[] calldata addressesToRemove) internal returns (uint256 removed, uint256 skipped) @@ -56,26 +72,32 @@ abstract contract RuleERC2980Internal { } } + /** + * @notice Adds a single address to the whitelist. + * @param targetAddress Address to add to the whitelist. + */ function _addWhitelistAddress(address targetAddress) internal virtual { _whitelist.add(targetAddress); } + /** + * @notice Removes a single address from the whitelist. + * @param targetAddress Address to remove from the whitelist. + */ function _removeWhitelistAddress(address targetAddress) internal virtual { _whitelist.remove(targetAddress); } - function _isWhitelisted(address targetAddress) internal view virtual returns (bool) { - return _whitelist.contains(targetAddress); - } - - function _whitelistCount() internal view virtual returns (uint256) { - return _whitelist.length(); - } - /*////////////////////////////////////////////////////////////// FROZENLIST — INTERNAL //////////////////////////////////////////////////////////////*/ + /** + * @notice Adds multiple addresses to the frozenlist, skipping any already present. + * @param addressesToAdd Addresses to add to the frozenlist. + * @return added Number of addresses newly added. + * @return skipped Number of addresses that were already frozen. + */ function _addFrozenlistAddresses(address[] calldata addressesToAdd) internal returns (uint256 added, uint256 skipped) @@ -89,6 +111,12 @@ abstract contract RuleERC2980Internal { } } + /** + * @notice Removes multiple addresses from the frozenlist, skipping any that are absent. + * @param addressesToRemove Addresses to remove from the frozenlist. + * @return removed Number of addresses actually removed. + * @return skipped Number of addresses that were not frozen. + */ function _removeFrozenlistAddresses(address[] calldata addressesToRemove) internal returns (uint256 removed, uint256 skipped) @@ -102,18 +130,56 @@ abstract contract RuleERC2980Internal { } } + /** + * @notice Adds a single address to the frozenlist. + * @param targetAddress Address to add to the frozenlist. + */ function _addFrozenlistAddress(address targetAddress) internal virtual { _frozenlist.add(targetAddress); } + /** + * @notice Removes a single address from the frozenlist. + * @param targetAddress Address to remove from the frozenlist. + */ function _removeFrozenlistAddress(address targetAddress) internal virtual { _frozenlist.remove(targetAddress); } + /*////////////////////////////////////////////////////////////// + VIEW — INTERNAL + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns whether an address is whitelisted. + * @param targetAddress Address to check. + * @return True if the address is whitelisted. + */ + function _isWhitelisted(address targetAddress) internal view virtual returns (bool) { + return _whitelist.contains(targetAddress); + } + + /** + * @notice Returns the number of whitelisted addresses. + * @return The count of whitelisted addresses. + */ + function _whitelistCount() internal view virtual returns (uint256) { + return _whitelist.length(); + } + + /** + * @notice Returns whether an address is frozen. + * @param targetAddress Address to check. + * @return True if the address is frozen. + */ function _isFrozen(address targetAddress) internal view virtual returns (bool) { return _frozenlist.contains(targetAddress); } + /** + * @notice Returns the number of frozen addresses. + * @return The count of frozen addresses. + */ function _frozenlistCount() internal view virtual returns (uint256) { return _frozenlist.length(); } diff --git a/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol b/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol index 0de56e5..65a9fe7 100644 --- a/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol +++ b/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol @@ -3,43 +3,106 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "../../invariant/RuleSharedInvariantStorage.sol"; +/** + * @title RuleERC2980InvariantStorage — constants, roles, events and errors for the ERC-2980 rule. + */ abstract contract RuleERC2980InvariantStorage is RuleSharedInvariantStorage { /* ============ String message ============ */ + /** + * @notice Restriction message returned when the sender is frozen. + */ string constant TEXT_ADDRESS_FROM_IS_FROZEN = "The sender address is frozen"; + /** + * @notice Restriction message returned when the recipient is frozen. + */ string constant TEXT_ADDRESS_TO_IS_FROZEN = "The recipient address is frozen"; + /** + * @notice Restriction message returned when the spender is frozen. + */ string constant TEXT_ADDRESS_SPENDER_IS_FROZEN = "The spender address is frozen"; + /** + * @notice Restriction message returned when the recipient is not whitelisted. + */ string constant TEXT_ADDRESS_TO_NOT_WHITELISTED = "The recipient is not in the whitelist"; /* ============ Code ============ */ // It is very important that each rule uses a unique code + /** + * @notice Restriction code returned when the sender is frozen. + */ uint8 public constant CODE_ADDRESS_FROM_IS_FROZEN = 60; + /** + * @notice Restriction code returned when the recipient is frozen. + */ uint8 public constant CODE_ADDRESS_TO_IS_FROZEN = 61; + /** + * @notice Restriction code returned when the spender is frozen. + */ uint8 public constant CODE_ADDRESS_SPENDER_IS_FROZEN = 62; + /** + * @notice Restriction code returned when the recipient is not whitelisted. + */ uint8 public constant CODE_ADDRESS_TO_NOT_WHITELISTED = 63; /* ============ Roles ============ */ + /** + * @notice Role allowed to add addresses to the whitelist. + */ bytes32 public constant WHITELIST_ADD_ROLE = keccak256("WHITELIST_ADD_ROLE"); + /** + * @notice Role allowed to remove addresses from the whitelist. + */ bytes32 public constant WHITELIST_REMOVE_ROLE = keccak256("WHITELIST_REMOVE_ROLE"); + /** + * @notice Role allowed to add addresses to the frozenlist. + */ bytes32 public constant FROZENLIST_ADD_ROLE = keccak256("FROZENLIST_ADD_ROLE"); + /** + * @notice Role allowed to remove addresses from the frozenlist. + */ bytes32 public constant FROZENLIST_REMOVE_ROLE = keccak256("FROZENLIST_REMOVE_ROLE"); /* ============ Events ============ */ - /// @notice Emitted when multiple addresses are added to the whitelist. + /** + * @notice Emitted when multiple addresses are added to the whitelist. + * @param targetAddresses Addresses added to the whitelist. + */ event AddWhitelistAddresses(address[] targetAddresses); - /// @notice Emitted when multiple addresses are removed from the whitelist. + /** + * @notice Emitted when multiple addresses are removed from the whitelist. + * @param targetAddresses Addresses removed from the whitelist. + */ event RemoveWhitelistAddresses(address[] targetAddresses); - /// @notice Emitted when a single address is added to the whitelist. + /** + * @notice Emitted when a single address is added to the whitelist. + * @param targetAddress Address added to the whitelist. + */ event AddWhitelistAddress(address indexed targetAddress); - /// @notice Emitted when a single address is removed from the whitelist. + /** + * @notice Emitted when a single address is removed from the whitelist. + * @param targetAddress Address removed from the whitelist. + */ event RemoveWhitelistAddress(address indexed targetAddress); - /// @notice Emitted when multiple addresses are added to the frozenlist. + /** + * @notice Emitted when multiple addresses are added to the frozenlist. + * @param targetAddresses Addresses added to the frozenlist. + */ event AddFrozenlistAddresses(address[] targetAddresses); - /// @notice Emitted when multiple addresses are removed from the frozenlist. + /** + * @notice Emitted when multiple addresses are removed from the frozenlist. + * @param targetAddresses Addresses removed from the frozenlist. + */ event RemoveFrozenlistAddresses(address[] targetAddresses); - /// @notice Emitted when a single address is added to the frozenlist. + /** + * @notice Emitted when a single address is added to the frozenlist. + * @param targetAddress Address added to the frozenlist. + */ event AddFrozenlistAddress(address indexed targetAddress); - /// @notice Emitted when a single address is removed from the frozenlist. + /** + * @notice Emitted when a single address is removed from the frozenlist. + * @param targetAddress Address removed from the frozenlist. + */ event RemoveFrozenlistAddress(address indexed targetAddress); /* ============ Custom errors ============ */ diff --git a/src/rules/validation/abstract/base/RuleBlacklistBase.sol b/src/rules/validation/abstract/base/RuleBlacklistBase.sol index e066d95..0323fe4 100644 --- a/src/rules/validation/abstract/base/RuleBlacklistBase.sol +++ b/src/rules/validation/abstract/base/RuleBlacklistBase.sol @@ -19,6 +19,10 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the blacklist rule base. + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + */ constructor(address forwarderIrrevocable) RuleAddressSet(forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// @@ -51,6 +55,9 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IRule + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) public pure @@ -62,6 +69,9 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack || restrictionCode == CODE_ADDRESS_SPENDER_IS_BLACKLISTED; } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -80,6 +90,9 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack } } + /** + * @inheritdoc RuleTransferValidation + */ function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { return RuleTransferValidation.supportsInterface(interfaceId); } @@ -88,6 +101,12 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Detects whether a direct transfer is restricted by the blacklist. + * @param from The sender address. + * @param to The recipient address. + * @return The restriction code, or TRANSFER_OK if neither party is blacklisted. + */ function _detectTransferRestriction( address from, address to, @@ -106,6 +125,14 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Detects whether a delegated transfer is restricted by the blacklist. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + * @return The restriction code, or TRANSFER_OK if no party is blacklisted. + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -118,6 +145,12 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack return _detectTransferRestriction(from, to, value); } + /** + * @notice Reverts if a direct transfer is blocked by the blacklist. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -126,6 +159,13 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack ); } + /** + * @notice Reverts if a delegated transfer is blocked by the blacklist. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( diff --git a/src/rules/validation/abstract/base/RuleERC2980Base.sol b/src/rules/validation/abstract/base/RuleERC2980Base.sol index 1061824..6daa858 100644 --- a/src/rules/validation/abstract/base/RuleERC2980Base.sol +++ b/src/rules/validation/abstract/base/RuleERC2980Base.sol @@ -38,6 +38,11 @@ abstract contract RuleERC2980Base is CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Initializes the rule, optionally whitelisting the zero address to allow burns. + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address, set permanently at deployment. + * @param allowBurn When true, whitelists `address(0)` so tokens can be burned (sent to the zero address). + */ constructor(address forwarderIrrevocable, bool allowBurn) MetaTxModuleStandalone(forwarderIrrevocable) { if (allowBurn) { _addWhitelistAddress(address(0)); @@ -69,11 +74,6 @@ abstract contract RuleERC2980Base is _; } - function _authorizeWhitelistAdd() internal view virtual; - function _authorizeWhitelistRemove() internal view virtual; - function _authorizeFrozenlistAdd() internal view virtual; - function _authorizeFrozenlistRemove() internal view virtual; - /*////////////////////////////////////////////////////////////// WHITELIST MANAGEMENT //////////////////////////////////////////////////////////////*/ @@ -81,6 +81,7 @@ abstract contract RuleERC2980Base is /** * @notice Adds multiple addresses to the whitelist. * @dev Does not revert if an address is already listed. + * @param targetAddresses Addresses to add to the whitelist. */ function addWhitelistAddresses(address[] calldata targetAddresses) public onlyWhitelistAdd { _addWhitelistAddresses(targetAddresses); @@ -90,6 +91,7 @@ abstract contract RuleERC2980Base is /** * @notice Removes multiple addresses from the whitelist. * @dev Does not revert if an address is not listed. + * @param targetAddresses Addresses to remove from the whitelist. */ function removeWhitelistAddresses(address[] calldata targetAddresses) public onlyWhitelistRemove { _removeWhitelistAddresses(targetAddresses); @@ -103,6 +105,7 @@ abstract contract RuleERC2980Base is * Deviation from ERC-2980 `Whitelistable` example interface: the spec's `addAddressToWhitelist` * returns `false` on duplicates instead of reverting. This implementation follows the codebase * convention of reverting on invalid single-item operations. + * @param targetAddress Address to add to the whitelist. */ function addWhitelistAddress(address targetAddress) public onlyWhitelistAdd { require(!_isWhitelisted(targetAddress), RuleERC2980_AddressAlreadyListed()); @@ -117,6 +120,7 @@ abstract contract RuleERC2980Base is * Deviation from ERC-2980 `Whitelistable` example interface: the spec's `removeAddressFromWhitelist` * returns `false` when not found instead of reverting. This implementation follows the codebase * convention of reverting on invalid single-item operations. + * @param targetAddress Address to remove from the whitelist. */ function removeWhitelistAddress(address targetAddress) public onlyWhitelistRemove { require(_isWhitelisted(targetAddress), RuleERC2980_AddressNotFound()); @@ -131,6 +135,7 @@ abstract contract RuleERC2980Base is /** * @notice Adds multiple addresses to the frozenlist. * @dev Does not revert if an address is already listed. + * @param targetAddresses Addresses to add to the frozenlist. */ function addFrozenlistAddresses(address[] calldata targetAddresses) public onlyFrozenlistAdd { _addFrozenlistAddresses(targetAddresses); @@ -140,6 +145,7 @@ abstract contract RuleERC2980Base is /** * @notice Removes multiple addresses from the frozenlist. * @dev Does not revert if an address is not listed. + * @param targetAddresses Addresses to remove from the frozenlist. */ function removeFrozenlistAddresses(address[] calldata targetAddresses) public onlyFrozenlistRemove { _removeFrozenlistAddresses(targetAddresses); @@ -153,6 +159,7 @@ abstract contract RuleERC2980Base is * Deviation from ERC-2980 `Freezable` example interface: the spec's `addAddressToFrozenlist` * returns `false` on duplicates instead of reverting. This implementation follows the codebase * convention of reverting on invalid single-item operations. + * @param targetAddress Address to add to the frozenlist. */ function addFrozenlistAddress(address targetAddress) public onlyFrozenlistAdd { require(!_isFrozen(targetAddress), RuleERC2980_AddressAlreadyListed()); @@ -167,6 +174,7 @@ abstract contract RuleERC2980Base is * Deviation from ERC-2980 `Freezable` example interface: the spec's `removeAddressFromFrozenlist` * returns `false` when not found instead of reverting. This implementation follows the codebase * convention of reverting on invalid single-item operations. + * @param targetAddress Address to remove from the frozenlist. */ function removeFrozenlistAddress(address targetAddress) public onlyFrozenlistRemove { require(_isFrozen(targetAddress), RuleERC2980_AddressNotFound()); @@ -178,6 +186,9 @@ abstract contract RuleERC2980Base is PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc IERC3643IComplianceContract + */ function transferred(address from, address to, uint256 value) public view @@ -187,6 +198,9 @@ abstract contract RuleERC2980Base is _transferred(from, to, value); } + /** + * @inheritdoc IRuleEngine + */ function transferred(address spender, address from, address to, uint256 value) public view @@ -196,6 +210,9 @@ abstract contract RuleERC2980Base is _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IRule + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) public pure @@ -207,6 +224,9 @@ abstract contract RuleERC2980Base is || restrictionCode == CODE_ADDRESS_SPENDER_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED; } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -227,12 +247,16 @@ abstract contract RuleERC2980Base is } } + /** + * @inheritdoc RuleTransferValidation + */ function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { return RuleTransferValidation.supportsInterface(interfaceId); } /** * @notice Returns the number of whitelisted addresses. + * @return The count of addresses currently in the whitelist. */ function whitelistAddressCount() public view returns (uint256) { return _whitelistCount(); @@ -240,6 +264,8 @@ abstract contract RuleERC2980Base is /** * @notice Returns true if the address is in the whitelist. + * @param targetAddress Address to check. + * @return True if the address is whitelisted. */ function isWhitelisted(address targetAddress) public view returns (bool) { return _isWhitelisted(targetAddress); @@ -247,6 +273,8 @@ abstract contract RuleERC2980Base is /** * @notice ERC-2980 getter: returns true if the address is whitelisted. + * @param _operator Address to check. + * @return True if the address is whitelisted. */ function whitelist(address _operator) public view virtual override(IERC2980) returns (bool) { return _isWhitelisted(_operator); @@ -256,6 +284,8 @@ abstract contract RuleERC2980Base is * @notice Returns true if the address is whitelisted (identity-verified). * @dev Reflects whitelist membership only. Frozen status is intentionally excluded: * freezing is a temporary enforcement action and does not revoke identity verification. + * @param targetAddress Address to check. + * @return True if the address is whitelisted. */ function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { return _isWhitelisted(targetAddress); @@ -263,6 +293,8 @@ abstract contract RuleERC2980Base is /** * @notice Checks multiple addresses for whitelist membership. + * @param targetAddresses Addresses to check. + * @return results Array of booleans, true where the corresponding address is whitelisted. */ function areWhitelisted(address[] memory targetAddresses) public view returns (bool[] memory results) { results = new bool[](targetAddresses.length); @@ -273,6 +305,7 @@ abstract contract RuleERC2980Base is /** * @notice Returns the number of frozen addresses. + * @return The count of addresses currently in the frozenlist. */ function frozenlistAddressCount() public view returns (uint256) { return _frozenlistCount(); @@ -280,6 +313,8 @@ abstract contract RuleERC2980Base is /** * @notice Returns true if the address is in the frozenlist. + * @param targetAddress Address to check. + * @return True if the address is frozen. */ function isFrozen(address targetAddress) public view returns (bool) { return _isFrozen(targetAddress); @@ -287,6 +322,8 @@ abstract contract RuleERC2980Base is /** * @notice ERC-2980 getter: returns true if the address is frozen. + * @param _operator Address to check. + * @return True if the address is frozen. */ function frozenlist(address _operator) public view virtual override(IERC2980) returns (bool) { return _isFrozen(_operator); @@ -294,6 +331,8 @@ abstract contract RuleERC2980Base is /** * @notice Checks multiple addresses for frozenlist membership. + * @param targetAddresses Addresses to check. + * @return results Array of booleans, true where the corresponding address is frozen. */ function areFrozen(address[] memory targetAddresses) public view returns (bool[] memory results) { results = new bool[](targetAddresses.length); @@ -306,6 +345,26 @@ abstract contract RuleERC2980Base is INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Authorization hook invoked before adding addresses to the whitelist. + */ + function _authorizeWhitelistAdd() internal view virtual; + /** + * @notice Authorization hook invoked before removing addresses from the whitelist. + */ + function _authorizeWhitelistRemove() internal view virtual; + /** + * @notice Authorization hook invoked before adding addresses to the frozenlist. + */ + function _authorizeFrozenlistAdd() internal view virtual; + /** + * @notice Authorization hook invoked before removing addresses from the frozenlist. + */ + function _authorizeFrozenlistRemove() internal view virtual; + + /** + * @inheritdoc RuleTransferValidation + */ function _detectTransferRestriction( address from, address to, @@ -330,6 +389,9 @@ abstract contract RuleERC2980Base is return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @inheritdoc RuleTransferValidation + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -343,6 +405,9 @@ abstract contract RuleERC2980Base is return _detectTransferRestriction(from, to, value); } + /** + * @inheritdoc RuleNFTAdapter + */ function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -351,6 +416,9 @@ abstract contract RuleERC2980Base is ); } + /** + * @inheritdoc RuleNFTAdapter + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( @@ -359,14 +427,23 @@ abstract contract RuleERC2980Base is ); } + /** + * @inheritdoc ERC2771Context + */ function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { return ERC2771Context._msgSender(); } + /** + * @inheritdoc ERC2771Context + */ function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { return ERC2771Context._msgData(); } + /** + * @inheritdoc ERC2771Context + */ function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); } diff --git a/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol b/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol index cbb2b95..b477357 100644 --- a/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol +++ b/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol @@ -14,12 +14,19 @@ import {IIdentityRegistryVerified} from "../../../interfaces/IIdentityRegistry.s * @dev Burns (to == address(0)) are allowed even if the sender is not verified. */ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegistryInvariantStorage { + /** + * @notice The ERC-3643 Identity Registry consulted to verify transfer participants; the zero address disables checks. + */ IIdentityRegistryVerified public identityRegistry; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Initializes the rule with an optional identity registry. + * @param identityRegistry_ Identity registry address; when the zero address, the registry is left unset (checks disabled). + */ constructor(address identityRegistry_) { if (identityRegistry_ != address(0)) { identityRegistry = IIdentityRegistryVerified(identityRegistry_); @@ -35,12 +42,15 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist _; } - function _authorizeIdentityRegistryManager() internal view virtual; - /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns whether this rule can produce the given restriction code. + * @param restrictionCode Restriction code to test. + * @return True if `restrictionCode` is one of this rule's identity-verification codes. + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { return restrictionCode == CODE_ADDRESS_FROM_NOT_VERIFIED || restrictionCode == CODE_ADDRESS_TO_NOT_VERIFIED || restrictionCode == CODE_ADDRESS_SPENDER_NOT_VERIFIED; @@ -50,25 +60,41 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the identity registry consulted during transfer checks. + * @param newRegistry New identity registry address; must not be the zero address. + */ function setIdentityRegistry(address newRegistry) public onlyIdentityRegistryManager { require(newRegistry != address(0), RuleIdentityRegistry_RegistryAddressZeroNotAllowed()); identityRegistry = IIdentityRegistryVerified(newRegistry); emit IdentityRegistryUpdated(newRegistry); } + /** + * @notice Clears the identity registry, disabling identity checks (all transfers pass this rule). + */ function clearIdentityRegistry() public onlyIdentityRegistryManager { identityRegistry = IIdentityRegistryVerified(address(0)); emit IdentityRegistryUpdated(address(0)); } + /** + * @inheritdoc IERC3643IComplianceContract + */ function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { _transferred(from, to, value); } + /** + * @inheritdoc IRuleEngine + */ function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -89,6 +115,17 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Authorization hook invoked before updating or clearing the identity registry. + */ + function _authorizeIdentityRegistryManager() internal view virtual; + + /** + * @notice Detects the restriction code for a direct transfer, verifying `from` and `to` against the registry. + * @param from Sender address; must be verified unless it is the zero address (mint). + * @param to Recipient address; must be verified unless it is the zero address (burn). + * @return The applicable restriction code, or TRANSFER_OK when no restriction applies. + */ function _detectTransferRestriction( address from, address to, @@ -115,6 +152,14 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Detects the restriction code for a `transferFrom`, verifying `spender` and delegating to the direct check. + * @param spender Approved spender initiating the transfer; must be verified unless it is the zero address. + * @param from Sender address, forwarded to the direct transfer check. + * @param to Recipient address, forwarded to the direct transfer check. + * @param value Transfer amount, forwarded to the direct transfer check. + * @return The applicable restriction code, or TRANSFER_OK when no restriction applies. + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -133,6 +178,9 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist return _detectTransferRestriction(from, to, value); } + /** + * @inheritdoc RuleNFTAdapter + */ function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -141,6 +189,9 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist ); } + /** + * @inheritdoc RuleNFTAdapter + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( @@ -148,5 +199,4 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist RuleIdentityRegistry_InvalidTransferFrom(address(this), spender, from, to, value, code) ); } - } diff --git a/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol b/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol index 8aebb27..9120715 100644 --- a/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol +++ b/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol @@ -13,14 +13,24 @@ import {RuleTransferValidation} from "../core/RuleTransferValidation.sol"; * @notice Restricts minting so that total supply never exceeds a maximum value. */ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotalSupplyInvariantStorage { - /// @dev tokenContract is trusted to return a correct totalSupply. + /** + * @dev tokenContract is trusted to return a correct totalSupply. + */ ITotalSupply public tokenContract; + /** + * @notice Maximum total supply; minting that would exceed this value is rejected. + */ uint256 public maxTotalSupply; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Initializes the rule with the token to observe and the supply cap. + * @param tokenContract_ Address of the token whose `totalSupply` is checked; must not be the zero address. + * @param maxTotalSupply_ Maximum total supply allowed. + */ constructor(address tokenContract_, uint256 maxTotalSupply_) { require(tokenContract_ != address(0), RuleMaxTotalSupply_TokenAddressZeroNotAllowed()); tokenContract = ITotalSupply(tokenContract_); @@ -31,6 +41,11 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns whether this rule can produce the given restriction code. + * @param restrictionCode Restriction code to test. + * @return True if `restrictionCode` is the max-total-supply-exceeded code. + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { return restrictionCode == CODE_MAX_TOTAL_SUPPLY_EXCEEDED; } @@ -39,25 +54,42 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Updates the maximum total supply. + * @param newMaxTotalSupply New maximum total supply value. + */ function setMaxTotalSupply(uint256 newMaxTotalSupply) public onlyMaxTotalSupplyManager { maxTotalSupply = newMaxTotalSupply; emit MaxTotalSupplyUpdated(newMaxTotalSupply); } + /** + * @notice Updates the token contract whose total supply is checked. + * @param newTokenContract New token contract address; must not be the zero address. + */ function setTokenContract(address newTokenContract) public onlyMaxTotalSupplyManager { require(newTokenContract != address(0), RuleMaxTotalSupply_TokenAddressZeroNotAllowed()); tokenContract = ITotalSupply(newTokenContract); emit TokenContractUpdated(newTokenContract); } + /** + * @inheritdoc IERC3643IComplianceContract + */ function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { _transferred(from, to, value); } + /** + * @inheritdoc IRuleEngine + */ function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -79,12 +111,18 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal _; } + /** + * @notice Authorization hook invoked before updating the max total supply or token contract. + */ function _authorizeMaxTotalSupplyManager() internal view virtual; /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc RuleTransferValidation + */ function _detectTransferRestriction( address from, address, @@ -105,6 +143,9 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @inheritdoc RuleTransferValidation + */ function _detectTransferRestrictionFrom(address, address from, address to, uint256 value) internal view @@ -114,6 +155,12 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal return _detectTransferRestriction(from, to, value); } + /** + * @notice Enforces the max-total-supply restriction for a direct transfer, reverting on violation. + * @param from Sender address; the zero address denotes a mint whose supply is checked. + * @param to Recipient address. + * @param value Transfer amount. + */ function _transferred(address from, address to, uint256 value) internal view virtual { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -122,6 +169,13 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal ); } + /** + * @notice Enforces the max-total-supply restriction for a `transferFrom`, reverting on violation. + * @param spender Approved spender initiating the transfer. + * @param from Sender address; the zero address denotes a mint whose supply is checked. + * @param to Recipient address. + * @param value Transfer amount. + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( diff --git a/src/rules/validation/abstract/base/RuleSanctionsListBase.sol b/src/rules/validation/abstract/base/RuleSanctionsListBase.sol index 586b949..fa90c34 100644 --- a/src/rules/validation/abstract/base/RuleSanctionsListBase.sol +++ b/src/rules/validation/abstract/base/RuleSanctionsListBase.sol @@ -15,12 +15,20 @@ import {IRule} from "RuleEngine/interfaces/IRule.sol"; * @notice Compliance rule enforcing sanctions-screening for token transfers. */ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapter, RuleSanctionsListInvariantStorage { + /** + * @notice The sanctions oracle consulted on each transfer; unset disables screening. + */ ISanctionsList public sanctionsList; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the sanctions-list rule base and optionally sets the oracle. + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + * @param sanctionContractOracle_ Initial sanctions oracle; skipped when the zero address. + */ constructor(address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) MetaTxModuleStandalone(forwarderIrrevocable) { @@ -33,6 +41,9 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc IRule + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { return restrictionCode == CODE_ADDRESS_FROM_IS_SANCTIONED || restrictionCode == CODE_ADDRESS_TO_IS_SANCTIONED || restrictionCode == CODE_ADDRESS_SPENDER_IS_SANCTIONED; @@ -42,23 +53,41 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the sanctions oracle consulted on each transfer. + * @dev Restricted to the sanction-list manager; reverts on the zero address. + * @param sanctionContractOracle_ The new sanctions oracle address. + */ function setSanctionListOracle(ISanctionsList sanctionContractOracle_) public virtual onlySanctionListManager { require(address(sanctionContractOracle_) != address(0), RuleSanctionsList_OracleAddressZeroNotAllowed()); _setSanctionListOracle(sanctionContractOracle_); } + /** + * @notice Clears the sanctions oracle, disabling sanctions screening. + * @dev Restricted to the sanction-list manager. + */ function clearSanctionListOracle() public virtual onlySanctionListManager { _setSanctionListOracle(ISanctionsList(address(0))); } + /** + * @inheritdoc IERC3643IComplianceContract + */ function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { _transferred(from, to, value); } + /** + * @inheritdoc IRuleEngine + */ function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -84,12 +113,31 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte _; } - function _authorizeSanctionListManager() internal view virtual; - /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Updates the stored sanctions oracle and emits {SetSanctionListOracle}. + * @param sanctionContractOracle_ The new sanctions oracle address (may be zero to disable). + */ + function _setSanctionListOracle(ISanctionsList sanctionContractOracle_) internal virtual { + sanctionsList = sanctionContractOracle_; + emit SetSanctionListOracle(sanctionContractOracle_); + } + + /** + * @notice Authorizes the caller as sanction-list manager; reverts otherwise. + * @dev Implemented by concrete subclasses with the desired access-control policy. + */ + function _authorizeSanctionListManager() internal view virtual; + + /** + * @notice Detects whether a direct transfer is restricted by the sanctions oracle. + * @param from The sender address. + * @param to The recipient address. + * @return The restriction code, or TRANSFER_OK when no party is sanctioned. + */ function _detectTransferRestriction( address from, address to, @@ -110,6 +158,14 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte return uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Detects whether a delegated transfer is restricted by the sanctions oracle. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + * @return The restriction code, or TRANSFER_OK when no party is sanctioned. + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -126,6 +182,12 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte return uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Reverts if a direct transfer is blocked by the sanctions oracle. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -134,6 +196,13 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte ); } + /** + * @notice Reverts if a delegated transfer is blocked by the sanctions oracle. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( @@ -141,9 +210,4 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte RuleSanctionsList_InvalidTransferFrom(address(this), spender, from, to, value, code) ); } - - function _setSanctionListOracle(ISanctionsList sanctionContractOracle_) internal virtual { - sanctionsList = sanctionContractOracle_; - emit SetSanctionListOracle(sanctionContractOracle_); - } } diff --git a/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol b/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol index cb37bdd..118590f 100644 --- a/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol +++ b/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol @@ -18,12 +18,21 @@ abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, Ru CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the spender-whitelist rule base. + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + */ constructor(address forwarderIrrevocable) RuleAddressSet(forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns whether this rule can emit the given restriction code. + * @param restrictionCode The restriction code to check. + * @return True if the code is produced by this rule. + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { return restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED; } @@ -37,10 +46,16 @@ abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, Ru */ function transferred(address, address, uint256) public view override(IERC3643IComplianceContract) {} + /** + * @inheritdoc IRuleEngine + */ function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -57,27 +72,50 @@ abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, Ru INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Direct transfers are always accepted by this rule. + * @return Always TRANSFER_OK. + */ function _detectTransferRestriction(address, address, uint256) internal pure virtual override returns (uint8) { return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } - function _detectTransferRestrictionFrom(address spender, address, address, uint256) + /** + * @notice Detects whether a delegated transfer is blocked because the spender is not whitelisted. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @return The restriction code, or TRANSFER_OK when allowed. + */ + function _detectTransferRestrictionFrom(address spender, address from, address to, uint256) internal view virtual override returns (uint8) { - if (spender != address(0) && !_isAddressListed(spender)) { + // Mint (from == address(0)) and burn (to == address(0)) are exempt from the spender check: + // the minter/burner acts on its own authority, not as a delegated ERC-20 spender. + if (from != address(0) && to != address(0) && !_isAddressListed(spender)) { return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; } return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice No-op: regular transfers are intentionally ignored by this rule. + */ function _transferred(address, address, uint256) internal view virtual override { // no-op: regular transfers are intentionally ignored by this rule } + /** + * @notice Reverts if a delegated transfer is blocked because the spender is not whitelisted. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( diff --git a/src/rules/validation/abstract/base/RuleWhitelistBase.sol b/src/rules/validation/abstract/base/RuleWhitelistBase.sol index 6cdb6cd..d094eb5 100644 --- a/src/rules/validation/abstract/base/RuleWhitelistBase.sol +++ b/src/rules/validation/abstract/base/RuleWhitelistBase.sol @@ -15,6 +15,12 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the whitelist rule base. + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + * @param checkSpender_ Whether to also verify the spender on delegated transfers. + * @param allowMintBurn When true, whitelists the zero address so mint/burn is permitted. + */ constructor(address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) RuleAddressSet(forwarderIrrevocable) { @@ -29,11 +35,19 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Enables or disables spender verification on delegated transfers. + * @dev Restricted to the check-spender manager; emits {CheckSpenderUpdated}. + * @param value The new state of the `checkSpender` flag. + */ function setCheckSpender(bool value) public virtual onlyCheckSpenderManager { _setCheckSpender(value); emit CheckSpenderUpdated(value); } + /** + * @inheritdoc IIdentityRegistryVerified + */ function isVerified(address targetAddress) public view @@ -44,6 +58,9 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde isListed = _isAddressListed(targetAddress); } + /** + * @inheritdoc RuleTransferValidation + */ function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { return RuleTransferValidation.supportsInterface(interfaceId); } @@ -57,12 +74,30 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde _; } - function _authorizeCheckSpenderManager() internal view virtual; - /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Internal helper to update the `checkSpender` flag. + * @param value New flag value. + */ + function _setCheckSpender(bool value) internal virtual { + checkSpender = value; + } + + /** + * @notice Authorizes the caller as check-spender manager; reverts otherwise. + * @dev Implemented by concrete subclasses with the desired access-control policy. + */ + function _authorizeCheckSpenderManager() internal view virtual; + + /** + * @notice Detects whether a direct transfer is restricted by the whitelist. + * @param from The sender address. + * @param to The recipient address. + * @return The restriction code, or TRANSFER_OK when both parties are whitelisted. + */ function _detectTransferRestriction( address from, address to, @@ -82,6 +117,14 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde return uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Detects whether a delegated transfer is restricted by the whitelist. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + * @return The restriction code, or TRANSFER_OK when allowed. + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -89,13 +132,11 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde override returns (uint8) { - if (checkSpender && !isAddressListed(spender)) { + // Mint (from == address(0)) and burn (to == address(0)) are exempt from the spender check: + // the minter/burner acts on its own authority, not as a delegated ERC-20 spender. + if (checkSpender && from != address(0) && to != address(0) && !isAddressListed(spender)) { return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; } return _detectTransferRestriction(from, to, value); } - - function _setCheckSpender(bool value) internal virtual { - checkSpender = value; - } } diff --git a/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol b/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol index 0e3d188..50f9653 100644 --- a/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol +++ b/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol @@ -26,7 +26,9 @@ abstract contract RuleWhitelistWrapperBase is CONSTRUCTOR //////////////////////////////////////////////////////////////*/ /** + * @notice Deploys the whitelist wrapper base. * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + * @param checkSpender_ Whether to also verify the spender on delegated transfers. */ constructor(address forwarderIrrevocable, bool checkSpender_) MetaTxModuleStandalone(forwarderIrrevocable) { checkSpender = checkSpender_; @@ -41,8 +43,6 @@ abstract contract RuleWhitelistWrapperBase is _; } - function _authorizeCheckSpenderManager() internal virtual; - /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -60,6 +60,9 @@ abstract contract RuleWhitelistWrapperBase is emit CheckSpenderUpdated(value); } + /** + * @inheritdoc RuleTransferValidation + */ function supportsInterface(bytes4 interfaceId) public view @@ -73,6 +76,8 @@ abstract contract RuleWhitelistWrapperBase is /** * @notice Returns true if the address is listed in at least one child whitelist rule. * @dev Delegates to the same child-rule scan used by transfer restriction checks. + * @param targetAddress The address to check across all child whitelist rules. + * @return True if the address is listed in at least one child rule. */ function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { address[] memory targets = new address[](1); @@ -85,6 +90,20 @@ abstract contract RuleWhitelistWrapperBase is INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Authorizes the caller as check-spender manager; reverts otherwise. + * @dev Implemented by concrete subclasses with the desired access-control policy. + */ + function _authorizeCheckSpenderManager() internal virtual; + + /** + * @notice Internal helper to update the `checkSpender` flag. + * @param value New flag value. + */ + function _setCheckSpender(bool value) internal virtual { + checkSpender = value; + } + /** * @notice Go through all the whitelist rules to know if a restriction exists on the transfer * @param from the origin address @@ -117,6 +136,14 @@ abstract contract RuleWhitelistWrapperBase is } } + /** + * @notice Go through all the whitelist rules to know if a delegated transfer is restricted. + * @param spender The delegated spender address. + * @param from The origin address. + * @param to The destination address. + * @param value The amount transferred. + * @return The restriction code or REJECTED_CODE_BASE.TRANSFER_OK. + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -124,7 +151,9 @@ abstract contract RuleWhitelistWrapperBase is override returns (uint8) { - if (!checkSpender) { + // Mint (from == address(0)) and burn (to == address(0)) are exempt from the spender check: + // the minter/burner acts on its own authority, not as a delegated ERC-20 spender. + if (!checkSpender || from == address(0) || to == address(0)) { return _detectTransferRestriction(from, to, value); } @@ -148,6 +177,12 @@ abstract contract RuleWhitelistWrapperBase is // ERC-7943 tokenId overloads are provided by {RuleNFTAdapter} via RuleWhitelistShared. + /** + * @notice Reverts if a direct transfer is blocked by any child whitelist rule. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferred(address from, address to, uint256 value) internal view @@ -157,6 +192,13 @@ abstract contract RuleWhitelistWrapperBase is RuleWhitelistShared._transferred(from, to, value); } + /** + * @notice Reverts if a delegated transfer is blocked by any child whitelist rule. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferred(address spender, address from, address to, uint256 value) internal view @@ -204,20 +246,13 @@ abstract contract RuleWhitelistWrapperBase is return result; } - /** - * @notice Internal helper to update the `checkSpender` flag. - * @param value New flag value. - */ - function _setCheckSpender(bool value) internal virtual { - checkSpender = value; - } - /*////////////////////////////////////////////////////////////// ERC-2771 //////////////////////////////////////////////////////////////*/ /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return sender The effective message sender, unwrapped from the meta-transaction if present. */ function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { return ERC2771Context._msgSender(); @@ -225,6 +260,7 @@ abstract contract RuleWhitelistWrapperBase is /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The effective calldata, unwrapped from the meta-transaction if present. */ function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { return ERC2771Context._msgData(); @@ -232,6 +268,7 @@ abstract contract RuleWhitelistWrapperBase is /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The length of the ERC-2771 context suffix appended to calldata. */ function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); diff --git a/src/rules/validation/abstract/core/RuleNFTAdapter.sol b/src/rules/validation/abstract/core/RuleNFTAdapter.sol index db03caa..2e90b30 100644 --- a/src/rules/validation/abstract/core/RuleNFTAdapter.sol +++ b/src/rules/validation/abstract/core/RuleNFTAdapter.sol @@ -17,13 +17,51 @@ import {ITransferContext} from "../../../interfaces/ITransferContext.sol"; * @dev Delegates tokenId overloads to RuleTransferValidation's internal hooks. */ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleComplianceExtend, ITransferContext { + /** + * @notice Selector of the ERC-3643 compliance `transferred` hook. + */ bytes4 internal constant TRANSFERRED_SELECTOR_ERC3643 = IERC3643IComplianceContract.transferred.selector; + /** + * @notice Selector of the RuleEngine `transferred` hook. + */ bytes4 internal constant TRANSFERRED_SELECTOR_RULE_ENGINE = IRuleEngine.transferred.selector; + /** + * @notice Selector of the ERC-7943 `transferred(from,to,tokenId,value)` hook. + */ bytes4 internal constant TRANSFERRED_SELECTOR_ERC7943 = bytes4(keccak256("transferred(address,address,uint256,uint256)")); + /** + * @notice Selector of the ERC-7943 `transferred(spender,from,to,tokenId,value)` hook. + */ bytes4 internal constant TRANSFERRED_SELECTOR_ERC7943_FROM = bytes4(keccak256("transferred(address,address,address,uint256,uint256)")); + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc ITransferContext + */ + function transferred(MultiTokenTransferContext calldata ctx) external virtual override { + if (ctx.sender != address(0) && ctx.sender != ctx.from) { + _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); + } else { + _transferred(ctx.from, ctx.to, ctx.value); + } + } + + /** + * @inheritdoc ITransferContext + */ + function transferred(FungibleTransferContext calldata ctx) external virtual override { + if (ctx.sender != address(0) && ctx.sender != ctx.from) { + _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); + } else { + _transferred(ctx.from, ctx.to, ctx.value); + } + } + /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -31,7 +69,7 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC /** * @inheritdoc IERC7943NonFungibleComplianceExtend */ - function detectTransferRestriction( + function transferred( address from, address to, uint256, @@ -39,88 +77,85 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC uint256 value ) public - view virtual override(IERC7943NonFungibleComplianceExtend) - returns (uint8) { - return _detectTransferRestriction(from, to, value); + _transferred(from, to, value); } /** * @inheritdoc IERC7943NonFungibleComplianceExtend */ - function detectTransferRestrictionFrom( + function transferred( address spender, address from, address to, uint256, /* tokenId */ uint256 value - ) public view virtual override(IERC7943NonFungibleComplianceExtend) returns (uint8) { - return _detectTransferRestrictionFrom(spender, from, to, value); + ) + public + virtual + override(IERC7943NonFungibleComplianceExtend) + { + _transferredFrom(spender, from, to, value); } /** - * @inheritdoc IERC7943NonFungibleCompliance + * @inheritdoc IERC7943NonFungibleComplianceExtend */ - function canTransfer( + function detectTransferRestriction( address from, address to, uint256, /* tokenId */ - uint256 amount + uint256 value ) public view - override(IERC7943NonFungibleCompliance) - returns (bool) + virtual + override(IERC7943NonFungibleComplianceExtend) + returns (uint8) { - return _detectTransferRestriction(from, to, amount) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + return _detectTransferRestriction(from, to, value); } /** * @inheritdoc IERC7943NonFungibleComplianceExtend */ - function canTransferFrom( + function detectTransferRestrictionFrom( address spender, address from, address to, uint256, /* tokenId */ uint256 value - ) - public - view - virtual - override(IERC7943NonFungibleComplianceExtend) - returns (bool) - { - return _detectTransferRestrictionFrom(spender, from, to, value) - == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + ) public view virtual override(IERC7943NonFungibleComplianceExtend) returns (uint8) { + return _detectTransferRestrictionFrom(spender, from, to, value); } /** - * @inheritdoc IERC7943NonFungibleComplianceExtend + * @inheritdoc IERC7943NonFungibleCompliance */ - function transferred( + function canTransfer( address from, address to, uint256, /* tokenId */ - uint256 value + uint256 amount ) public - virtual - override(IERC7943NonFungibleComplianceExtend) + view + override(IERC7943NonFungibleCompliance) + returns (bool) { - _transferred(from, to, value); + return _detectTransferRestriction(from, to, amount) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } /** * @inheritdoc IERC7943NonFungibleComplianceExtend */ - function transferred( + function canTransferFrom( address spender, address from, address to, @@ -129,32 +164,13 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC uint256 value ) public + view virtual override(IERC7943NonFungibleComplianceExtend) + returns (bool) { - _transferredFrom(spender, from, to, value); - } - - /** - * @inheritdoc ITransferContext - */ - function transferred(MultiTokenTransferContext calldata ctx) external virtual override { - if (ctx.sender != address(0) && ctx.sender != ctx.from) { - _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); - } else { - _transferred(ctx.from, ctx.to, ctx.value); - } - } - - /** - * @inheritdoc ITransferContext - */ - function transferred(FungibleTransferContext calldata ctx) external virtual override { - if (ctx.sender != address(0) && ctx.sender != ctx.from) { - _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); - } else { - _transferred(ctx.from, ctx.to, ctx.value); - } + return _detectTransferRestrictionFrom(spender, from, to, value) + == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } /*////////////////////////////////////////////////////////////// @@ -163,11 +179,18 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC /** * @notice Internal hook for post-transfer validation or state updates. + * @param from Address tokens are transferred from. + * @param to Address tokens are transferred to. + * @param value Amount transferred. */ function _transferred(address from, address to, uint256 value) internal virtual; /** * @notice Internal hook for post-transfer validation or state updates (spender-aware). + * @param spender Address executing the transfer on behalf of `from`. + * @param from Address tokens are transferred from. + * @param to Address tokens are transferred to. + * @param value Amount transferred. */ function _transferredFrom(address spender, address from, address to, uint256 value) internal virtual; } diff --git a/src/rules/validation/abstract/core/RuleTransferValidation.sol b/src/rules/validation/abstract/core/RuleTransferValidation.sol index ed0fa18..534067f 100644 --- a/src/rules/validation/abstract/core/RuleTransferValidation.sol +++ b/src/rules/validation/abstract/core/RuleTransferValidation.sol @@ -15,6 +15,10 @@ import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; /* ==== Modules === */ import {VersionModule} from "../../../../modules/VersionModule.sol"; +/** + * @title RuleTransferValidation — base transfer-restriction checks and interface support for rules. + * @notice Exposes ERC-1404 / ERC-3643 / ERC-7551 read views delegating to internal restriction hooks. + */ abstract contract RuleTransferValidation is VersionModule, IERC1404Extend, @@ -83,6 +87,11 @@ abstract contract RuleTransferValidation is == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Returns whether this contract implements the given interface. + * @param interfaceId The ERC-165 interface identifier to query. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID diff --git a/src/rules/validation/abstract/core/RuleWhitelistShared.sol b/src/rules/validation/abstract/core/RuleWhitelistShared.sol index b53fbad..5e3b99f 100644 --- a/src/rules/validation/abstract/core/RuleWhitelistShared.sol +++ b/src/rules/validation/abstract/core/RuleWhitelistShared.sol @@ -101,6 +101,9 @@ abstract contract RuleWhitelistShared is RuleNFTAdapter, RuleWhitelistInvariantS INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc RuleNFTAdapter + */ function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -109,6 +112,9 @@ abstract contract RuleWhitelistShared is RuleNFTAdapter, RuleWhitelistInvariantS ); } + /** + * @inheritdoc RuleNFTAdapter + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( diff --git a/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol index e6adfd1..40c2378 100644 --- a/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol @@ -3,16 +3,41 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "./RuleSharedInvariantStorage.sol"; +/** + * @title RuleIdentityRegistryInvariantStorage — constants and event for the identity-registry rule. + */ abstract contract RuleIdentityRegistryInvariantStorage is RuleSharedInvariantStorage { + /** + * @notice Restriction message returned when the sender is not verified. + */ string constant TEXT_ADDRESS_FROM_NOT_VERIFIED = "The sender is not verified"; + /** + * @notice Restriction message returned when the recipient is not verified. + */ string constant TEXT_ADDRESS_TO_NOT_VERIFIED = "The recipient is not verified"; + /** + * @notice Restriction message returned when the spender is not verified. + */ string constant TEXT_ADDRESS_SPENDER_NOT_VERIFIED = "The spender is not verified"; // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when the sender is not verified. + */ uint8 public constant CODE_ADDRESS_FROM_NOT_VERIFIED = 55; + /** + * @notice Restriction code returned when the recipient is not verified. + */ uint8 public constant CODE_ADDRESS_TO_NOT_VERIFIED = 56; + /** + * @notice Restriction code returned when the spender is not verified. + */ uint8 public constant CODE_ADDRESS_SPENDER_NOT_VERIFIED = 57; + /** + * @notice Emitted when the identity registry address is updated. + * @param newRegistry Address of the newly configured identity registry. + */ event IdentityRegistryUpdated(address indexed newRegistry); error RuleIdentityRegistry_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); diff --git a/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol index 310a1c5..02a8be5 100644 --- a/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol @@ -3,13 +3,30 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "./RuleSharedInvariantStorage.sol"; +/** + * @title RuleMaxTotalSupplyInvariantStorage — constants and events for the max-total-supply rule. + */ abstract contract RuleMaxTotalSupplyInvariantStorage is RuleSharedInvariantStorage { + /** + * @notice Restriction message returned when the max total supply would be exceeded. + */ string constant TEXT_MAX_TOTAL_SUPPLY_EXCEEDED = "Max total supply exceeded"; // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when the max total supply would be exceeded. + */ uint8 public constant CODE_MAX_TOTAL_SUPPLY_EXCEEDED = 50; + /** + * @notice Emitted when the maximum total supply is updated. + * @param newMaxTotalSupply New maximum total supply cap. + */ event MaxTotalSupplyUpdated(uint256 newMaxTotalSupply); + /** + * @notice Emitted when the tracked token contract is updated. + * @param newTokenContract Address of the newly configured token contract. + */ event TokenContractUpdated(address indexed newTokenContract); error RuleMaxTotalSupply_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); diff --git a/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol index 10d8f06..c717f01 100644 --- a/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol @@ -5,22 +5,50 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "./RuleSharedInvariantStorage.sol"; import {ISanctionsList} from "../../../interfaces/ISanctionsList.sol"; +/** + * @title RuleSanctionsListInvariantStorage — constants, role, event and errors for the sanctions-list rule. + */ abstract contract RuleSanctionsListInvariantStorage is RuleSharedInvariantStorage { /* ============ Role ============ */ + /** + * @notice Role allowed to configure the sanctions-list oracle. + */ bytes32 public constant SANCTIONLIST_ROLE = keccak256("SANCTIONLIST_ROLE"); /* ============ String message ============ */ + /** + * @notice Restriction message returned when the sender is sanctioned. + */ string constant TEXT_ADDRESS_FROM_IS_SANCTIONED = "The sender is sanctioned"; + /** + * @notice Restriction message returned when the recipient is sanctioned. + */ string constant TEXT_ADDRESS_TO_IS_SANCTIONED = "The recipient is sanctioned"; + /** + * @notice Restriction message returned when the spender is sanctioned. + */ string constant TEXT_ADDRESS_SPENDER_IS_SANCTIONED = "The spender is sanctioned"; /* ============ Code ============ */ // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when the sender is sanctioned. + */ uint8 public constant CODE_ADDRESS_FROM_IS_SANCTIONED = 30; + /** + * @notice Restriction code returned when the recipient is sanctioned. + */ uint8 public constant CODE_ADDRESS_TO_IS_SANCTIONED = 31; + /** + * @notice Restriction code returned when the spender is sanctioned. + */ uint8 public constant CODE_ADDRESS_SPENDER_IS_SANCTIONED = 32; /* ============ Event ============ */ + /** + * @notice Emitted when the sanctions-list oracle is set. + * @param newOracle Address of the newly configured sanctions-list oracle. + */ event SetSanctionListOracle(ISanctionsList newOracle); /* ============ Custom errors ============ */ diff --git a/src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol index e04dabf..5a99fdf 100644 --- a/src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol @@ -1,7 +1,13 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title RuleSharedInvariantStorage — constants shared across all rules. + */ abstract contract RuleSharedInvariantStorage { /* ============ String message ============ */ + /** + * @notice Message returned when a restriction code has no associated message. + */ string constant TEXT_CODE_NOT_FOUND = "Unknown restriction code"; } diff --git a/src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol index dc8bd63..5898c4a 100644 --- a/src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol @@ -3,9 +3,18 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "./RuleSharedInvariantStorage.sol"; +/** + * @title RuleSpenderWhitelistInvariantStorage — constants and error for the spender-whitelist rule. + */ abstract contract RuleSpenderWhitelistInvariantStorage is RuleSharedInvariantStorage { // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when the spender is not whitelisted. + */ uint8 public constant CODE_ADDRESS_SPENDER_NOT_WHITELISTED = 66; + /** + * @notice Restriction message returned when the spender is not whitelisted. + */ string constant TEXT_ADDRESS_SPENDER_NOT_WHITELISTED = "SpenderWhitelist: Spender is not whitelisted"; error RuleSpenderWhitelist_InvalidTransferFrom( diff --git a/src/rules/validation/deployment/RuleBlacklist.sol b/src/rules/validation/deployment/RuleBlacklist.sol index 421abbc..f071f47 100644 --- a/src/rules/validation/deployment/RuleBlacklist.sol +++ b/src/rules/validation/deployment/RuleBlacklist.sol @@ -29,6 +29,11 @@ contract RuleBlacklist is RuleBlacklistBase, AccessControlModuleStandalone { PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -44,22 +49,40 @@ contract RuleBlacklist is RuleBlacklistBase, AccessControlModuleStandalone { ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the blacklist to holders of ADDRESS_LIST_ADD_ROLE. + */ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + /** + * @notice Restricts removing addresses from the blacklist to holders of ADDRESS_LIST_REMOVE_ROLE. + */ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol b/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol index f299e82..a39eb72 100644 --- a/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; import {RuleBlacklistBase} from "../abstract/base/RuleBlacklistBase.sol"; import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; @@ -11,33 +12,76 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; * @title RuleBlacklistOwnable2Step * @notice Ownable2Step variant of RuleBlacklist with owner-based authorization hooks. */ -contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step { +contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule and sets the initial owner and meta-transaction forwarder. + * @param owner Contract owner. + * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + */ constructor(address owner, address forwarderIrrevocable) RuleBlacklistBase(forwarderIrrevocable) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleBlacklistBase, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleBlacklistBase.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the blacklist to the contract owner. + */ function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + /** + * @notice Restricts removing addresses from the blacklist to the contract owner. + */ function _authorizeAddressListRemove() internal view virtual override onlyOwner {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleERC2980.sol b/src/rules/validation/deployment/RuleERC2980.sol index a8cbe33..5a2bc6f 100644 --- a/src/rules/validation/deployment/RuleERC2980.sol +++ b/src/rules/validation/deployment/RuleERC2980.sol @@ -48,6 +48,11 @@ contract RuleERC2980 is RuleERC2980Base, AccessControlModuleStandalone { PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -62,26 +67,50 @@ contract RuleERC2980 is RuleERC2980Base, AccessControlModuleStandalone { ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the whitelist to holders of WHITELIST_ADD_ROLE. + */ function _authorizeWhitelistAdd() internal view virtual override onlyRole(WHITELIST_ADD_ROLE) {} + /** + * @notice Restricts removing addresses from the whitelist to holders of WHITELIST_REMOVE_ROLE. + */ function _authorizeWhitelistRemove() internal view virtual override onlyRole(WHITELIST_REMOVE_ROLE) {} + /** + * @notice Restricts adding addresses to the frozenlist to holders of FROZENLIST_ADD_ROLE. + */ function _authorizeFrozenlistAdd() internal view virtual override onlyRole(FROZENLIST_ADD_ROLE) {} + /** + * @notice Restricts removing addresses from the frozenlist to holders of FROZENLIST_REMOVE_ROLE. + */ function _authorizeFrozenlistRemove() internal view virtual override onlyRole(FROZENLIST_REMOVE_ROLE) {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleERC2980Base) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleERC2980Base) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleERC2980Base) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol b/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol index 072adbe..2f70bf5 100644 --- a/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol +++ b/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; import {RuleERC2980Base} from "../abstract/base/RuleERC2980Base.sol"; /** @@ -11,7 +12,7 @@ import {RuleERC2980Base} from "../abstract/base/RuleERC2980Base.sol"; * @notice Ownable2Step variant of RuleERC2980 with owner-based authorization hooks. * @dev All whitelist and frozenlist management functions are restricted to the contract owner. */ -contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step { +contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -26,30 +27,74 @@ contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step { Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleERC2980Base, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleERC2980Base.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the whitelist to the contract owner. + */ function _authorizeWhitelistAdd() internal view virtual override onlyOwner {} + /** + * @notice Restricts removing addresses from the whitelist to the contract owner. + */ function _authorizeWhitelistRemove() internal view virtual override onlyOwner {} + /** + * @notice Restricts adding addresses to the frozenlist to the contract owner. + */ function _authorizeFrozenlistAdd() internal view virtual override onlyOwner {} + /** + * @notice Restricts removing addresses from the frozenlist to the contract owner. + */ function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleERC2980Base) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleERC2980Base) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleERC2980Base) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleIdentityRegistry.sol b/src/rules/validation/deployment/RuleIdentityRegistry.sol index 72109f8..9a252ad 100644 --- a/src/rules/validation/deployment/RuleIdentityRegistry.sol +++ b/src/rules/validation/deployment/RuleIdentityRegistry.sol @@ -16,6 +16,11 @@ contract RuleIdentityRegistry is AccessControlModuleStandalone, RuleIdentityRegi CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule, sets the admin and the ERC-3643 identity registry. + * @param admin Address that receives the default admin role. + * @param identityRegistry_ Address of the ERC-3643 identity registry to query. + */ constructor(address admin, address identityRegistry_) AccessControlModuleStandalone(admin) RuleIdentityRegistryBase(identityRegistry_) @@ -25,6 +30,11 @@ contract RuleIdentityRegistry is AccessControlModuleStandalone, RuleIdentityRegi PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -40,5 +50,8 @@ contract RuleIdentityRegistry is AccessControlModuleStandalone, RuleIdentityRegi ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts identity registry management to holders of DEFAULT_ADMIN_ROLE. + */ function _authorizeIdentityRegistryManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} } diff --git a/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol b/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol index c46ff38..7dba09a 100644 --- a/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol @@ -3,22 +3,52 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; +import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; import {RuleIdentityRegistryBase} from "../abstract/base/RuleIdentityRegistryBase.sol"; /** * @title RuleIdentityRegistryOwnable2Step * @notice Ownable2Step variant of RuleIdentityRegistry. */ -contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step { +contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule, sets the owner and the ERC-3643 identity registry. + * @param owner Contract owner. + * @param identityRegistry_ Address of the ERC-3643 identity registry to query. + */ constructor(address owner, address identityRegistry_) RuleIdentityRegistryBase(identityRegistry_) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleTransferValidation, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleTransferValidation.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts identity registry management to the contract owner. + */ function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {} } diff --git a/src/rules/validation/deployment/RuleMaxTotalSupply.sol b/src/rules/validation/deployment/RuleMaxTotalSupply.sol index 854f45d..87c83c7 100644 --- a/src/rules/validation/deployment/RuleMaxTotalSupply.sol +++ b/src/rules/validation/deployment/RuleMaxTotalSupply.sol @@ -29,6 +29,11 @@ contract RuleMaxTotalSupply is AccessControlModuleStandalone, RuleMaxTotalSupply PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -44,5 +49,8 @@ contract RuleMaxTotalSupply is AccessControlModuleStandalone, RuleMaxTotalSupply ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts maximum total supply management to holders of DEFAULT_ADMIN_ROLE. + */ function _authorizeMaxTotalSupplyManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} } diff --git a/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol b/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol index 46a78d4..ae7e8fe 100644 --- a/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol @@ -3,25 +3,56 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; +import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; import {RuleMaxTotalSupplyBase} from "../abstract/base/RuleMaxTotalSupplyBase.sol"; /** * @title RuleMaxTotalSupplyOwnable2Step * @notice Ownable2Step variant of RuleMaxTotalSupply. */ -contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step { +contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule, sets the owner, the token contract and the initial maximum supply. + * @param owner Contract owner. + * @param tokenContract_ Token contract that exposes totalSupply (must be non-zero). + * @param maxTotalSupply_ Initial maximum supply. + */ constructor(address owner, address tokenContract_, uint256 maxTotalSupply_) RuleMaxTotalSupplyBase(tokenContract_, maxTotalSupply_) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleTransferValidation, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleTransferValidation.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts maximum total supply management to the contract owner. + */ function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {} } diff --git a/src/rules/validation/deployment/RuleSanctionsList.sol b/src/rules/validation/deployment/RuleSanctionsList.sol index 73751c4..65558cd 100644 --- a/src/rules/validation/deployment/RuleSanctionsList.sol +++ b/src/rules/validation/deployment/RuleSanctionsList.sol @@ -21,6 +21,7 @@ contract RuleSanctionsList is AccessControlModuleStandalone, RuleSanctionsListBa /** * @param admin Address of the contract (Access Control) * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + * @param sanctionContractOracle_ Chainalysis sanctions oracle used to screen addresses */ constructor(address admin, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) AccessControlModuleStandalone(admin) @@ -31,6 +32,11 @@ contract RuleSanctionsList is AccessControlModuleStandalone, RuleSanctionsListBa PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -46,20 +52,35 @@ contract RuleSanctionsList is AccessControlModuleStandalone, RuleSanctionsListBa ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts sanctions list management to holders of SANCTIONLIST_ROLE. + */ function _authorizeSanctionListManager() internal view virtual override onlyRole(SANCTIONLIST_ROLE) {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { return ERC2771Context._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { return ERC2771Context._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol b/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol index 47b40fe..85d9cbf 100644 --- a/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol @@ -4,7 +4,9 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; import {ERC2771Context} from "../../../modules/MetaTxModuleStandalone.sol"; +import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; import {RuleSanctionsListBase} from "../abstract/base/RuleSanctionsListBase.sol"; import {ISanctionsList} from "../../interfaces/ISanctionsList.sol"; @@ -12,34 +14,75 @@ import {ISanctionsList} from "../../interfaces/ISanctionsList.sol"; * @title RuleSanctionsListOwnable2Step * @notice Ownable2Step variant of RuleSanctionsList. */ -contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step { +contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule, sets the owner, the forwarder and the sanctions oracle. + * @param owner Contract owner. + * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + * @param sanctionContractOracle_ Chainalysis sanctions oracle used to screen addresses. + */ constructor(address owner, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) RuleSanctionsListBase(forwarderIrrevocable, sanctionContractOracle_) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleTransferValidation, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleTransferValidation.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts sanctions list management to the contract owner. + */ function _authorizeSanctionListManager() internal view virtual override onlyOwner {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { return ERC2771Context._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { return ERC2771Context._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleSpenderWhitelist.sol b/src/rules/validation/deployment/RuleSpenderWhitelist.sol index 50d3bf3..b01746d 100644 --- a/src/rules/validation/deployment/RuleSpenderWhitelist.sol +++ b/src/rules/validation/deployment/RuleSpenderWhitelist.sol @@ -17,6 +17,11 @@ contract RuleSpenderWhitelist is RuleSpenderWhitelistBase, AccessControlModuleSt CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule, sets the admin and the meta-transaction forwarder. + * @param admin Address that receives the default admin role. + * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + */ constructor(address admin, address forwarderIrrevocable) RuleSpenderWhitelistBase(forwarderIrrevocable) AccessControlModuleStandalone(admin) @@ -26,6 +31,11 @@ contract RuleSpenderWhitelist is RuleSpenderWhitelistBase, AccessControlModuleSt PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -41,22 +51,40 @@ contract RuleSpenderWhitelist is RuleSpenderWhitelistBase, AccessControlModuleSt ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the spender whitelist to holders of ADDRESS_LIST_ADD_ROLE. + */ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + /** + * @notice Restricts removing addresses from the spender whitelist to holders of ADDRESS_LIST_REMOVE_ROLE. + */ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol b/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol index f9c2de8..fe72c1b 100644 --- a/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; +import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; import {RuleSpenderWhitelistBase} from "../abstract/base/RuleSpenderWhitelistBase.sol"; import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; @@ -11,36 +13,79 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; * @title RuleSpenderWhitelistOwnable2Step * @notice Ownable2Step deployment variant of spender whitelist rule. */ -contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step { +contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule, sets the owner and the meta-transaction forwarder. + * @param owner Contract owner. + * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + */ constructor(address owner, address forwarderIrrevocable) RuleSpenderWhitelistBase(forwarderIrrevocable) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleTransferValidation, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleTransferValidation.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the spender whitelist to the contract owner. + */ function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + /** + * @notice Restricts removing addresses from the spender whitelist to the contract owner. + */ function _authorizeAddressListRemove() internal view virtual override onlyOwner {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleWhitelist.sol b/src/rules/validation/deployment/RuleWhitelist.sol index 4168280..ff63bbd 100644 --- a/src/rules/validation/deployment/RuleWhitelist.sol +++ b/src/rules/validation/deployment/RuleWhitelist.sol @@ -59,24 +59,45 @@ contract RuleWhitelist is RuleWhitelistBase, AccessControlModuleStandalone { ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts toggling the spender-check setting to holders of DEFAULT_ADMIN_ROLE. + */ function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + /** + * @notice Restricts adding addresses to the whitelist to holders of ADDRESS_LIST_ADD_ROLE. + */ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + /** + * @notice Restricts removing addresses from the whitelist to holders of ADDRESS_LIST_REMOVE_ROLE. + */ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol b/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol index d2f8aaf..40cebe2 100644 --- a/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; import {RuleWhitelistBase} from "../abstract/base/RuleWhitelistBase.sol"; import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; @@ -11,7 +12,7 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; * @title RuleWhitelistOwnable2Step * @notice Ownable2Step variant of RuleWhitelist with owner-based authorization hooks. */ -contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step { +contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -27,28 +28,69 @@ contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step { Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleWhitelistBase, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleWhitelistBase.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the whitelist to the contract owner. + */ function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + /** + * @notice Restricts removing addresses from the whitelist to the contract owner. + */ function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + /** + * @notice Restricts toggling the spender-check setting to the contract owner. + */ function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleWhitelistWrapper.sol b/src/rules/validation/deployment/RuleWhitelistWrapper.sol index 99297e1..a5e7b99 100644 --- a/src/rules/validation/deployment/RuleWhitelistWrapper.sol +++ b/src/rules/validation/deployment/RuleWhitelistWrapper.sol @@ -8,17 +8,20 @@ import {Context} from "@openzeppelin/contracts/utils/Context.sol"; /* ==== Abstract contracts === */ import {AccessControlModuleStandalone} from "../../../modules/AccessControlModuleStandalone.sol"; import {RuleWhitelistWrapperBase} from "../abstract/base/RuleWhitelistWrapperBase.sol"; +/* ==== RuleEngine === */ +import {RulesManagementModuleRolesStorage} from "RuleEngine/modules/library/RulesManagementModuleRolesStorage.sol"; /** * @title Wrapper to call several different whitelist rules */ -contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleStandalone { +contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleStandalone, RulesManagementModuleRolesStorage { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ /** * @param admin Address of the contract (Access Control) * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + * @param checkSpender_ Enables spender checks for transferFrom when true. */ constructor(address admin, address forwarderIrrevocable, bool checkSpender_) RuleWhitelistWrapperBase(forwarderIrrevocable, checkSpender_) @@ -30,7 +33,11 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt //////////////////////////////////////////////////////////////*/ /** + * @notice Returns whether `account` has been granted `role`. * @dev Returns `true` if `account` has been granted `role`. + * @param role Role identifier being queried. + * @param account Address being checked for the role. + * @return True if `account` holds `role`. */ function hasRole(bytes32 role, address account) public @@ -42,6 +49,11 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt return AccessControlModuleStandalone.hasRole(role, account); } + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -57,35 +69,32 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts toggling the spender-check setting to holders of DEFAULT_ADMIN_ROLE. + */ function _authorizeCheckSpenderManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} /** + * @notice Restricts rules management to holders of RULES_MANAGEMENT_ROLE. * @dev Restrict rules management to the dedicated role. */ function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + /** + * @notice Restricts rules-limit management to holders of RULES_MANAGEMENT_ROLE. + */ + function _onlyRulesLimitManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ - function _msgSender() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (address sender) { - return RuleWhitelistWrapperBase._msgSender(); - } - - function _msgData() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (bytes calldata) { - return RuleWhitelistWrapperBase._msgData(); - } - - function _contextSuffixLength() - internal - view - virtual - override(RuleWhitelistWrapperBase, Context) - returns (uint256) - { - return RuleWhitelistWrapperBase._contextSuffixLength(); - } - + /** + * @notice Grants `role` to `account`, keeping role enumeration in sync. + * @param role Role identifier to grant. + * @param account Address receiving the role. + * @return True if the role was newly granted. + */ function _grantRole(bytes32 role, address account) internal virtual @@ -95,6 +104,12 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt return AccessControlEnumerable._grantRole(role, account); } + /** + * @notice Revokes `role` from `account`, keeping role enumeration in sync. + * @param role Role identifier to revoke. + * @param account Address losing the role. + * @return True if the role was previously held and is now revoked. + */ function _revokeRole(bytes32 role, address account) internal virtual @@ -103,4 +118,34 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt { return AccessControlEnumerable._revokeRole(role, account); } + + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ + function _msgSender() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (address sender) { + return RuleWhitelistWrapperBase._msgSender(); + } + + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ + function _msgData() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (bytes calldata) { + return RuleWhitelistWrapperBase._msgData(); + } + + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ + function _contextSuffixLength() + internal + view + virtual + override(RuleWhitelistWrapperBase, Context) + returns (uint256) + { + return RuleWhitelistWrapperBase._contextSuffixLength(); + } } diff --git a/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol b/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol index d5f7bad..a9ffcb0 100644 --- a/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol @@ -6,48 +6,91 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; /* ==== Abstract contracts === */ import {RuleWhitelistWrapperBase} from "../abstract/base/RuleWhitelistWrapperBase.sol"; /** * @title Wrapper to call several different whitelist rules (Ownable2Step) */ -contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2Step { +contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ /** * @param owner Address of the contract owner * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + * @param checkSpender_ Enables spender checks for transferFrom when true. */ constructor(address owner, address forwarderIrrevocable, bool checkSpender_) RuleWhitelistWrapperBase(forwarderIrrevocable, checkSpender_) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleWhitelistWrapperBase, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleWhitelistWrapperBase.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts toggling the spender-check setting to the contract owner. + */ function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} /** + * @notice Restricts rules management to the contract owner. * @dev Restrict rules management to the owner. */ function _onlyRulesManager() internal view virtual override onlyOwner {} + /** + * @notice Restricts rules-limit management to the contract owner. + */ + function _onlyRulesLimitManager() internal view virtual override onlyOwner {} + /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (address sender) { return RuleWhitelistWrapperBase._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (bytes calldata) { return RuleWhitelistWrapperBase._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view diff --git a/test/DeploymentScripts/DeployCMTATWithBlacklist.t.sol b/test/DeploymentScripts/DeployCMTATWithBlacklist.t.sol index 30a0cb5..8701f3d 100644 --- a/test/DeploymentScripts/DeployCMTATWithBlacklist.t.sol +++ b/test/DeploymentScripts/DeployCMTATWithBlacklist.t.sol @@ -2,19 +2,19 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; import {DeployCMTATWithBlacklist} from "script/DeployCMTATWithBlacklist.s.sol"; contract DeployCMTATWithBlacklistTest is Test { function testDeployCMTATWithBlacklist() public { DeployCMTATWithBlacklist script = new DeployCMTATWithBlacklist(); - (CMTATStandalone token, RuleBlacklist rule) = _deploy(script); + (CMTATStandardStandalone token, RuleBlacklist rule) = _deploy(script); assertEq(address(token.ruleEngine()), address(rule)); } - function _deploy(DeployCMTATWithBlacklist script) internal returns (CMTATStandalone token, RuleBlacklist rule) { + function _deploy(DeployCMTATWithBlacklist script) internal returns (CMTATStandardStandalone token, RuleBlacklist rule) { (token, rule) = script.deploy(address(1), address(0)); } } diff --git a/test/DeploymentScripts/DeployCMTATWithBlacklistAndSanctionsList.t.sol b/test/DeploymentScripts/DeployCMTATWithBlacklistAndSanctionsList.t.sol index 35d1f9a..e269aa0 100644 --- a/test/DeploymentScripts/DeployCMTATWithBlacklistAndSanctionsList.t.sol +++ b/test/DeploymentScripts/DeployCMTATWithBlacklistAndSanctionsList.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; import {RuleSanctionsList} from "src/rules/validation/deployment/RuleSanctionsList.sol"; @@ -39,7 +39,7 @@ contract DeployCMTATWithBlacklistAndSanctionsListTest is uint8 constant TRANSFER_OK = 0; uint256 constant INITIAL_BALANCE = 100; - CMTATStandalone token; + CMTATStandardStandalone token; RuleEngine ruleEngine; RuleBlacklist ruleBlacklist; RuleSanctionsList ruleSanctionsList; @@ -255,8 +255,9 @@ contract DeployCMTATWithBlacklistAndSanctionsListTest is vm.expectRevert( abi.encodeWithSelector( - RuleBlacklist_InvalidTransfer.selector, + RuleBlacklist_InvalidTransferFrom.selector, address(ruleBlacklist), + ADMIN, address(0), ADDRESS3, amount, diff --git a/test/DeploymentScripts/DeployCMTATWithWhitelist.t.sol b/test/DeploymentScripts/DeployCMTATWithWhitelist.t.sol index ded0513..d7987b3 100644 --- a/test/DeploymentScripts/DeployCMTATWithWhitelist.t.sol +++ b/test/DeploymentScripts/DeployCMTATWithWhitelist.t.sol @@ -2,18 +2,18 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; import {DeployCMTATWithWhitelist} from "script/DeployCMTATWithWhitelist.s.sol"; contract DeployCMTATWithWhitelistTest is Test { function testDeployCMTATWithWhitelist() public { DeployCMTATWithWhitelist script = new DeployCMTATWithWhitelist(); - (CMTATStandalone token, RuleWhitelist rule) = _deploy(script); + (CMTATStandardStandalone token, RuleWhitelist rule) = _deploy(script); assertEq(address(token.ruleEngine()), address(rule)); } - function _deploy(DeployCMTATWithWhitelist script) internal returns (CMTATStandalone token, RuleWhitelist rule) { + function _deploy(DeployCMTATWithWhitelist script) internal returns (CMTATStandardStandalone token, RuleWhitelist rule) { (token, rule) = script.deploy(address(1), address(0), false); } } diff --git a/test/HelperContract.sol b/test/HelperContract.sol index d7a11a0..bca9d7f 100644 --- a/test/HelperContract.sol +++ b/test/HelperContract.sol @@ -1,7 +1,7 @@ //SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; -import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; // RuleEngine import {RuleEngineInvariantStorage} from "RuleEngine/modules/library/RuleEngineInvariantStorage.sol"; @@ -21,6 +21,10 @@ import {RuleConditionalTransferLight} from "src/rules/operation/RuleConditionalT import { RuleConditionalTransferLightInvariantStorage } from "src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol"; +// RuleMintAllowance +import { + RuleMintAllowanceInvariantStorage +} from "src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol"; import { RuleWhitelistInvariantStorage } from "src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol"; @@ -39,7 +43,7 @@ import { } from "src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol"; // utils -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; /** * @title Constants used by the tests @@ -52,6 +56,7 @@ abstract contract HelperContract is RuleMaxTotalSupplyInvariantStorage, RuleIdentityRegistryInvariantStorage, RuleConditionalTransferLightInvariantStorage, + RuleMintAllowanceInvariantStorage, RuleEngineInvariantStorage { // Test result @@ -87,7 +92,7 @@ abstract contract HelperContract is // CMTAT CMTATDeployment cmtatDeployment; - CMTATStandalone internal cmtatContract; + CMTATStandardStandalone internal cmtatContract; // RuleEngine Mock RuleEngine public ruleEngineMock; diff --git a/test/Ownable2Step/Ownable2StepERC165Support.t.sol b/test/Ownable2Step/Ownable2StepERC165Support.t.sol new file mode 100644 index 0000000..fd3908d --- /dev/null +++ b/test/Ownable2Step/Ownable2StepERC165Support.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {OwnableInterfaceId} from "RuleEngine/modules/library/OwnableInterfaceId.sol"; +import {Ownable2StepInterfaceId} from "RuleEngine/modules/library/Ownable2StepInterfaceId.sol"; + +import {RuleBlacklistOwnable2Step} from "src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol"; +import {RuleWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol"; +import {RuleWhitelistWrapperOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol"; +import {RuleSpenderWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol"; +import {RuleERC2980Ownable2Step} from "src/rules/validation/deployment/RuleERC2980Ownable2Step.sol"; +import {RuleSanctionsListOwnable2Step} from "src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol"; +import {RuleIdentityRegistryOwnable2Step} from "src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol"; +import {RuleMaxTotalSupplyOwnable2Step} from "src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol"; +import {RuleConditionalTransferLightOwnable2Step} from "src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol"; +import { + RuleConditionalTransferLightMultiTokenOwnable2Step +} from "src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol"; +import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; + +contract Ownable2StepERC165SupportTest is Test { + address internal constant OWNER = address(0xA11CE); + + function testAllOwnable2StepRulesAdvertiseOwnableInterfaces() public { + RuleBlacklistOwnable2Step blacklist = new RuleBlacklistOwnable2Step(OWNER, address(0)); + RuleWhitelistOwnable2Step whitelist = new RuleWhitelistOwnable2Step(OWNER, address(0), false, false); + RuleWhitelistWrapperOwnable2Step wrapper = new RuleWhitelistWrapperOwnable2Step(OWNER, address(0), false); + RuleSpenderWhitelistOwnable2Step spenderWhitelist = new RuleSpenderWhitelistOwnable2Step(OWNER, address(0)); + RuleERC2980Ownable2Step erc2980 = new RuleERC2980Ownable2Step(OWNER, address(0), false); + RuleSanctionsListOwnable2Step sanctions = + new RuleSanctionsListOwnable2Step(OWNER, address(0), ISanctionsList(address(0))); + RuleIdentityRegistryOwnable2Step identity = new RuleIdentityRegistryOwnable2Step(OWNER, address(0)); + RuleMaxTotalSupplyOwnable2Step maxSupply = new RuleMaxTotalSupplyOwnable2Step(OWNER, address(1), 1); + RuleConditionalTransferLightOwnable2Step conditional = new RuleConditionalTransferLightOwnable2Step(OWNER); + RuleConditionalTransferLightMultiTokenOwnable2Step conditionalMulti = + new RuleConditionalTransferLightMultiTokenOwnable2Step(OWNER); + + _assertOwnable2StepInterfaces(address(blacklist)); + _assertOwnable2StepInterfaces(address(whitelist)); + _assertOwnable2StepInterfaces(address(wrapper)); + _assertOwnable2StepInterfaces(address(spenderWhitelist)); + _assertOwnable2StepInterfaces(address(erc2980)); + _assertOwnable2StepInterfaces(address(sanctions)); + _assertOwnable2StepInterfaces(address(identity)); + _assertOwnable2StepInterfaces(address(maxSupply)); + _assertOwnable2StepInterfaces(address(conditional)); + _assertOwnable2StepInterfaces(address(conditionalMulti)); + } + + function _assertOwnable2StepInterfaces(address target) internal view { + IERC165 i = IERC165(target); + assertTrue(i.supportsInterface(type(IERC165).interfaceId)); + assertTrue(i.supportsInterface(OwnableInterfaceId.IERC173_INTERFACE_ID)); + assertTrue(i.supportsInterface(Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID)); + assertFalse(i.supportsInterface(type(IAccessControl).interfaceId)); + assertFalse(i.supportsInterface(bytes4(0xdeadbeef))); + } +} diff --git a/test/RuleBlacklist/CMTATIntegration.t.sol b/test/RuleBlacklist/CMTATIntegration.t.sol index 8bd6a1a..cb3d033 100644 --- a/test/RuleBlacklist/CMTATIntegration.t.sol +++ b/test/RuleBlacklist/CMTATIntegration.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {HelperContract} from "../HelperContract.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; /** @@ -207,8 +207,9 @@ contract CMTATIntegration is Test, HelperContract { // Act vm.expectRevert( abi.encodeWithSelector( - RuleBlacklist_InvalidTransfer.selector, + RuleBlacklist_InvalidTransferFrom.selector, address(ruleBlacklist), + DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ADDRESS1, amount, diff --git a/test/RuleBlacklist/CMTATIntegrationDirect.t.sol b/test/RuleBlacklist/CMTATIntegrationDirect.t.sol index 9e47b29..9e9f715 100644 --- a/test/RuleBlacklist/CMTATIntegrationDirect.t.sol +++ b/test/RuleBlacklist/CMTATIntegrationDirect.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {HelperContract} from "../HelperContract.sol"; -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; @@ -178,8 +178,9 @@ contract CMTATIntegrationDirectBlacklist is Test, HelperContract { vm.expectRevert( abi.encodeWithSelector( - RuleBlacklist_InvalidTransfer.selector, + RuleBlacklist_InvalidTransferFrom.selector, address(ruleBlacklist), + DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ADDRESS1, amount, diff --git a/test/RuleConditionalTransferLightMultiToken/CMTATIntegrationDirectMultiToken.t.sol b/test/RuleConditionalTransferLightMultiToken/CMTATIntegrationDirectMultiToken.t.sol new file mode 100644 index 0000000..7c67eb3 --- /dev/null +++ b/test/RuleConditionalTransferLightMultiToken/CMTATIntegrationDirectMultiToken.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; +import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol"; + +contract CMTATIntegrationDirectMultiToken is Test, HelperContract { + CMTATStandardStandalone private tokenA; + CMTATStandardStandalone private tokenB; + RuleConditionalTransferLightMultiToken private rule; + + function setUp() public { + CMTATDeployment deploymentA = new CMTATDeployment(); + CMTATDeployment deploymentB = new CMTATDeployment(); + + tokenA = deploymentA.cmtat(); + tokenB = deploymentB.cmtat(); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(address(tokenA)); + rule.bindToken(address(tokenB)); + tokenA.setRuleEngine(IRuleEngine(address(rule))); + tokenB.setRuleEngine(IRuleEngine(address(rule))); + + tokenA.mint(ADDRESS1, 100); + tokenB.mint(ADDRESS1, 100); + vm.stopPrank(); + } + + function testDetectRestriction_IsTokenScoped() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(address(tokenA), ADDRESS1, ADDRESS2, 10); + + resUint8 = tokenA.detectTransferRestriction(ADDRESS1, ADDRESS2, 10); + assertEq(resUint8, TRANSFER_OK); + + resUint8 = tokenB.detectTransferRestriction(ADDRESS1, ADDRESS2, 10); + assertEq(resUint8, CODE_TRANSFER_REQUEST_NOT_APPROVED); + } + + function testTransferConsumption_IsTokenScoped() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(address(tokenA), ADDRESS1, ADDRESS2, 10); + + vm.prank(ADDRESS1); + tokenA.transfer(ADDRESS2, 10); + + assertEq(rule.approvedCount(address(tokenA), ADDRESS1, ADDRESS2, 10), 0); + assertEq(rule.approvedCount(address(tokenB), ADDRESS1, ADDRESS2, 10), 0); + + vm.prank(ADDRESS1); + vm.expectRevert(); + tokenB.transfer(ADDRESS2, 10); + } +} diff --git a/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol new file mode 100644 index 0000000..9cbd950 --- /dev/null +++ b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol"; +import {MockERC20WithTransferContext} from "src/mocks/MockERC20WithTransferContext.sol"; + +contract RuleConditionalTransferLightMultiTokenTest is Test, HelperContract { + RuleConditionalTransferLightMultiToken private rule; + MockERC20WithTransferContext private tokenA; + MockERC20WithTransferContext private tokenB; + + function setUp() public { + tokenA = new MockERC20WithTransferContext("Token A", "TKNA"); + tokenB = new MockERC20WithTransferContext("Token B", "TKNB"); + + rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(address(tokenA)); + rule.bindToken(address(tokenB)); + vm.stopPrank(); + + tokenA.setRule(address(rule)); + tokenB.setRule(address(rule)); + + tokenA.mint(ADDRESS1, 100); + tokenB.mint(ADDRESS1, 100); + } + + function testApprovalForTokenADoesNotAuthorizeTokenB() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(address(tokenA), ADDRESS1, ADDRESS2, 10); + + vm.prank(ADDRESS1); + tokenA.transfer(ADDRESS2, 10); + + assertEq(tokenA.balanceOf(ADDRESS1), 90); + assertEq(tokenA.balanceOf(ADDRESS2), 10); + + vm.expectRevert(); + vm.prank(ADDRESS1); + tokenB.transfer(ADDRESS2, 10); + } + + function testApproveAndTransferIfAllowedUsesTokenScopedApproval() public { + vm.prank(ADDRESS1); + tokenA.approve(address(rule), 10); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveAndTransferIfAllowed(address(tokenA), ADDRESS1, ADDRESS2, 10); + + assertEq(tokenA.balanceOf(ADDRESS1), 90); + assertEq(tokenA.balanceOf(ADDRESS2), 10); + assertEq(rule.approvedCount(address(tokenA), ADDRESS1, ADDRESS2, 10), 0); + assertEq(rule.approvedCount(address(tokenB), ADDRESS1, ADDRESS2, 10), 0); + } + + function testApproveTransferRevertsForUnboundToken() public { + vm.expectRevert(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS3, ADDRESS1, ADDRESS2, 10); + } +} diff --git a/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiTokenRuleEngineIntegration.t.sol b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiTokenRuleEngineIntegration.t.sol new file mode 100644 index 0000000..e652b66 --- /dev/null +++ b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiTokenRuleEngineIntegration.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; +import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol"; + +contract RuleConditionalTransferLightMultiTokenRuleEngineIntegrationTest is Test, HelperContract { + RuleConditionalTransferLightMultiToken private rule; + RuleEngine private sharedRuleEngine; + + function setUp() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + sharedRuleEngine = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + + // In RuleEngine path, rule sees only msg.sender == RuleEngine. + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(address(sharedRuleEngine)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS2); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + sharedRuleEngine.addRule(rule); + } + + function testTokenScopedApprovalIsNotVisibleThroughSharedRuleEngineCallerContext() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + + resUint8 = sharedRuleEngine.detectTransferRestriction(ADDRESS2, ADDRESS3, 10); + assertEq(resUint8, CODE_TRANSFER_REQUEST_NOT_APPROVED); + } + + function testRuleEngineScopedApprovalIsConsumableButNotTokenScoped() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(address(sharedRuleEngine), ADDRESS2, ADDRESS3, 10); + + resUint8 = sharedRuleEngine.detectTransferRestriction(ADDRESS2, ADDRESS3, 10); + assertEq(resUint8, TRANSFER_OK); + + vm.prank(address(sharedRuleEngine)); + rule.transferred(ADDRESS2, ADDRESS3, 10); + + resUint8 = sharedRuleEngine.detectTransferRestriction(ADDRESS2, ADDRESS3, 10); + assertEq(resUint8, CODE_TRANSFER_REQUEST_NOT_APPROVED); + } +} diff --git a/test/RuleMintAllowance/CMTATIntegration.t.sol b/test/RuleMintAllowance/CMTATIntegration.t.sol new file mode 100644 index 0000000..00ff6c6 --- /dev/null +++ b/test/RuleMintAllowance/CMTATIntegration.t.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; +import {RuleMintAllowance} from "src/rules/operation/RuleMintAllowance.sol"; +import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; + +/** + * @title End-to-end integration test: CMTAT + RuleEngine + RuleMintAllowance + * @notice Verifies that the mint allowance is enforced through the full CMTAT call chain. + * In CMTAT v3.3+ the minter address is passed as `spender` when the rule engine + * calls `transferred(spender, address(0), to, value)`. + */ +contract CMTATIntegration is Test, HelperContract { + address constant MINTER = address(10); + + RuleMintAllowance private mintAllowanceRule; + + function setUp() public { + cmtatDeployment = new CMTATDeployment(); + cmtatContract = cmtatDeployment.cmtat(); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, address(cmtatContract)); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule = new RuleMintAllowance(DEFAULT_ADMIN_ADDRESS); + + // Bind the rule to the RuleEngine (the entity that calls transferred() on the rule) + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.bindToken(address(ruleEngineMock)); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock.addRule(mintAllowanceRule); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.setRuleEngine(ruleEngineMock); + + // Grant minter role on CMTAT to MINTER + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.grantRole(keccak256("MINTER_ROLE"), MINTER); + } + + function testMintSucceedsWithSufficientAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 500); + + vm.prank(MINTER); + cmtatContract.mint(ADDRESS1, 300); + + assertEq(cmtatContract.balanceOf(ADDRESS1), 300); + assertEq(mintAllowanceRule.mintAllowance(MINTER), 200); + } + + function testMintRevertsWhenAllowanceExceeded() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 100); + + vm.prank(MINTER); + vm.expectRevert( + abi.encodeWithSelector( + RuleMintAllowance_AllowanceExceeded.selector, address(mintAllowanceRule), MINTER, 100, 101 + ) + ); + cmtatContract.mint(ADDRESS1, 101); + } + + function testMintRevertsWithZeroAllowance() public { + // Default allowance is 0 + vm.prank(MINTER); + vm.expectRevert(); + cmtatContract.mint(ADDRESS1, 1); + } + + function testBatchMintSucceedsWithinCumulativeAllowance() public { + address[] memory accounts = new address[](2); + accounts[0] = ADDRESS1; + accounts[1] = ADDRESS2; + + uint256[] memory values = new uint256[](2); + values[0] = 300; + values[1] = 200; + + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 500); + + vm.prank(MINTER); + cmtatContract.batchMint(accounts, values); + + assertEq(cmtatContract.balanceOf(ADDRESS1), 300); + assertEq(cmtatContract.balanceOf(ADDRESS2), 200); + assertEq(mintAllowanceRule.mintAllowance(MINTER), 0); + } + + function testBatchMintRevertsAndRollsBackWhenCumulativeAllowanceExceeded() public { + address[] memory accounts = new address[](2); + accounts[0] = ADDRESS1; + accounts[1] = ADDRESS2; + + uint256[] memory values = new uint256[](2); + values[0] = 300; + values[1] = 201; + + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 500); + + vm.prank(MINTER); + vm.expectRevert( + abi.encodeWithSelector( + RuleMintAllowance_AllowanceExceeded.selector, address(mintAllowanceRule), MINTER, 200, 201 + ) + ); + cmtatContract.batchMint(accounts, values); + + assertEq(cmtatContract.balanceOf(ADDRESS1), 0); + assertEq(cmtatContract.balanceOf(ADDRESS2), 0); + assertEq(mintAllowanceRule.mintAllowance(MINTER), 500); + } + + function testMultipleMintersSeparateAllowances() public { + address minter2 = address(11); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.grantRole(keccak256("MINTER_ROLE"), minter2); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 500); + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(minter2, 200); + + vm.prank(MINTER); + cmtatContract.mint(ADDRESS1, 500); + vm.prank(minter2); + cmtatContract.mint(ADDRESS2, 200); + + assertEq(mintAllowanceRule.mintAllowance(MINTER), 0); + assertEq(mintAllowanceRule.mintAllowance(minter2), 0); + assertEq(cmtatContract.balanceOf(ADDRESS1), 500); + assertEq(cmtatContract.balanceOf(ADDRESS2), 200); + } + + function testDetectTransferRestrictionFromViaCMTAT() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 100); + + // Sufficient allowance + assertEq(cmtatContract.detectTransferRestrictionFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 100), TRANSFER_OK); + assertTrue(cmtatContract.canTransferFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 100)); + + // Exceeds allowance + assertEq( + cmtatContract.detectTransferRestrictionFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 101), + CODE_MINTER_ALLOWANCE_EXCEEDED + ); + assertFalse(cmtatContract.canTransferFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 101)); + } + + function testRegularTransfersAreNotRestricted() public { + // Set up balances via admin mint (admin has unlimited system-level access before rule kicks in) + // We need to give admin an allowance to mint first + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(DEFAULT_ADMIN_ADDRESS, 1000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.mint(ADDRESS1, 500); + + // Regular transfer should not be restricted by mint allowance rule + assertTrue(cmtatContract.canTransfer(ADDRESS1, ADDRESS2, 100)); + assertEq(cmtatContract.detectTransferRestriction(ADDRESS1, ADDRESS2, 100), TRANSFER_OK); + + vm.prank(ADDRESS1); + // forge-lint: disable-next-line(erc20-unchecked-transfer) + cmtatContract.transfer(ADDRESS2, 100); + assertEq(cmtatContract.balanceOf(ADDRESS2), 100); + } + + function testAllowanceCanBeIncreasedAfterExhaustion() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 100); + + vm.prank(MINTER); + cmtatContract.mint(ADDRESS1, 100); + assertEq(mintAllowanceRule.mintAllowance(MINTER), 0); + + // Next mint fails + vm.prank(MINTER); + vm.expectRevert(); + cmtatContract.mint(ADDRESS1, 1); + + // Operator increases allowance + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.increaseMintAllowance(MINTER, 50); + vm.prank(MINTER); + cmtatContract.mint(ADDRESS1, 50); + assertEq(mintAllowanceRule.mintAllowance(MINTER), 0); + } +} diff --git a/test/RuleMintAllowance/RuleMintAllowance.t.sol b/test/RuleMintAllowance/RuleMintAllowance.t.sol new file mode 100644 index 0000000..93e3c71 --- /dev/null +++ b/test/RuleMintAllowance/RuleMintAllowance.t.sol @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {RuleMintAllowance} from "src/rules/operation/RuleMintAllowance.sol"; +import {AccessControlModuleStandalone} from "src/modules/AccessControlModuleStandalone.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IERC3643ComplianceFull} from "src/mocks/IERC3643ComplianceFull.sol"; + +contract RuleMintAllowanceTest is Test, HelperContract { + uint8 internal constant CODE_ALLOWANCE_EXCEEDED = 70; + string internal constant TEXT_ALLOWANCE_EXCEEDED = "MintAllowance: minter allowance exceeded"; + + address constant OPERATOR = address(10); + address constant MINTER = address(11); + address constant BOUND_ENGINE = address(12); + + RuleMintAllowance private rule; + + function setUp() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule = new RuleMintAllowance(DEFAULT_ADMIN_ADDRESS); + // Bind a fake engine so transferred() calls succeed + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(BOUND_ENGINE); + } + + /*////////////////////////////////////////////////////////////// + DEPLOYMENT + //////////////////////////////////////////////////////////////*/ + + function testCannotDeployWithZeroAdmin() public { + vm.expectRevert(AccessControlModuleStandalone.AccessControlModuleStandalone_AddressZeroNotAllowed.selector); + new RuleMintAllowance(ZERO_ADDRESS); + } + + /*////////////////////////////////////////////////////////////// + ALLOWANCE MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + function testSetMintAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + assertEq(rule.mintAllowance(MINTER), 1000); + } + + function testSetMintAllowanceEmitsEvent() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectEmit(true, false, false, true); + emit MintAllowanceSet(MINTER, 1000); + rule.setMintAllowance(MINTER, 1000); + } + + function testSetMintAllowanceOverridesExisting() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 500); + assertEq(rule.mintAllowance(MINTER), 500); + } + + function testIncreaseMintAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 500); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.increaseMintAllowance(MINTER, 300); + assertEq(rule.mintAllowance(MINTER), 800); + } + + function testIncreaseMintAllowanceEmitsEvent() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 500); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectEmit(true, false, false, true); + emit MintAllowanceIncreased(MINTER, 300, 800); + rule.increaseMintAllowance(MINTER, 300); + } + + function testDecreaseMintAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.decreaseMintAllowance(MINTER, 400); + assertEq(rule.mintAllowance(MINTER), 600); + } + + function testDecreaseMintAllowanceEmitsEvent() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectEmit(true, false, false, true); + emit MintAllowanceDecreased(MINTER, 400, 600); + rule.decreaseMintAllowance(MINTER, 400); + } + + function testDecreaseMintAllowanceRevertsOnUnderflow() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert(abi.encodeWithSelector(RuleMintAllowance_DecreaseBelowZero.selector, MINTER, 100, 200)); + rule.decreaseMintAllowance(MINTER, 200); + } + + function testDecreaseMintAllowanceToExactZero() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.decreaseMintAllowance(MINTER, 100); + assertEq(rule.mintAllowance(MINTER), 0); + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + function testOnlyOperatorCanSetAllowance() public { + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.setMintAllowance(MINTER, 1000); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.grantRole(ALLOWANCE_OPERATOR_ROLE, OPERATOR); + vm.prank(OPERATOR); + rule.setMintAllowance(MINTER, 1000); + assertEq(rule.mintAllowance(MINTER), 1000); + } + + function testOnlyOperatorCanIncrease() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.increaseMintAllowance(MINTER, 50); + } + + function testOnlyOperatorCanDecrease() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.decreaseMintAllowance(MINTER, 50); + } + + function testDefaultAdminHasOperatorRole() public { + assertTrue(rule.hasRole(ALLOWANCE_OPERATOR_ROLE, DEFAULT_ADMIN_ADDRESS)); + } + + /*////////////////////////////////////////////////////////////// + DETECT TRANSFER RESTRICTION + //////////////////////////////////////////////////////////////*/ + + function testDetectTransferRestrictionAlwaysOk() public view { + assertEq(rule.detectTransferRestriction(ADDRESS1, ADDRESS2, 100), TRANSFER_OK); + assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS2, 100), TRANSFER_OK); + } + + function testCanTransferAlwaysTrue() public view { + assertTrue(rule.canTransfer(ADDRESS1, ADDRESS2, 100)); + assertTrue(rule.canTransfer(ZERO_ADDRESS, ADDRESS2, 100)); + } + + function testDetectTransferRestrictionFromMintWithSufficientAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 500); + assertEq(rule.detectTransferRestrictionFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 500), TRANSFER_OK); + assertTrue(rule.canTransferFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 500)); + } + + function testDetectTransferRestrictionFromMintExceedsAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + assertEq(rule.detectTransferRestrictionFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 101), CODE_ALLOWANCE_EXCEEDED); + assertFalse(rule.canTransferFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 101)); + } + + function testDetectTransferRestrictionFromRegularTransferAlwaysOk() public view { + // Non-mint transfers are always OK regardless of allowance + assertEq(rule.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ADDRESS2, 10000), TRANSFER_OK); + assertTrue(rule.canTransferFrom(ADDRESS3, ADDRESS1, ADDRESS2, 10000)); + } + + function testDetectTransferRestrictionFromBurnAlwaysOk() public view { + // Burns (to == address(0)) are not restricted + assertEq(rule.detectTransferRestrictionFrom(ADDRESS1, ADDRESS1, ZERO_ADDRESS, 10000), TRANSFER_OK); + assertTrue(rule.canTransferFrom(ADDRESS1, ADDRESS1, ZERO_ADDRESS, 10000)); + } + + function testZeroAllowanceMintBlocked() public view { + // Default allowance is 0; any mint amount should be blocked + assertEq(rule.detectTransferRestrictionFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 1), CODE_ALLOWANCE_EXCEEDED); + assertFalse(rule.canTransferFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 1)); + } + + /*////////////////////////////////////////////////////////////// + TRANSFERRED (STATE) + //////////////////////////////////////////////////////////////*/ + + function testTransferredFourArgConsumesAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 400); + assertEq(rule.mintAllowance(MINTER), 600); + } + + function testTransferredFourArgEmitsConsumedEvent() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(BOUND_ENGINE); + vm.expectEmit(true, false, false, true); + emit MintAllowanceConsumed(MINTER, 400, 600); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 400); + } + + function testTransferredFourArgRevertsOnExceedAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + vm.prank(BOUND_ENGINE); + vm.expectRevert( + abi.encodeWithSelector(RuleMintAllowance_AllowanceExceeded.selector, address(rule), MINTER, 100, 101) + ); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 101); + } + + function testTransferredFourArgRegularTransferNoOp() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + // Regular transfer (from != address(0)) should not affect allowance + vm.prank(BOUND_ENGINE); + rule.transferred(ADDRESS3, ADDRESS1, ADDRESS2, 500); + assertEq(rule.mintAllowance(MINTER), 1000); + } + + function testTransferredThreeArgNoOp() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + // 3-arg path: no deduction even for from == address(0) + vm.prank(BOUND_ENGINE); + rule.transferred(ZERO_ADDRESS, ADDRESS1, 500); + assertEq(rule.mintAllowance(MINTER), 1000); + } + + function testTransferredOnlyBoundTokenCan3Arg() public { + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.transferred(ADDRESS1, ADDRESS2, 10); + } + + function testTransferredOnlyBoundTokenCan4Arg() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 100); + } + + function testMultipleMintConsumesSequentially() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 300); + assertEq(rule.mintAllowance(MINTER), 700); + + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS2, 700); + assertEq(rule.mintAllowance(MINTER), 0); + + // Next mint must fail + vm.prank(BOUND_ENGINE); + vm.expectRevert(); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 1); + } + + function testSetAllowanceResetsAfterExhaustion() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 100); + assertEq(rule.mintAllowance(MINTER), 0); + + // Operator resets the allowance + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 200); + assertEq(rule.mintAllowance(MINTER), 200); + + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 200); + assertEq(rule.mintAllowance(MINTER), 0); + } + + /*////////////////////////////////////////////////////////////// + RESTRICTION CODE & MESSAGE + //////////////////////////////////////////////////////////////*/ + + function testCanReturnRestrictionCode() public view { + assertTrue(rule.canReturnTransferRestrictionCode(CODE_ALLOWANCE_EXCEEDED)); + assertFalse(rule.canReturnTransferRestrictionCode(CODE_NONEXISTENT)); + } + + function testMessageForTransferRestriction() public view { + assertEq(rule.messageForTransferRestriction(CODE_ALLOWANCE_EXCEEDED), TEXT_ALLOWANCE_EXCEEDED); + assertEq(rule.messageForTransferRestriction(CODE_NONEXISTENT), TEXT_CODE_NOT_FOUND); + } + + /*////////////////////////////////////////////////////////////// + ERC-165 + //////////////////////////////////////////////////////////////*/ + + function testSupportsInterface() public view { + assertTrue(rule.supportsInterface(type(IAccessControl).interfaceId)); + assertTrue(rule.supportsInterface(RuleInterfaceId.IRULE_INTERFACE_ID)); + assertTrue(rule.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID)); + assertTrue(rule.supportsInterface(RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID)); + assertTrue(rule.supportsInterface(type(IERC7551Compliance).interfaceId)); + assertFalse(rule.supportsInterface(type(IERC3643ComplianceFull).interfaceId)); + assertFalse(rule.supportsInterface(bytes4(0xdeadbeef))); + } + + /*////////////////////////////////////////////////////////////// + COMPLIANCE MODULE (BINDING) + //////////////////////////////////////////////////////////////*/ + + function testBindTokenOnlyComplianceManager() public { + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.bindToken(ADDRESS2); + + vm.expectRevert(RuleMintAllowance_TokenAlreadyBound.selector); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS2); + } + + function testBindTokenAfterUnbind() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.unbindToken(BOUND_ENGINE); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS2); + assertTrue(rule.isTokenBound(ADDRESS2)); + } + + function testUnbindToken() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.unbindToken(BOUND_ENGINE); + assertFalse(rule.isTokenBound(BOUND_ENGINE)); + } + + function testCreatedAndDestroyedAreNoOps() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(BOUND_ENGINE); + rule.created(ADDRESS1, 500); + assertEq(rule.mintAllowance(MINTER), 1000); + + vm.prank(BOUND_ENGINE); + rule.destroyed(ADDRESS1, 300); + assertEq(rule.mintAllowance(MINTER), 1000); + } +} diff --git a/test/RuleMintAllowance/RuleMintAllowanceOwnable2Step.t.sol b/test/RuleMintAllowance/RuleMintAllowanceOwnable2Step.t.sol new file mode 100644 index 0000000..564dea0 --- /dev/null +++ b/test/RuleMintAllowance/RuleMintAllowanceOwnable2Step.t.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {RuleMintAllowanceOwnable2Step} from "src/rules/operation/RuleMintAllowanceOwnable2Step.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {OwnableInterfaceId} from "RuleEngine/modules/library/OwnableInterfaceId.sol"; +import {Ownable2StepInterfaceId} from "RuleEngine/modules/library/Ownable2StepInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IERC3643ComplianceFull} from "src/mocks/IERC3643ComplianceFull.sol"; + +contract RuleMintAllowanceOwnable2StepTest is Test, HelperContract { + address constant OWNER = address(1); + address constant MINTER = address(11); + address constant BOUND_ENGINE = address(12); + + RuleMintAllowanceOwnable2Step private rule; + + function setUp() public { + vm.prank(OWNER); + rule = new RuleMintAllowanceOwnable2Step(OWNER); + vm.prank(OWNER); + rule.bindToken(BOUND_ENGINE); + } + + function testOwnerCanSetAllowance() public { + vm.prank(OWNER); + rule.setMintAllowance(MINTER, 500); + assertEq(rule.mintAllowance(MINTER), 500); + } + + function testNonOwnerCannotSetAllowance() public { + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.setMintAllowance(MINTER, 500); + } + + function testOwnerCanIncreaseAndDecrease() public { + vm.prank(OWNER); + rule.setMintAllowance(MINTER, 1000); + vm.prank(OWNER); + rule.increaseMintAllowance(MINTER, 200); + assertEq(rule.mintAllowance(MINTER), 1200); + vm.prank(OWNER); + rule.decreaseMintAllowance(MINTER, 300); + assertEq(rule.mintAllowance(MINTER), 900); + } + + function testOwnerCanBindUnbind() public { + vm.expectRevert(RuleMintAllowance_TokenAlreadyBound.selector); + vm.prank(OWNER); + rule.bindToken(ADDRESS2); + + vm.prank(OWNER); + rule.unbindToken(BOUND_ENGINE); + + vm.prank(OWNER); + rule.bindToken(ADDRESS2); + assertTrue(rule.isTokenBound(ADDRESS2)); + vm.prank(OWNER); + rule.unbindToken(ADDRESS2); + assertFalse(rule.isTokenBound(ADDRESS2)); + } + + function testNonOwnerCannotBind() public { + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.bindToken(ADDRESS2); + } + + function testTransferredConsumesAllowance() public { + vm.prank(OWNER); + rule.setMintAllowance(MINTER, 1000); + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 300); + assertEq(rule.mintAllowance(MINTER), 700); + } + + function testOwnershipTransferTwoStep() public { + vm.prank(OWNER); + rule.transferOwnership(ADDRESS1); + assertEq(rule.pendingOwner(), ADDRESS1); + assertEq(rule.owner(), OWNER); + + vm.prank(ADDRESS1); + rule.acceptOwnership(); + assertEq(rule.owner(), ADDRESS1); + } + + function testSupportsInterface() public view { + assertTrue(rule.supportsInterface(OwnableInterfaceId.IERC173_INTERFACE_ID)); + assertTrue(rule.supportsInterface(Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID)); + assertTrue(rule.supportsInterface(RuleInterfaceId.IRULE_INTERFACE_ID)); + assertTrue(rule.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID)); + assertTrue(rule.supportsInterface(RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID)); + assertTrue(rule.supportsInterface(type(IERC7551Compliance).interfaceId)); + assertFalse(rule.supportsInterface(type(IERC3643ComplianceFull).interfaceId)); + assertFalse(rule.supportsInterface(bytes4(0xdeadbeef))); + } +} diff --git a/test/RuleSanctionList/RuleSanctionListTest.t.sol b/test/RuleSanctionList/RuleSanctionListTest.t.sol index aeacda7..4c48b18 100644 --- a/test/RuleSanctionList/RuleSanctionListTest.t.sol +++ b/test/RuleSanctionList/RuleSanctionListTest.t.sol @@ -193,6 +193,23 @@ contract RuleSanctionlistTest is Test, HelperContract { ruleSanctionList.transferred(ADDRESS1, ADDRESS2, 0, 20); } + function testRemoveFromSanctionsListUnblocksTransfer() public { + // Arrange: ATTACKER is sanctioned in setUp, so the rule blocks the transfer + assertEq(sanctionlistOracle.isSanctioned(ATTACKER), true); + resUint8 = ruleSanctionList.detectTransferRestriction(ATTACKER, ADDRESS2, 20); + assertEq(resUint8, CODE_ADDRESS_FROM_IS_SANCTIONED); + + // Act: remove ATTACKER from the sanctions list + sanctionlistOracle.removeFromSanctionsList(ATTACKER); + + // Assert: no longer sanctioned and the transfer now passes + assertEq(sanctionlistOracle.isSanctioned(ATTACKER), false); + resUint8 = ruleSanctionList.detectTransferRestriction(ATTACKER, ADDRESS2, 20); + assertEq(resUint8, NO_ERROR); + // No revert + ruleSanctionList.transferred(ATTACKER, ADDRESS2, 20); + } + function testDetectTransferRestrictionWitSpenderOk() public { // Act resUint8 = ruleSanctionList.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ADDRESS2, 20); diff --git a/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol b/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol index 075475b..e1aaad1 100644 --- a/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol +++ b/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol @@ -40,6 +40,20 @@ contract RuleSpenderWhitelistTest is Test, HelperContract { rule.transferred(ADDRESS3, ADDRESS1, ADDRESS2, 10); } + function testMintExemptFromSpenderCheck() public { + // Minter (spender) not whitelisted — mint must still succeed because from == address(0). + assertEq(rule.detectTransferRestrictionFrom(ADDRESS3, ZERO_ADDRESS, ADDRESS2, 10), TRANSFER_OK); + assertTrue(rule.canTransferFrom(ADDRESS3, ZERO_ADDRESS, ADDRESS2, 10)); + rule.transferred(ADDRESS3, ZERO_ADDRESS, ADDRESS2, 10); + } + + function testBurnExemptFromSpenderCheck() public { + // Burner (spender) not whitelisted — burn must still succeed because to == address(0). + assertEq(rule.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ZERO_ADDRESS, 10), TRANSFER_OK); + assertTrue(rule.canTransferFrom(ADDRESS3, ADDRESS1, ZERO_ADDRESS, 10)); + rule.transferred(ADDRESS3, ADDRESS1, ZERO_ADDRESS, 10); + } + function testTransferFromAllowedWhenSpenderWhitelisted() public { vm.prank(DEFAULT_ADMIN_ADDRESS); rule.addAddress(ADDRESS3); diff --git a/test/RuleWhitelist/CMTATIntegration.t.sol b/test/RuleWhitelist/CMTATIntegration.t.sol index c861294..06c14e4 100644 --- a/test/RuleWhitelist/CMTATIntegration.t.sol +++ b/test/RuleWhitelist/CMTATIntegration.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {HelperContract} from "../HelperContract.sol"; -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; diff --git a/test/RuleWhitelist/CMTATIntegrationWhitelistWrapper.t.sol b/test/RuleWhitelist/CMTATIntegrationWhitelistWrapper.t.sol index a102d90..1ebf60a 100644 --- a/test/RuleWhitelist/CMTATIntegrationWhitelistWrapper.t.sol +++ b/test/RuleWhitelist/CMTATIntegrationWhitelistWrapper.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {HelperContract} from "../HelperContract.sol"; -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; diff --git a/test/RuleWhitelist/CMTATRuleEngineIntegration.t.sol b/test/RuleWhitelist/CMTATRuleEngineIntegration.t.sol index 6a5794d..1c5cd76 100644 --- a/test/RuleWhitelist/CMTATRuleEngineIntegration.t.sol +++ b/test/RuleWhitelist/CMTATRuleEngineIntegration.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {HelperContract} from "../HelperContract.sol"; -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; diff --git a/test/Version.t.sol b/test/Version.t.sol index 9bc8030..54580f4 100644 --- a/test/Version.t.sol +++ b/test/Version.t.sol @@ -13,7 +13,7 @@ import {RuleERC2980} from "src/rules/validation/deployment/RuleERC2980.sol"; import {RuleConditionalTransferLight} from "src/rules/operation/RuleConditionalTransferLight.sol"; contract VersionTest is Test, HelperContract { - string constant EXPECTED_VERSION = "0.3.0"; + string constant EXPECTED_VERSION = "0.4.0"; function testVersionRuleWhitelist() public { RuleWhitelist rule = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, false); diff --git a/test/utils/CMTATDeployment.sol b/test/utils/CMTATDeployment.sol new file mode 100644 index 0000000..0ce653f --- /dev/null +++ b/test/utils/CMTATDeployment.sol @@ -0,0 +1,32 @@ +//SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {ICMTATConstructor, CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol"; + +contract CMTATDeployment { + // Share with helper contract + address constant ZERO_ADDRESS = address(0); + address constant DEFAULT_ADMIN_ADDRESS = address(1); + + CMTATStandardStandalone public cmtat; + + constructor() { + // CMTAT + ICMTATConstructor.ERC20Attributes memory erc20Attributes = + ICMTATConstructor.ERC20Attributes("CMTA Token", "CMTAT", 0); + ICMTATConstructor.ExtraInformationAttributes memory extraInformationAttributes = + ICMTATConstructor.ExtraInformationAttributes( + "CMTAT_ISIN", + IERC1643CMTAT.DocumentInfo( + "Terms", "https://cmta.ch", 0x9ff867f6592aa9d6d039e7aad6bd71f1659720cbc4dd9eae1554f6eab490098b + ), + "CMTAT_info" + ); + ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(ZERO_ADDRESS)); + cmtat = new CMTATStandardStandalone( + ZERO_ADDRESS, DEFAULT_ADMIN_ADDRESS, erc20Attributes, extraInformationAttributes, engines + ); + } +}