diff --git a/AGENTS.md b/AGENTS.md index a4f9878..83f9367 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,9 +24,9 @@ Modular compliance-rule library for CMTAT / ERC-3643 security tokens. Each rule | `RuleMaxTotalSupply` | Cap minting so total supply never exceeds a maximum | | `RuleIdentityRegistry` | Check ERC-3643 identity registry for participant verification | | `RuleSpenderWhitelist` / `RuleSpenderWhitelistOwnable2Step` | Allow `transferFrom` only when spender is whitelisted; direct transfers are always allowed | -| `RuleERC2980` | ERC-2980 Swiss Compliant rule: whitelist (recipient-only) + frozenlist (blocks sender and recipient); frozenlist takes priority | +| `RuleERC2980` | ERC-2980 Swiss Compliant rule: whitelist (recipient-only) + frozenlist (blocks sender, recipient, and spender for `transferFrom`); frozenlist takes priority | | `RuleERC2980Ownable2Step` | Ownable2Step variant of RuleERC2980 | -| `RuleConditionalTransferLight` | Require operator approval before each transfer | +| `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 | | `AccessControlModuleStandalone` | Base RBAC module; admin implicitly holds all roles | | `MetaTxModuleStandalone` | ERC-2771 meta-transaction support | @@ -66,6 +66,7 @@ Foundry config: `foundry.toml` (solc 0.8.34, EVM prague, optimizer 200 runs). - AccessControl variants must use `onlyRole(ROLE)` in `_authorize*()` methods (avoid direct `_checkRole`). - AccessControl variants treat the default admin as having all roles via `hasRole`, but the admin may not appear in role member enumerations unless explicitly granted. - All rules implement `IERC3643Version` via `VersionModule`; the current version string is `"0.2.0"`. +- **ERC-165 interface IDs**: `type(IFoo).interfaceId` only XORs selectors defined directly on `IFoo` and does NOT include selectors from inherited interfaces. Always use the pre-computed library constants instead: `ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID` (from `CMTAT/library/`), `RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID` (from `CMTAT/library/`), and `RuleInterfaceId.IRULE_INTERFACE_ID` (from `RuleEngine/modules/library/`). If no pre-computed constant exists for an interface, define a flat mock interface that redeclares all functions from the full inheritance tree and use `type(IFooFlattened).interfaceId` to compute the correct value (see `lib/RuleEngine/src/mocks/IRuleInterfaceIdHelper.sol` for the established pattern). - Batch add/remove operations are non-reverting (skip duplicates); single-item operations revert on invalid input. - All `internal` functions should be marked `virtual`. - Do not create git commits; provide commit messages only when requested. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0eefa..13c6b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,9 +43,40 @@ Custom changelog tag: `Dependencies`, `Documentation`, `Testing` - Update surya doc by running the 3 scripts in [./doc/script](./doc/script) - Update changelog -## v0.2.0 - TBD +## v0.3.0 - -Commit: `TBD` +### Security + +- **H1 fix** — `RuleConditionalTransferLight`: enforced single-token binding by overriding `bindToken` to revert with `RuleConditionalTransferLight_TokenAlreadyBound` if a token is already bound. Eliminates cross-token approval replay and approval-draining attacks. Use `unbindToken` first to migrate to a new token. +- **M1 fix** — Added `IERC7551Compliance` (`0x7157797f`), `IERC3643IComplianceContract` (validation rules), and the full ERC-3643 `ICompliance` ID via flat mock `IERC3643ComplianceFull` (`0x3144991c`) to all `supportsInterface` implementations. Silent `false` on ERC-165 introspection no longer occurs for compliant callers. + +### Added + +- `RuleConditionalTransferLightApprovalBase` — new abstract contract holding the pure approval state machine (approval counts, `approveTransfer`, `cancelTransferApproval`, `approvedCount`, and the `transferred` callback). No ERC-3643 / IRule knowledge. +- `IERC3643ComplianceFull` (`src/mocks/IERC3643ComplianceFull.sol`) — flat mock interface redeclaring all eight ERC-3643 `ICompliance` functions; used to compute the correct ERC-165 ID (`0x3144991c`) since `type(IERC3643Compliance).interfaceId` only XORs directly-defined selectors. + +### Changed + +- `RuleConditionalTransferLightBase` refactored into two layers: `RuleConditionalTransferLightApprovalBase` (state machine) + `RuleConditionalTransferLightBase` (ERC-3643 / IRule compliance integration). Eliminates code duplication between the AccessControl and Ownable2Step variants. +- `RuleConditionalTransferLightOwnable2Step` now inherits `ERC3643ComplianceModule` via the base (consistent with the AccessControl variant); `_authorizeTransferExecution` consolidated into the base and checks `isTokenBound(_msgSender())`. +- `approveAndTransferIfAllowed` no longer takes a `token` parameter — bound token is retrieved directly via `getTokenBound()`. +- Custom error `RuleConditionalTransferLight_TokenAddressZeroNotAllowed` renamed to `RuleConditionalTransferLight_TokenNotBound` for clarity. +- Solidity style guide ordering (type declarations → state variables → events → errors → modifiers → functions; constructor → external → public → internal → private) enforced across all `src/` contracts. +- `supportsInterface` in `RuleConditionalTransferLight` and `RuleConditionalTransferLightOwnable2Step` now advertises `IERC7551Compliance` and the full ERC-3643 `ICompliance` interface ID instead of the narrow `IERC3643IComplianceContract`. +- `supportsInterface` in `RuleTransferValidation` (cascades to all validation rules) now also advertises `IERC7551Compliance` and `IERC3643IComplianceContract`. +- Update contract version to `0.3.0` + +### Documentation + +- Wake Arena (Ackee Blockchain Security) AI-assisted static analysis report and project feedback added to `doc/security/audits/tools/v0.2.0/`. +- README Security section updated with Wake Arena findings summary table. +- README Access Control section updated to document intentional `DEFAULT_ADMIN_ROLE` implicit-role behaviour, `grantRole` no-op semantics, and off-chain monitoring guidance (I2). +- `RuleERC2980` documentation updated to clarify that a frozen address acting as `transferFrom` spender is also blocked (code 62) (I1). +- `CLAUDE.md` / `AGENTS.md` convention added: always use pre-computed library constants for ERC-165 IDs; use a flat mock interface when no constant exists. + +## v0.2.0 - 2026-03-10 + +Commit: [`d72a98a`](https://github.com/CMTA/Rules/commit/d72a98abbba29cd82a7056b59104e82ac65389e7) ### Added diff --git a/CLAUDE.md b/CLAUDE.md index a4f9878..83f9367 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,9 +24,9 @@ Modular compliance-rule library for CMTAT / ERC-3643 security tokens. Each rule | `RuleMaxTotalSupply` | Cap minting so total supply never exceeds a maximum | | `RuleIdentityRegistry` | Check ERC-3643 identity registry for participant verification | | `RuleSpenderWhitelist` / `RuleSpenderWhitelistOwnable2Step` | Allow `transferFrom` only when spender is whitelisted; direct transfers are always allowed | -| `RuleERC2980` | ERC-2980 Swiss Compliant rule: whitelist (recipient-only) + frozenlist (blocks sender and recipient); frozenlist takes priority | +| `RuleERC2980` | ERC-2980 Swiss Compliant rule: whitelist (recipient-only) + frozenlist (blocks sender, recipient, and spender for `transferFrom`); frozenlist takes priority | | `RuleERC2980Ownable2Step` | Ownable2Step variant of RuleERC2980 | -| `RuleConditionalTransferLight` | Require operator approval before each transfer | +| `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 | | `AccessControlModuleStandalone` | Base RBAC module; admin implicitly holds all roles | | `MetaTxModuleStandalone` | ERC-2771 meta-transaction support | @@ -66,6 +66,7 @@ Foundry config: `foundry.toml` (solc 0.8.34, EVM prague, optimizer 200 runs). - AccessControl variants must use `onlyRole(ROLE)` in `_authorize*()` methods (avoid direct `_checkRole`). - AccessControl variants treat the default admin as having all roles via `hasRole`, but the admin may not appear in role member enumerations unless explicitly granted. - All rules implement `IERC3643Version` via `VersionModule`; the current version string is `"0.2.0"`. +- **ERC-165 interface IDs**: `type(IFoo).interfaceId` only XORs selectors defined directly on `IFoo` and does NOT include selectors from inherited interfaces. Always use the pre-computed library constants instead: `ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID` (from `CMTAT/library/`), `RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID` (from `CMTAT/library/`), and `RuleInterfaceId.IRULE_INTERFACE_ID` (from `RuleEngine/modules/library/`). If no pre-computed constant exists for an interface, define a flat mock interface that redeclares all functions from the full inheritance tree and use `type(IFooFlattened).interfaceId` to compute the correct value (see `lib/RuleEngine/src/mocks/IRuleInterfaceIdHelper.sol` for the established pattern). - Batch add/remove operations are non-reverting (skip duplicates); single-item operations revert on invalid input. - All `internal` functions should be marked `virtual`. - Do not create git commits; provide commit messages only when requested. diff --git a/README.md b/README.md index 83a13f5..c252ea5 100644 --- a/README.md +++ b/README.md @@ -366,7 +366,7 @@ Several rules are available in multiple access-control variants. Use the simples | RuleMaxTotalSupply | Read-Only | | | | This rule limits minting so that the total supply never exceeds a configured maximum. | | RuleIdentityRegistry | Read-Only | | | | This rule checks the ERC-3643 Identity Registry for transfer participants when configured. | | RuleSpenderWhitelist | Read-Only | | | | This rule blocks `transferFrom` when the spender is not in the whitelist. Direct transfers are always allowed. | -| RuleERC2980 | Read-Only | | | | ERC-2980 Swiss Compliant rule combining a whitelist (recipient-only) and a frozenlist (blocks both sender and recipient). Frozenlist takes priority over whitelist. | +| RuleERC2980 | Read-Only | | | | ERC-2980 Swiss Compliant rule combining a whitelist (recipient-only) and a frozenlist (blocks sender, recipient, and spender for `transferFrom`). Frozenlist takes priority over whitelist. | | RuleConditionalTransferLight | Read-Write | | | | This rule requires that transfers have to be approved by an operator before being executed. Each approval is consumed once and the same transfer can be approved multiple times. | | [RuleConditionalTransfer](https://github.com/CMTA/RuleConditionalTransfer) (external) | Read-Write | | |
(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. | @@ -402,12 +402,12 @@ Detailed technical documentation for each rule is available in [`doc/technical/` - Read-only rules still implement `transferred()` to comply with ERC-3643 and RuleEngine interfaces, but they do not change state. - `RuleConditionalTransferLight` approvals are keyed by `(from, to, value)` and are not nonce-based. - `RuleConditionalTransferLight` provides `approveAndTransferIfAllowed` to approve and immediately execute `transferFrom` when this rule has allowance; it assumes the token calls back `transferred()` during the transfer. -- `RuleConditionalTransferLight` restricts `transferred()` to tokens bound via `bindToken` (ERC3643ComplianceModule). +- `RuleConditionalTransferLight` restricts `transferred()` to the single token bound via `bindToken`. Only one token can be bound at a time: a second `bindToken` call reverts with `RuleConditionalTransferLight_TokenAlreadyBound`. The token can be unbound with `unbindToken`, after which a new token may be bound. - `RuleConditionalTransferLight` exempts mints (`from == address(0)`) and burns (`to == address(0)`) from the approval requirement; `created` and `destroyed` delegate to `_transferred`, which returns early for those cases. - AccessControl variants use `onlyRole(ROLE)` in `_authorize*()` and internal helpers are marked `virtual`. - AccessControl variants use `AccessControlEnumerable`, so role members can be enumerated with `getRoleMember` / `getRoleMemberCount`. The default admin is treated as having all roles via `hasRole`, but may not appear in role member lists unless explicitly granted. - `forwarderIrrevocable` is accepted as-is (including `address(0)`), and is not validated against ERC-165 because some forwarders do not implement it. -- `RuleERC2980` frozenlist takes priority over the whitelist: an address that is both whitelisted and frozen will be rejected. +- `RuleERC2980` frozenlist takes priority over the whitelist: an address that is both whitelisted and frozen will be rejected. A frozen address acting as a `transferFrom` spender is also blocked (code 62), even if `from` and `to` are not frozen. - `RuleERC2980` sender (`from`) does not need to be whitelisted; only the recipient (`to`) must be whitelisted for a transfer to succeed. - All rules implement `IERC3643Version` via `VersionModule` and expose a `version()` function returning `"0.2.0"`. @@ -490,8 +490,8 @@ The operator sets `RuleBlacklist` on the token. The issuer tries to transfer to Implements the [ERC-2980](https://eips.ethereum.org/EIPS/eip-2980) Swiss Compliant Asset Token transfer restriction using two independent address lists managed in a single rule: - **Whitelist**: only whitelisted addresses may *receive* tokens. Senders do not need to be whitelisted and may freely transfer tokens they already hold. -- **Frozenlist**: frozen addresses are completely blocked — they can neither send nor receive tokens. -- **Priority**: frozenlist is checked first. If `from` or `to` is frozen, the transfer is rejected regardless of whitelist membership. +- **Frozenlist**: frozen addresses are completely blocked — they can neither send nor receive tokens. Additionally, a frozen address acting as a `transferFrom` spender will have the transfer rejected (code 62), even if `from` and `to` are not frozen. +- **Priority**: frozenlist is checked first. If `from`, `to`, or `spender` is frozen, the transfer is rejected regardless of whitelist membership. ![surya_inheritance_RuleERC2980.sol](./doc/surya/surya_inheritance/surya_inheritance_RuleERC2980.sol.png) @@ -562,17 +562,23 @@ This rule requires that transfers must be approved by an operator before being e **Usage scenario** -An operator calls `approveTransfer(from, to, value)`. The compliance manager binds the token with `bindToken(token)`. 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`. +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. ## Access Control -The modules `AccessControlModuleStandalone` allows to implement RBAC access control by inheriting from the contract `AccessControl`from OpenZeppelin. +The module `AccessControlModuleStandalone` implements RBAC access control by inheriting from OpenZeppelin's `AccessControlEnumerable`. -This module overrides the OpenZeppelin function `hasRole`to give by default all the roles to the `admin`. +Each rule implements its own access control by inheriting from `AccessControlModuleStandalone`. The default admin is the address passed as `admin` to the constructor at deployment. -Each rule implements its own access control by inheriting from the module `AccessControlModuleStandalone`. +#### `DEFAULT_ADMIN_ROLE` implicit role behaviour -For all rules, the default admin is the address put in argument(`admin`) inside the constructor and set when the contract is deployed. +`AccessControlModuleStandalone` overrides OpenZeppelin's `hasRole` so that any account holding `DEFAULT_ADMIN_ROLE` returns `true` for **every** role check. This is intentional: the OpenZeppelin `DEFAULT_ADMIN_ROLE` holder can already grant itself any role at any time, so treating it as implicitly holding all roles from the start removes unnecessary ceremony and makes access management easier in practice. + +Practical consequences integrators must be aware of: + +- **`grantRole` to a default admin is a no-op.** `_grantRole` checks `!hasRole(role, account)` before writing storage; since the admin already returns `true` via the override, the storage write and the `RoleGranted` event are skipped. The admin will **not** appear in `getRoleMember` / `getRoleMemberCount` enumerations for non-default roles unless the role was explicitly granted before the admin was set. +- **`revokeRole` / `renounceRole` on a non-default role for a default admin are misleading.** They emit `RoleRevoked` and clear the storage flag, but `hasRole` continues to return `true` because the account still holds `DEFAULT_ADMIN_ROLE`. The effective privilege is unchanged. To fully remove access, `DEFAULT_ADMIN_ROLE` itself must be revoked. +- **Off-chain monitoring should use `hasRole` queries**, not role-membership events or enumeration, to determine the effective privileges of admin accounts. See also [docs.openzeppelin.com - AccessControl](https://docs.openzeppelin.com/contracts/5.x/api/access#AccessControl) @@ -1664,6 +1670,19 @@ Static analysis was performed with [Slither](https://github.com/crytic/slither). | unindexed-event-address | Informational | 2 | Out of scope (both in `lib/RuleEngine`); `IAddressList` events previously fixed | | unused-state | Informational | 60 | False positive — `RuleNFTAdapter` constants used in base dispatch logic; Slither per-contract analysis limitation | +#### Wake Arena (v0.2.0) + +AI-assisted static analysis was performed with [Wake Arena](https://getwake.io) by Ackee Blockchain Security. The full report and the project team's feedback are available in [`doc/security/audits/tools/v0.2.0/`](./doc/security/audits/tools/v0.2.0/). + +*Ackee Blockchain Security, Wake Arena AI Report | CMTA: Rules, March 16, 2026 18:00 UTC.* + +| ID | Title | Severity | Confidence | Verdict | +|---|---|---|---|---| +| H-1 | ConditionalTransferLight approvals not scoped by token | High | High | Fixed — single-token binding enforced in `bindToken`; `RuleConditionalTransferLight_TokenAlreadyBound` error added | +| M-1 | Incomplete `supportsInterface` breaks ERC-165 discovery | Medium | High | Fixed — pre-computed constants + `IERC7551Compliance` + full ERC-3643 `ICompliance` ID (`IERC3643ComplianceFull`, `0x3144991c`) added | +| I-1 | RuleERC2980 docs omit frozen spender on `transferFrom` | Informational | High | Fixed (doc only) — README, `AGENTS.md`, and `CLAUDE.md` updated to document spender freeze path | +| I-2 | `hasRole` override: admin implicitly passes all role checks | Informational | High | Fixed (doc only) — dedicated section added to README documenting intentional design and off-chain monitoring guidance | + ## Intellectual property The code is copyright (c) Capital Market and Technology Association, 2022-2026, and is released under [Mozilla Public License 2.0](https://github.com/CMTA/CMTAT/blob/master/LICENSE.md). diff --git a/doc/security/audits/tools/v0.2.0/ackee-wake-arena-v0.2.0-feedback.md b/doc/security/audits/tools/v0.2.0/ackee-wake-arena-v0.2.0-feedback.md new file mode 100644 index 0000000..00fdb8c --- /dev/null +++ b/doc/security/audits/tools/v0.2.0/ackee-wake-arena-v0.2.0-feedback.md @@ -0,0 +1,295 @@ +# Wake Arena (Ackee Blockchain Security) — Feedback + +**Report date:** 2026-03-16 +**Tool:** Wake Arena (AI-assisted static analysis) +**Audited commit:** `d72a98abbba29cd82a7056b59104e82ac65389e7` +**Report file:** [ackee-wake-arenav0.2.0.pdf](./ackee-wake-arenav0.2.0.pdf) +**Feedback date:** 2026-03-18 + +--- + +## Summary + +| ID | Title | Severity | Confidence | Verdict | +|----|-------|----------|------------|---------| +| H1 | ConditionalTransferLight approvals not scoped by token | High | High | **Fixed** | +| M1 | Incomplete `supportsInterface` breaks ERC-165 discovery | Medium | High | **Fixed** | +| I1 | RuleERC2980 docs omit frozen spender on `transferFrom` | Info | High | **Fixed (doc only)** | +| I2 | `hasRole` override: admin implicitly passes all role checks | Info | High | **Fixed (doc only)** | + +--- + +## H1 — ConditionalTransferLight approvals not scoped by token + +**Severity:** High | **Confidence:** High | **Verdict: Fixed** + +### Finding + +`approvalCounts` in `RuleConditionalTransferLightBase` was keyed exclusively by +`_transferHash(from, to, value)` — a keccak256 of the three packed arguments — with no +token address in the key. + +`_authorizeTransferExecution()` only verified that `msg.sender` was a bound token via +`isTokenBound(_msgSender())`, without verifying *which* token the approval was created +for. Because `ERC3643ComplianceModule` supported multiple simultaneous token bindings via +an `EnumerableSet.AddressSet`, a single rule instance could be bound to Token A and +Token B at the same time, allowing approvals recorded for one token to be consumed by the +other. + +Three concrete attack vectors were identified: + +1. **Cross-token replay** — an approval created by Token A's operator can be consumed by + a `transferred` call from Token B. +2. **Approval draining / DoS** — a malicious operator on Token B deliberately consumes + approvals meant for Token A, preventing legitimate transfers. +3. **Rebinding hazard** — stale approvals survive an unbind/rebind cycle and can be + consumed by a new token. + +### Assessment + +Valid and actionable. The exploit requires no elevated privilege beyond being an operator +of a second bound token. + +### Fix + +**Commit:** `2e41c72` + +Single-token binding is enforced by overriding `bindToken` in +`RuleConditionalTransferLightBase`: + +```solidity +function bindToken(address token) public override onlyComplianceManager { + require(getTokenBound() == address(0), RuleConditionalTransferLight_TokenAlreadyBound()); + _bindToken(token); +} +``` + +A second `bindToken` call now reverts with `RuleConditionalTransferLight_TokenAlreadyBound`. +To migrate to a new token the compliance manager must first call `unbindToken`. + +This eliminates vectors 1 and 2 entirely. Vector 3 (stale approvals after an explicit +unbind/rebind) remains a conscious operator decision — the operator who controls rebinding +also controls approvals — and is documented in NatSpec on `bindToken`. + +The error `RuleConditionalTransferLight_TokenAlreadyBound` was added to +`RuleConditionalTransferLightInvariantStorage`. Tests added: +`testBindToken_RevertsIfAlreadyBound` and `testBindToken_RevertsForUnauthorizedCaller`. + +As a follow-up refactor (commit `7e3abb2`) the base contract was split into +`RuleConditionalTransferLightApprovalBase` (pure approval state machine) and +`RuleConditionalTransferLightBase` (ERC-3643 / IRule compliance integration). The +`bindToken` guard and `_authorizeTransferExecution` were consolidated into the base to +eliminate duplication between the AccessControl and Ownable2Step variants. +`RuleConditionalTransferLightOwnable2Step` was also updated to inherit +`ERC3643ComplianceModule` for consistency. + +--- + +## M1 — Incomplete `supportsInterface` breaks ERC-165 discovery + +**Severity:** Medium | **Confidence:** High | **Verdict: Fixed** + +### Finding + +`RuleTransferValidation.supportsInterface` only declared `type(IRule).interfaceId` and +`RuleInterfaceId.IRULE_INTERFACE_ID`, omitting `IERC165`, `IERC1404`, `IERC1404Extend`, +`IERC3643ComplianceRead`, and `IERC7551Compliance`. + +`RuleConditionalTransferLight.supportsInterface` delegated to +`AccessControlEnumerable.supportsInterface` (covering `IERC165`, `IAccessControl`, +`IAccessControlEnumerable`) but omitted `IERC1404`, `IERC1404Extend`, +`IERC3643ComplianceRead`, `IERC7551Compliance`, and `IERC3643IComplianceContract`. + +Silent `false` responses from `supportsInterface` cause integration failures or silent +enforcement bypasses in tools and front-ends that use ERC-165 introspection to verify +rule capabilities before wiring them. + +### Assessment + +Valid. The gap is directly observable from source. `type(IFoo).interfaceId` in Solidity +only XORs selectors defined directly on `IFoo` and does **not** include selectors from +inherited interfaces — a subtle but critical point for hierarchical compliance interfaces. + +### Fix + +**Commit:** `5500a74` (initial fix) — follow-up additions below. + +Pre-computed library constants from CMTAT are used in place of raw `type(X).interfaceId` +calls: + +- `ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID` (`0x78a8de7d`) — covers the full + ERC-1404 / ERC-1404-Extend compliance interface family +- `RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID` (`0x20c49ce7`) — covers `IRuleEngine` +- `RuleInterfaceId.IRULE_INTERFACE_ID` (`0x2497d6cb`) — covers `IRule` + +Two further interfaces were added as follow-up: + +- `type(IERC7551Compliance).interfaceId` (`0x7157797f`) — single selector + `canTransferFrom(address,address,address,uint256)`; safe to use `type(X).interfaceId` + directly because `IERC7551Compliance` defines exactly one function. +- For **validation rules** (`RuleTransferValidation` and all subclasses): + `type(IERC3643IComplianceContract).interfaceId` — single selector + `transferred(address,address,uint256)`; also safe to use directly. +- For **`RuleConditionalTransferLight`** and **`RuleConditionalTransferLightOwnable2Step`**: + instead of the narrow `IERC3643IComplianceContract`, the full ERC-3643 `ICompliance` + interface ID is advertised via the flat mock `IERC3643ComplianceFull` + (`src/mocks/IERC3643ComplianceFull.sol`, `0x3144991c`). These contracts implement all + eight `ICompliance` functions (`bindToken`, `unbindToken`, `isTokenBound`, + `getTokenBound`, `canTransfer`, `transferred`, `created`, `destroyed`). Using + `type(IERC3643Compliance).interfaceId` directly would be wrong because it only XORs + the seven selectors defined directly on `IERC3643Compliance`, missing `canTransfer` + and `transferred` which come from parent interfaces. The flat mock redeclares all eight + so the XOR is correct. + +Final state of each `supportsInterface`: + +**`RuleTransferValidation`** (cascades to all validation rules): + +```solidity +function supportsInterface(bytes4 interfaceId) public view virtual 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(IERC3643IComplianceContract).interfaceId; +} +``` + +**`RuleConditionalTransferLight`**: + +```solidity +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); +} +``` + +**`RuleConditionalTransferLightOwnable2Step`**: + +```solidity +function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return interfaceId == type(IERC165).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; +} +``` + +The convention is now documented in `CLAUDE.md` and `AGENTS.md`: +> `type(IFoo).interfaceId` only XORs selectors defined directly on `IFoo`. Always use +> pre-computed library constants. If no constant exists, define a flat mock interface +> that redeclares the full inheritance tree and use `type(IFooFlattened).interfaceId`. + +--- + +## I1 — RuleERC2980 docs omit frozen spender on `transferFrom` + +**Severity:** Info | **Confidence:** High | **Verdict: Fixed (doc only)** + +### Finding + +`RuleERC2980Base._detectTransferRestrictionFrom` correctly returns +`CODE_ADDRESS_SPENDER_IS_FROZEN` (code 62) when `_isFrozen(spender)` is true. However, +the README, `AGENTS.md`, and `CLAUDE.md` described the frozenlist as blocking only +`from` and `to`: + +> "frozen addresses are completely blocked — they can neither send nor receive tokens" + +The spender path for `transferFrom` was never mentioned, causing integrators to not +anticipate that freezing an exchange or escrow address also blocks its `transferFrom` +delegation. + +### Assessment + +Valid — code is correct, documentation is incomplete. No security vulnerability: the +behaviour is more restrictive (safer) than documented, so no funds can be stolen, only +unexpected reverts. + +### Fix + +**Commit:** `8926e0a` + +Documentation updated in `README.md`, `AGENTS.md`, and `CLAUDE.md`: + +> "Frozen addresses are completely blocked — they can neither send nor receive tokens. +> Additionally, a frozen address acting as a `transferFrom` spender will have the +> transfer rejected (code 62), even if `from` and `to` are not frozen." + +The `RuleERC2980` summary table row was updated to: + +> "ERC-2980 Swiss Compliant rule: whitelist (recipient-only) + frozenlist (blocks +> sender, recipient, **and spender for `transferFrom`**); frozenlist takes priority." + +--- + +## I2 — `hasRole` override: admin implicitly passes all role checks + +**Severity:** Info | **Confidence:** High | **Verdict: Fixed (doc only)** + +### Finding + +`AccessControlModuleStandalone.hasRole` overrides the OpenZeppelin implementation so that +any account holding `DEFAULT_ADMIN_ROLE` returns `true` for every role check: + +```solidity +if (AccessControl.hasRole(DEFAULT_ADMIN_ROLE, account)) { + return true; +} +``` + +This diverges from standard OpenZeppelin semantics where `DEFAULT_ADMIN_ROLE` only +controls who can grant/revoke other roles. Consequences noted by the auditor: + +- `grantRole` to a default admin is a no-op — `_grantRole` skips storage and no + `RoleGranted` event is emitted; the admin is absent from enumeration. +- `revokeRole` / `renounceRole` emit `RoleRevoked` and clear storage, but `hasRole` + continues to return `true`. Effective access is unchanged. +- Off-chain monitoring relying on events or enumerable queries will incorrectly believe + the admin lost a role. + +### Assessment + +**Intentional by design — no code change.** The OpenZeppelin `DEFAULT_ADMIN_ROLE` holder +can already grant itself any role at any time. Treating it as implicitly holding all roles +removes unnecessary ceremony and simplifies access management for regulated token issuers. +The misleading event and enumeration behaviour is acknowledged and documented. + +### Fix + +**Commit:** `769ec2d` + +A dedicated sub-section added to `README.md` under *Access Control* documenting: + +- The intentional `hasRole` override and its rationale. +- `grantRole` no-op behaviour and enumeration absence for admin accounts. +- `revokeRole` / `renounceRole` misleading semantics; `DEFAULT_ADMIN_ROLE` itself must be + revoked to fully remove access. +- Recommendation for off-chain tooling to use `hasRole` queries rather than role events + or enumeration. + +**Possible future improvement:** introduce a `hasPermission` helper alongside a dedicated +`onlyRoleOrAdmin` modifier, keeping `hasRole` standard (no override) while preserving the +admin bypass only where explicitly needed: + +```solidity +function hasPermission(bytes32 role, address account) public view returns (bool) { + return AccessControl.hasRole(DEFAULT_ADMIN_ROLE, account) + || AccessControl.hasRole(role, account); +} + +modifier onlyRoleOrAdmin(bytes32 role) { + require(hasPermission(role, _msgSender()), ...); + _; +} +``` + +This would restore event and enumeration consistency without changing the effective access +model. diff --git a/doc/security/audits/tools/v0.2.0/ackee-wake-arenav0.2.0.pdf b/doc/security/audits/tools/v0.2.0/ackee-wake-arenav0.2.0.pdf new file mode 100644 index 0000000..e121299 Binary files /dev/null and b/doc/security/audits/tools/v0.2.0/ackee-wake-arenav0.2.0.pdf differ diff --git a/doc/specification/RulesSpecificationv0.2.0.pdf b/doc/specification/RulesSpecificationv0.2.0.pdf new file mode 100644 index 0000000..4356e8a Binary files /dev/null and b/doc/specification/RulesSpecificationv0.2.0.pdf differ diff --git a/doc/specification/cover_page.odg b/doc/specification/cover_page.odg index 6b8293f..855eff6 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 new file mode 100644 index 0000000..0b3519c Binary files /dev/null and b/doc/specification/cover_page.pdf differ diff --git a/src/mocks/IERC3643ComplianceFull.sol b/src/mocks/IERC3643ComplianceFull.sol new file mode 100644 index 0000000..44a1be7 --- /dev/null +++ b/src/mocks/IERC3643ComplianceFull.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +/** + * @title IERC3643ComplianceFull + * @dev Flat interface redeclaring the complete ERC-3643 ICompliance function set, + * including functions inherited by IERC3643Compliance from its parent interfaces + * (IERC3643ComplianceRead.canTransfer, IERC3643IComplianceContract.transferred). + * + * Purpose: computing the correct ERC-165 interface ID for the full ERC-3643 + * ICompliance interface via `type(IERC3643ComplianceFull).interfaceId`. + * + * Background: `type(IFoo).interfaceId` only XORs selectors defined *directly* on + * `IFoo`, not those inherited from parent interfaces. Using `type(IERC3643Compliance).interfaceId` + * would therefore miss `canTransfer` and `transferred`. This flat interface + * redeclares all eight functions so the XOR covers the full hierarchy. + * + * Do NOT use this interface as a type annotation or for casting — use the actual + * `IERC3643Compliance` (from RuleEngine) for that. + * + * 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 + function transferred(address from, address to, uint256 value) external; + // From IERC3643Compliance (directly defined) + function bindToken(address token) external; + function unbindToken(address token) external; + function isTokenBound(address token) external view returns (bool isBound); + function getTokenBound() external view returns (address token); + function created(address to, uint256 value) external; + function destroyed(address from, uint256 value) external; +} diff --git a/src/mocks/IdentityRegistryMock.sol b/src/mocks/IdentityRegistryMock.sol index c1f4349..936a26f 100644 --- a/src/mocks/IdentityRegistryMock.sol +++ b/src/mocks/IdentityRegistryMock.sol @@ -6,6 +6,10 @@ import {IIdentityRegistryVerified} from "src/rules/interfaces/IIdentityRegistry. contract IdentityRegistryMock is IIdentityRegistryVerified { mapping(address => bool) private verified; + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function setVerified(address user, bool verified_) external { verified[user] = verified_; } diff --git a/src/mocks/MockERC20TransferFromFalse.sol b/src/mocks/MockERC20TransferFromFalse.sol index 9148545..96469fe 100644 --- a/src/mocks/MockERC20TransferFromFalse.sol +++ b/src/mocks/MockERC20TransferFromFalse.sol @@ -4,6 +4,10 @@ pragma solidity ^0.8.20; contract MockERC20TransferFromFalse { mapping(address => mapping(address => uint256)) private _allowances; + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function setAllowance(address owner, address spender, uint256 value) external { _allowances[owner][spender] = value; } diff --git a/src/mocks/MockERC20WithTransferContext.sol b/src/mocks/MockERC20WithTransferContext.sol index aac490d..20d2a02 100644 --- a/src/mocks/MockERC20WithTransferContext.sol +++ b/src/mocks/MockERC20WithTransferContext.sol @@ -7,8 +7,16 @@ import {ITransferContext} from "src/rules/interfaces/ITransferContext.sol"; contract MockERC20WithTransferContext is ERC20 { ITransferContext public rule; + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function setRule(address rule_) external { rule = ITransferContext(rule_); } @@ -17,19 +25,6 @@ contract MockERC20WithTransferContext is ERC20 { _mint(to, value); } - function transfer(address to, uint256 value) public virtual override returns (bool) { - bool success = super.transfer(to, value); - _notifyFungible(_msgSender(), _msgSender(), to, value); - return success; - } - - function transferFrom(address from, address to, uint256 value) public virtual override returns (bool) { - address sender = _msgSender(); - bool success = super.transferFrom(from, to, value); - _notifyFungible(sender, from, to, value); - return success; - } - function transferWithContext(address to, uint256 value, bool useFungibleContext, uint256 tokenId) external returns (bool) @@ -59,6 +54,27 @@ contract MockERC20WithTransferContext is ERC20 { return true; } + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function transfer(address to, uint256 value) public virtual override returns (bool) { + bool success = super.transfer(to, value); + _notifyFungible(_msgSender(), _msgSender(), to, value); + return success; + } + + function transferFrom(address from, address to, uint256 value) public virtual override returns (bool) { + address sender = _msgSender(); + bool success = super.transferFrom(from, to, value); + _notifyFungible(sender, from, to, value); + return success; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _notifyFungible(address sender, address from, address to, uint256 value) internal { if (address(rule) == address(0)) { return; diff --git a/src/mocks/MockERC721WithTransferContext.sol b/src/mocks/MockERC721WithTransferContext.sol index 38b0977..50963d4 100644 --- a/src/mocks/MockERC721WithTransferContext.sol +++ b/src/mocks/MockERC721WithTransferContext.sol @@ -7,8 +7,16 @@ import {ITransferContext} from "src/rules/interfaces/ITransferContext.sol"; contract MockERC721WithTransferContext is ERC721 { ITransferContext public rule; + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function setRule(address rule_) external { rule = ITransferContext(rule_); } @@ -17,12 +25,20 @@ contract MockERC721WithTransferContext is ERC721 { _mint(to, tokenId); } + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function transferFrom(address from, address to, uint256 tokenId) public virtual override { address sender = _msgSender(); super.transferFrom(from, to, tokenId); _notifyRule(sender, from, to, tokenId); } + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + 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 b81a377..41d586d 100644 --- a/src/mocks/SanctionListOracle.sol +++ b/src/mocks/SanctionListOracle.sol @@ -6,6 +6,10 @@ import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; contract SanctionListOracle is ISanctionsList { mapping(address => bool) private sanctionedAddresses; + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function addToSanctionsList(address newSanction) public { sanctionedAddresses[newSanction] = true; } diff --git a/src/mocks/TotalSupplyMock.sol b/src/mocks/TotalSupplyMock.sol index 92a6485..3b72179 100644 --- a/src/mocks/TotalSupplyMock.sol +++ b/src/mocks/TotalSupplyMock.sol @@ -4,11 +4,15 @@ pragma solidity ^0.8.20; contract TotalSupplyMock { uint256 private _totalSupply; - function totalSupply() external view returns (uint256) { - return _totalSupply; - } + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ function setTotalSupply(uint256 newTotalSupply) external { _totalSupply = newTotalSupply; } + + function totalSupply() external view returns (uint256) { + return _totalSupply; + } } diff --git a/src/mocks/harness/DeploymentCoverageHarnesses.sol b/src/mocks/harness/DeploymentCoverageHarnesses.sol index d3e3d17..7194cfe 100644 --- a/src/mocks/harness/DeploymentCoverageHarnesses.sol +++ b/src/mocks/harness/DeploymentCoverageHarnesses.sol @@ -13,82 +13,154 @@ import {RuleWhitelistWrapperOwnable2Step} from "src/rules/validation/deployment/ import {RuleERC2980Ownable2Step} from "src/rules/validation/deployment/RuleERC2980Ownable2Step.sol"; contract RuleBlacklistHarness is RuleBlacklist { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address admin, address forwarderIrrevocable) RuleBlacklist(admin, forwarderIrrevocable) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } contract RuleWhitelistHarness is RuleWhitelist { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address admin, address forwarderIrrevocable, bool checkSpender_) RuleWhitelist(admin, forwarderIrrevocable, checkSpender_) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } contract RuleWhitelistWrapperHarness is RuleWhitelistWrapper { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address admin, address forwarderIrrevocable, bool checkSpender_) RuleWhitelistWrapper(admin, forwarderIrrevocable, checkSpender_) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } contract RuleERC2980Harness is RuleERC2980 { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address admin, address forwarderIrrevocable) RuleERC2980(admin, forwarderIrrevocable) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } contract RuleSanctionsListHarness is RuleSanctionsList { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address admin, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) RuleSanctionsList(admin, forwarderIrrevocable, sanctionContractOracle_) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } contract RuleBlacklistOwnable2StepHarness is RuleBlacklistOwnable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address forwarderIrrevocable) RuleBlacklistOwnable2Step(owner, forwarderIrrevocable) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } contract RuleWhitelistOwnable2StepHarness is RuleWhitelistOwnable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address forwarderIrrevocable, bool checkSpender_) RuleWhitelistOwnable2Step(owner, forwarderIrrevocable, checkSpender_) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } contract RuleWhitelistWrapperOwnable2StepHarness is RuleWhitelistWrapperOwnable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address forwarderIrrevocable, bool checkSpender_) RuleWhitelistWrapperOwnable2Step(owner, forwarderIrrevocable, checkSpender_) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } contract RuleERC2980Ownable2StepHarness is RuleERC2980Ownable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address forwarderIrrevocable) RuleERC2980Ownable2Step(owner, forwarderIrrevocable) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } diff --git a/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol b/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol index 8584253..dedfac7 100644 --- a/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol +++ b/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol @@ -5,10 +5,18 @@ import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; import {RuleSanctionsListOwnable2Step} from "src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol"; contract RuleSanctionsListOwnable2StepHarness is RuleSanctionsListOwnable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) RuleSanctionsListOwnable2Step(owner, forwarderIrrevocable, sanctionContractOracle_) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgSender() external view returns (address) { return _msgSender(); } diff --git a/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol b/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol index a6095d9..9aa9d82 100644 --- a/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol +++ b/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol @@ -5,8 +5,16 @@ import {RuleSpenderWhitelist} from "src/rules/validation/deployment/RuleSpenderW import {RuleSpenderWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol"; contract RuleSpenderWhitelistHarness is RuleSpenderWhitelist { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address admin, address forwarderIrrevocable) RuleSpenderWhitelist(admin, forwarderIrrevocable) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgSender() external view returns (address) { return _msgSender(); } @@ -21,10 +29,18 @@ contract RuleSpenderWhitelistHarness is RuleSpenderWhitelist { } contract RuleSpenderWhitelistOwnable2StepHarness is RuleSpenderWhitelistOwnable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address forwarderIrrevocable) RuleSpenderWhitelistOwnable2Step(owner, forwarderIrrevocable) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function exposedMsgSender() external view returns (address) { return _msgSender(); } diff --git a/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol b/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol index 36a74ab..c02598b 100644 --- a/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol +++ b/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol @@ -4,10 +4,18 @@ pragma solidity ^0.8.20; import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol"; contract RuleWhitelistWrapperHarnessInternal is RuleWhitelistWrapper { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address admin, address forwarderIrrevocable, bool checkSpender_) RuleWhitelistWrapper(admin, forwarderIrrevocable, checkSpender_) {} + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + 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 9c5a706..1888ef3 100644 --- a/src/modules/AccessControlModuleStandalone.sol +++ b/src/modules/AccessControlModuleStandalone.sol @@ -9,7 +9,11 @@ import {AccessControlEnumerable} from "OZ/access/extensions/AccessControlEnumera abstract contract AccessControlModuleStandalone is AccessControlEnumerable { error AccessControlModuleStandalone_AddressZeroNotAllowed(); - /* ============ Constructor ============ */ + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + /** * @notice Assigns the provided address as the default admin. * @dev @@ -20,7 +24,6 @@ abstract contract AccessControlModuleStandalone is AccessControlEnumerable { * * @param admin The address that will receive the `DEFAULT_ADMIN_ROLE`. */ - constructor(address admin) { require(admin != address(0), AccessControlModuleStandalone_AddressZeroNotAllowed()); // we don't check the return value @@ -30,8 +33,9 @@ abstract contract AccessControlModuleStandalone is AccessControlEnumerable { } /*////////////////////////////////////////////////////////////// - PUBLIC/EXTERNAL FUNCTIONS + PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** * @dev Returns `true` if `account` has been granted `role`. */ diff --git a/src/modules/VersionModule.sol b/src/modules/VersionModule.sol index 0ae2ee1..8da4a78 100644 --- a/src/modules/VersionModule.sol +++ b/src/modules/VersionModule.sol @@ -8,7 +8,11 @@ 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.2.0"; + string private constant VERSION = "0.3.0"; + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ /// @inheritdoc IERC3643Version function version() public view virtual override returns (string memory version_) { diff --git a/src/rules/operation/RuleConditionalTransferLight.sol b/src/rules/operation/RuleConditionalTransferLight.sol index 1d16d00..d0e70b8 100644 --- a/src/rules/operation/RuleConditionalTransferLight.sol +++ b/src/rules/operation/RuleConditionalTransferLight.sol @@ -5,8 +5,11 @@ import {AccessControlEnumerable} from "OZ/access/extensions/AccessControlEnumera import {IERC165} from "OZ/utils/introspection/IERC165.sol"; import {IRule} from "RuleEngine/interfaces/IRule.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 {ERC3643ComplianceModule} from "RuleEngine/modules/ERC3643ComplianceModule.sol"; import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransferLightBase.sol"; /** @@ -14,16 +17,20 @@ import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransf * @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, - ERC3643ComplianceModule, - RuleConditionalTransferLightBase -{ +contract RuleConditionalTransferLight is AccessControlModuleStandalone, RuleConditionalTransferLightBase { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + /** * @param admin Address of the contract admin. */ constructor(address admin) AccessControlModuleStandalone(admin) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function supportsInterface(bytes4 interfaceId) public view @@ -31,23 +38,19 @@ contract RuleConditionalTransferLight is override(AccessControlEnumerable, IERC165) returns (bool) { - return interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IRule).interfaceId + 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); } - 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); - } + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} - function _authorizeTransferExecution() internal view virtual override { - require(isTokenBound(_msgSender()), RuleConditionalTransferLight_TransferExecutorUnauthorized(_msgSender())); - } - function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} } diff --git a/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol b/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol index 6fcf88f..163623e 100644 --- a/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol +++ b/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol @@ -3,9 +3,12 @@ pragma solidity ^0.8.20; import {Ownable} from "OZ/access/Ownable.sol"; import {Ownable2Step} from "OZ/access/Ownable2Step.sol"; -import {IRule} from "RuleEngine/interfaces/IRule.sol"; import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; import {IERC165} from "OZ/utils/introspection/IERC165.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"; /** @@ -13,14 +16,30 @@ import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransf * @notice Ownable2Step variant of RuleConditionalTransferLight. */ contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLightBase, Ownable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { - return interfaceId == type(IERC165).interfaceId || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID - || interfaceId == type(IRule).interfaceId; + return interfaceId == type(IERC165).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; } + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeTransferApproval() internal view virtual override onlyOwner {} - function _authorizeTransferExecution() internal view virtual override onlyOwner {} + function _onlyComplianceManager() internal virtual override onlyOwner {} } diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol new file mode 100644 index 0000000..83b4b3b --- /dev/null +++ b/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {ITransferContext} from "../../interfaces/ITransferContext.sol"; +import {RuleConditionalTransferLightInvariantStorage} from "./RuleConditionalTransferLightInvariantStorage.sol"; + +/** + * @title RuleConditionalTransferLightApprovalBase + * @dev Pure approval state machine: stores and consumes per-transfer approvals. + * No knowledge of token binding or compliance interfaces. + */ +abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTransferLightInvariantStorage { + // Mapping from transfer hash to approval count + mapping(bytes32 => uint256) public approvalCounts; + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + modifier onlyTransferApprover() { + _authorizeTransferApproval(); + _; + } + + modifier onlyTransferExecutor() { + _authorizeTransferExecution(); + _; + } + + function _authorizeTransferApproval() internal view virtual; + + function _authorizeTransferExecution() internal view virtual; + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function transferred(ITransferContext.FungibleTransferContext calldata ctx) external onlyTransferExecutor { + _transferredFromContext(ctx); + } + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + 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]); + } + + function cancelTransferApproval(address from, address to, uint256 value) public onlyTransferApprover { + bytes32 transferHash = _transferHash(from, to, value); + uint256 count = approvalCounts[transferHash]; + require(count != 0, TransferApprovalNotFound()); + approvalCounts[transferHash] = count - 1; + emit TransferApprovalCancelled(from, to, value, approvalCounts[transferHash]); + } + + function approvedCount(address from, address to, uint256 value) public view returns (uint256) { + bytes32 transferHash = _transferHash(from, to, value); + return approvalCounts[transferHash]; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _transferredFromContext(ITransferContext.FungibleTransferContext calldata ctx) internal virtual { + _transferred(ctx.from, ctx.to, ctx.value); + } + + function _transferred(address from, address to, uint256 value) internal virtual { + if (from == address(0) || to == address(0)) { + return; + } + bytes32 transferHash = _transferHash(from, to, value); + uint256 count = approvalCounts[transferHash]; + + require(count != 0, TransferNotApproved()); + + approvalCounts[transferHash] = count - 1; + emit TransferExecuted(from, to, value, approvalCounts[transferHash]); + } + + 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") { + let ptr := mload(0x40) + mstore(ptr, shl(96, from)) + mstore(add(ptr, 0x20), shl(96, to)) + mstore(add(ptr, 0x40), value) + hash := keccak256(ptr, 0x60) + } + } +} diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol index 3b0a9cb..7b2e609 100644 --- a/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol +++ b/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol @@ -6,50 +6,67 @@ import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC 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 {ITransferContext} from "../../interfaces/ITransferContext.sol"; +import {ERC3643ComplianceModule} from "RuleEngine/modules/ERC3643ComplianceModule.sol"; import {IERC20} from "OZ/token/ERC20/IERC20.sol"; -import {RuleConditionalTransferLightInvariantStorage} from "./RuleConditionalTransferLightInvariantStorage.sol"; +import {RuleConditionalTransferLightApprovalBase} from "./RuleConditionalTransferLightApprovalBase.sol"; import {VersionModule} from "../../../modules/VersionModule.sol"; /** * @title RuleConditionalTransferLightBase - * @dev Requires operator approval for each transfer. Same transfer (from, to, value) - * can be approved multiple times to allow repeated transfers. + * @dev Wires the approval state machine into the ERC-3643 / ERC-1404 / IRule compliance + * interface layer and enforces single-token binding. */ abstract contract RuleConditionalTransferLightBase is VersionModule, - RuleConditionalTransferLightInvariantStorage, + ERC3643ComplianceModule, + RuleConditionalTransferLightApprovalBase, IRule { - // Mapping from transfer hash to approval count - mapping(bytes32 => uint256) public approvalCounts; + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ - 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]); + function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { + return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; } - function cancelTransferApproval(address from, address to, uint256 value) public onlyTransferApprover { - bytes32 transferHash = _transferHash(from, to, value); - uint256 count = approvalCounts[transferHash]; - require(count != 0, TransferApprovalNotFound()); - approvalCounts[transferHash] = count - 1; - emit TransferApprovalCancelled(from, to, value, approvalCounts[transferHash]); + 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; } + 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 + //////////////////////////////////////////////////////////////*/ + /** * @notice Approves and performs a transferFrom using this rule as spender. * @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. */ - function approveAndTransferIfAllowed(address token, address from, address to, uint256 value) + function approveAndTransferIfAllowed(address from, address to, uint256 value) public onlyTransferApprover returns (bool) { - require(token != address(0), RuleConditionalTransferLight_TokenAddressZeroNotAllowed()); + address token = getTokenBound(); + require(token != address(0), RuleConditionalTransferLight_TokenNotBound()); approveTransfer(from, to, value); @@ -61,11 +78,6 @@ abstract contract RuleConditionalTransferLightBase is return true; } - function approvedCount(address from, address to, uint256 value) public view returns (uint256) { - bytes32 transferHash = _transferHash(from, to, value); - return approvalCounts[transferHash]; - } - function transferred(address from, address to, uint256 value) public override(IERC3643IComplianceContract) @@ -88,6 +100,20 @@ abstract contract RuleConditionalTransferLightBase is _transferred(from, to, value); } + /** + * @notice Binds a token to this rule. Reverts if a token is already bound. + * @dev Enforces single-token binding to prevent cross-token approval replay. + * To migrate to a new token, call `unbindToken` first. + * @dev WARNING: `unbindToken` does not clear `approvalCounts`. Stale approvals + * 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. + */ + function bindToken(address token) public override onlyComplianceManager { + require(getTokenBound() == address(0), RuleConditionalTransferLight_TokenAlreadyBound()); + _bindToken(token); + } + function detectTransferRestriction(address from, address to, uint256 value) public view @@ -138,69 +164,11 @@ abstract contract RuleConditionalTransferLightBase is == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } - function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { - return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; - } - - 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; - } - - function transferred(ITransferContext.FungibleTransferContext calldata ctx) external onlyTransferExecutor { - _transferredFromContext(ctx); - } - - function _transferredFromContext(ITransferContext.FungibleTransferContext calldata ctx) internal virtual { - _transferred(ctx.from, ctx.to, ctx.value); - } - - function _transferred(address from, address to, uint256 value) internal virtual { - if (from == address(0) || to == address(0)) { - return; - } - bytes32 transferHash = _transferHash(from, to, value); - uint256 count = approvalCounts[transferHash]; - - require(count != 0, TransferNotApproved()); - - approvalCounts[transferHash] = count - 1; - emit TransferExecuted(from, to, value, approvalCounts[transferHash]); - } - - 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") { - let ptr := mload(0x40) - mstore(ptr, shl(96, from)) - mstore(add(ptr, 0x20), shl(96, to)) - mstore(add(ptr, 0x40), value) - hash := keccak256(ptr, 0x60) - } - } - /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ - modifier onlyTransferApprover() { - _authorizeTransferApproval(); - _; + function _authorizeTransferExecution() internal view override { + require(isTokenBound(_msgSender()), RuleConditionalTransferLight_TransferExecutorUnauthorized(_msgSender())); } - - modifier onlyTransferExecutor() { - _authorizeTransferExecution(); - _; - } - - function _authorizeTransferApproval() internal view virtual; - - function _authorizeTransferExecution() internal view virtual; } diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol b/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol index 83639b2..08b3d6f 100644 --- a/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol +++ b/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol @@ -12,18 +12,19 @@ abstract contract RuleConditionalTransferLightInvariantStorage is RuleSharedInva // It is very important that each rule uses an unique code uint8 public constant CODE_TRANSFER_REQUEST_NOT_APPROVED = 46; + /* ============ Events ============ */ + event TransferApproved(address indexed from, address indexed to, uint256 value, uint256 count); + event TransferExecuted(address indexed from, address indexed to, uint256 value, uint256 remaining); + event TransferApprovalCancelled(address indexed from, address indexed to, uint256 value, uint256 remaining); + /* ============ Custom error ============ */ error RuleConditionalTransferLight_TransferExecutorUnauthorized(address account); - error RuleConditionalTransferLight_TokenAddressZeroNotAllowed(); + error RuleConditionalTransferLight_TokenNotBound(); + error RuleConditionalTransferLight_TokenAlreadyBound(); error RuleConditionalTransferLight_InsufficientAllowance( address token, address owner, uint256 allowance, uint256 required ); error RuleConditionalTransferLight_TransferFailed(); error TransferNotApproved(); error TransferApprovalNotFound(); - - /* ============ Events ============ */ - event TransferApproved(address indexed from, address indexed to, uint256 value, uint256 count); - event TransferExecuted(address indexed from, address indexed to, uint256 value, uint256 remaining); - event TransferApprovalCancelled(address indexed from, address indexed to, uint256 value, uint256 remaining); } diff --git a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol index 40e6e71..a183d2e 100644 --- a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol +++ b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol @@ -23,10 +23,6 @@ abstract contract RuleAddressSet is RuleAddressSetInvariantStorage, IAddressList { - /*////////////////////////////////////////////////////////////// - STATE - //////////////////////////////////////////////////////////////*/ - /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -38,7 +34,25 @@ abstract contract RuleAddressSet is constructor(address forwarderIrrevocable) MetaTxModuleStandalone(forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// - CORE LOGIC + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + modifier onlyAddressListAdd() { + _authorizeAddressListAdd(); + _; + } + + modifier onlyAddressListRemove() { + _authorizeAddressListRemove(); + _; + } + + function _authorizeAddressListAdd() internal view virtual; + + function _authorizeAddressListRemove() internal view virtual; + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ /** @@ -91,24 +105,6 @@ abstract contract RuleAddressSet is emit RemoveAddress(targetAddress); } - /*////////////////////////////////////////////////////////////// - ACCESS CONTROL - //////////////////////////////////////////////////////////////*/ - - modifier onlyAddressListAdd() { - _authorizeAddressListAdd(); - _; - } - - modifier onlyAddressListRemove() { - _authorizeAddressListRemove(); - _; - } - - function _authorizeAddressListAdd() internal view virtual; - - function _authorizeAddressListRemove() internal view virtual; - /** * @notice Returns the total number of currently listed addresses. * @return count The number of listed addresses. @@ -148,7 +144,7 @@ abstract contract RuleAddressSet is } /*////////////////////////////////////////////////////////////// - ERC-2771 META TX + INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ /// @inheritdoc ERC2771Context diff --git a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol index 7ff5ae9..479b626 100644 --- a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol +++ b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol @@ -23,7 +23,7 @@ abstract contract RuleAddressSetInternal { EnumerableSet.AddressSet private _listedAddresses; /*////////////////////////////////////////////////////////////// - INTERNAL + INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ /** diff --git a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol index 5d1a8a9..d31478e 100644 --- a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol +++ b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol @@ -3,14 +3,14 @@ pragma solidity ^0.8.20; abstract contract RuleAddressSetInvariantStorage { + /* ============ Role ============ */ + bytes32 public constant ADDRESS_LIST_REMOVE_ROLE = keccak256("ADDRESS_LIST_REMOVE_ROLE"); + 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. error RuleAddressSet_AddressAlreadyListed(); /// @notice Thrown when trying to remove an address that is not listed. error RuleAddressSet_AddressNotFound(); - - /* ============ Role ============ */ - bytes32 public constant ADDRESS_LIST_REMOVE_ROLE = keccak256("ADDRESS_LIST_REMOVE_ROLE"); - bytes32 public constant ADDRESS_LIST_ADD_ROLE = keccak256("ADDRESS_LIST_ADD_ROLE"); } diff --git a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol index 02fa84a..a356e7b 100644 --- a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol +++ b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol @@ -5,12 +5,7 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "../../invariant/RuleSharedInvariantStorage.sol"; abstract contract RuleBlacklistInvariantStorage is RuleSharedInvariantStorage { - error RuleBlacklist_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); - error RuleBlacklist_InvalidTransferFrom( - address rule, address spender, address from, address to, uint256 value, uint8 code - ); /* ============ String message ============ */ - string constant TEXT_ADDRESS_FROM_IS_BLACKLISTED = "The sender is blacklisted"; string constant TEXT_ADDRESS_TO_IS_BLACKLISTED = "The recipient is blacklisted"; string constant TEXT_ADDRESS_SPENDER_IS_BLACKLISTED = "The spender is blacklisted"; @@ -20,4 +15,9 @@ abstract contract RuleBlacklistInvariantStorage is RuleSharedInvariantStorage { uint8 public constant CODE_ADDRESS_FROM_IS_BLACKLISTED = 36; uint8 public constant CODE_ADDRESS_TO_IS_BLACKLISTED = 37; uint8 public constant CODE_ADDRESS_SPENDER_IS_BLACKLISTED = 38; + + error RuleBlacklist_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); + error RuleBlacklist_InvalidTransferFrom( + address rule, address spender, 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 eed9da1..59fc65f 100644 --- a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol +++ b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol @@ -5,12 +5,7 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "../../invariant/RuleSharedInvariantStorage.sol"; abstract contract RuleWhitelistInvariantStorage is RuleSharedInvariantStorage { - error RuleWhitelist_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); - error RuleWhitelist_InvalidTransferFrom( - address rule, address spender, address from, address to, uint256 value, uint8 code - ); /* ============ String message ============ */ - string constant TEXT_ADDRESS_FROM_NOT_WHITELISTED = "The sender is not in the whitelist"; string constant TEXT_ADDRESS_TO_NOT_WHITELISTED = "The recipient is not in the whitelist"; string constant TEXT_ADDRESS_SPENDER_NOT_WHITELISTED = "The spender is not in the whitelist"; @@ -24,4 +19,9 @@ abstract contract RuleWhitelistInvariantStorage is RuleSharedInvariantStorage { /* ============ Events ============ */ /// @dev Emitted when the `checkSpender` flag is updated. event CheckSpenderUpdated(bool newValue); + + error RuleWhitelist_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); + error RuleWhitelist_InvalidTransferFrom( + address rule, address spender, address from, address to, uint256 value, uint8 code + ); } diff --git a/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol b/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol index 8160ff8..0de56e5 100644 --- a/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol +++ b/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol @@ -4,14 +4,6 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "../../invariant/RuleSharedInvariantStorage.sol"; abstract contract RuleERC2980InvariantStorage is RuleSharedInvariantStorage { - /* ============ Custom errors ============ */ - error RuleERC2980_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); - error RuleERC2980_InvalidTransferFrom( - address rule, address spender, address from, address to, uint256 value, uint8 code - ); - error RuleERC2980_AddressAlreadyListed(); - error RuleERC2980_AddressNotFound(); - /* ============ String message ============ */ string constant TEXT_ADDRESS_FROM_IS_FROZEN = "The sender address is frozen"; string constant TEXT_ADDRESS_TO_IS_FROZEN = "The recipient address is frozen"; @@ -49,4 +41,12 @@ abstract contract RuleERC2980InvariantStorage is RuleSharedInvariantStorage { event AddFrozenlistAddress(address indexed targetAddress); /// @notice Emitted when a single address is removed from the frozenlist. event RemoveFrozenlistAddress(address indexed targetAddress); + + /* ============ Custom errors ============ */ + error RuleERC2980_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); + error RuleERC2980_InvalidTransferFrom( + address rule, address spender, address from, address to, uint256 value, uint8 code + ); + error RuleERC2980_AddressAlreadyListed(); + error RuleERC2980_AddressNotFound(); } diff --git a/src/rules/validation/abstract/base/RuleBlacklistBase.sol b/src/rules/validation/abstract/base/RuleBlacklistBase.sol index 9e6d32c..e066d95 100644 --- a/src/rules/validation/abstract/base/RuleBlacklistBase.sol +++ b/src/rules/validation/abstract/base/RuleBlacklistBase.sol @@ -15,36 +15,40 @@ import {IRule} from "RuleEngine/interfaces/IRule.sol"; * @notice Core blacklist logic without access-control policy. */ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlacklistInvariantStorage { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address forwarderIrrevocable) RuleAddressSet(forwarderIrrevocable) {} - function _detectTransferRestriction( - address from, - address to, - uint256 /* value */ - ) - internal + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IERC3643IComplianceContract + * @dev Validation only; does not modify state. + */ + function transferred(address from, address to, uint256 value) + public view - override - returns (uint8) + virtual + override(IERC3643IComplianceContract) { - if (isAddressListed(from)) { - return CODE_ADDRESS_FROM_IS_BLACKLISTED; - } else if (isAddressListed(to)) { - return CODE_ADDRESS_TO_IS_BLACKLISTED; - } - return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + _transferred(from, to, value); } - function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - internal + /** + * @inheritdoc IRuleEngine + * @dev Validation only; does not modify state. + */ + function transferred(address spender, address from, address to, uint256 value) + public view - override - returns (uint8) + virtual + override(IRuleEngine) { - if (isAddressListed(spender)) { - return CODE_ADDRESS_SPENDER_IS_BLACKLISTED; - } - return _detectTransferRestriction(from, to, value); + _transferredFrom(spender, from, to, value); } function canReturnTransferRestrictionCode(uint8 restrictionCode) @@ -80,30 +84,38 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack return RuleTransferValidation.supportsInterface(interfaceId); } - /** - * @inheritdoc IERC3643IComplianceContract - * @dev Validation only; does not modify state. - */ - function transferred(address from, address to, uint256 value) - public + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _detectTransferRestriction( + address from, + address to, + uint256 /* value */ + ) + internal view - virtual - override(IERC3643IComplianceContract) + override + returns (uint8) { - _transferred(from, to, value); + if (isAddressListed(from)) { + return CODE_ADDRESS_FROM_IS_BLACKLISTED; + } else if (isAddressListed(to)) { + return CODE_ADDRESS_TO_IS_BLACKLISTED; + } + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } - /** - * @inheritdoc IRuleEngine - * @dev Validation only; does not modify state. - */ - function transferred(address spender, address from, address to, uint256 value) - public + function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + internal view - virtual - override(IRuleEngine) + override + returns (uint8) { - _transferredFrom(spender, from, to, value); + if (isAddressListed(spender)) { + return CODE_ADDRESS_SPENDER_IS_BLACKLISTED; + } + return _detectTransferRestriction(from, to, value); } function _transferred(address from, address to, uint256 value) internal view virtual override { diff --git a/src/rules/validation/abstract/base/RuleERC2980Base.sol b/src/rules/validation/abstract/base/RuleERC2980Base.sol index 247e64f..a1345af 100644 --- a/src/rules/validation/abstract/base/RuleERC2980Base.sol +++ b/src/rules/validation/abstract/base/RuleERC2980Base.sol @@ -41,118 +41,33 @@ abstract contract RuleERC2980Base is constructor(address forwarderIrrevocable) MetaTxModuleStandalone(forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// - TRANSFER RESTRICTION LOGIC - //////////////////////////////////////////////////////////////*/ - - function _detectTransferRestriction( - address from, - address to, - uint256 /* value */ - ) - internal - view - virtual - override - returns (uint8) - { - // Frozenlist check has priority - if (_isFrozen(from)) { - return CODE_ADDRESS_FROM_IS_FROZEN; - } else if (_isFrozen(to)) { - return CODE_ADDRESS_TO_IS_FROZEN; - } - // Whitelist check: only the recipient must be whitelisted - if (!_isWhitelisted(to)) { - return CODE_ADDRESS_TO_NOT_WHITELISTED; - } - return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - } - - function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - internal - view - virtual - override - returns (uint8) - { - if (_isFrozen(spender)) { - return CODE_ADDRESS_SPENDER_IS_FROZEN; - } - return _detectTransferRestriction(from, to, value); - } - - /*////////////////////////////////////////////////////////////// - ERC-3643 / IRuleEngine HOOKS + ACCESS CONTROL //////////////////////////////////////////////////////////////*/ - function transferred(address from, address to, uint256 value) - public - view - virtual - override(IERC3643IComplianceContract) - { - _transferred(from, to, value); - } - - function transferred(address spender, address from, address to, uint256 value) - public - view - virtual - override(IRuleEngine) - { - _transferredFrom(spender, from, to, value); + modifier onlyWhitelistAdd() { + _authorizeWhitelistAdd(); + _; } - function _transferred(address from, address to, uint256 value) internal view virtual override { - uint8 code = _detectTransferRestriction(from, to, value); - require( - code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), - RuleERC2980_InvalidTransfer(address(this), from, to, value, code) - ); + modifier onlyWhitelistRemove() { + _authorizeWhitelistRemove(); + _; } - function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { - uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); - require( - code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), - RuleERC2980_InvalidTransferFrom(address(this), spender, from, to, value, code) - ); + modifier onlyFrozenlistAdd() { + _authorizeFrozenlistAdd(); + _; } - /*////////////////////////////////////////////////////////////// - RESTRICTION CODE HELPERS - //////////////////////////////////////////////////////////////*/ - - function canReturnTransferRestrictionCode(uint8 restrictionCode) - public - pure - virtual - override(IRule) - returns (bool) - { - return restrictionCode == CODE_ADDRESS_FROM_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_IS_FROZEN - || restrictionCode == CODE_ADDRESS_SPENDER_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED; + modifier onlyFrozenlistRemove() { + _authorizeFrozenlistRemove(); + _; } - function messageForTransferRestriction(uint8 restrictionCode) - public - pure - virtual - override(IERC1404) - returns (string memory) - { - if (restrictionCode == CODE_ADDRESS_FROM_IS_FROZEN) { - return TEXT_ADDRESS_FROM_IS_FROZEN; - } else if (restrictionCode == CODE_ADDRESS_TO_IS_FROZEN) { - return TEXT_ADDRESS_TO_IS_FROZEN; - } else if (restrictionCode == CODE_ADDRESS_SPENDER_IS_FROZEN) { - return TEXT_ADDRESS_SPENDER_IS_FROZEN; - } else if (restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED) { - return TEXT_ADDRESS_TO_NOT_WHITELISTED; - } else { - return TEXT_CODE_NOT_FOUND; - } - } + function _authorizeWhitelistAdd() internal view virtual; + function _authorizeWhitelistRemove() internal view virtual; + function _authorizeFrozenlistAdd() internal view virtual; + function _authorizeFrozenlistRemove() internal view virtual; /*////////////////////////////////////////////////////////////// WHITELIST MANAGEMENT @@ -204,46 +119,6 @@ abstract contract RuleERC2980Base is emit RemoveWhitelistAddress(targetAddress); } - /** - * @notice Returns the number of whitelisted addresses. - */ - function whitelistAddressCount() public view returns (uint256) { - return _whitelistCount(); - } - - /** - * @notice Returns true if the address is in the whitelist. - */ - function isWhitelisted(address targetAddress) public view returns (bool) { - return _isWhitelisted(targetAddress); - } - - /** - * @notice ERC-2980 getter: returns true if the address is whitelisted. - */ - function whitelist(address _operator) public view virtual override(IERC2980) returns (bool) { - return _isWhitelisted(_operator); - } - - /** - * @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. - */ - function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { - return _isWhitelisted(targetAddress); - } - - /** - * @notice Checks multiple addresses for whitelist membership. - */ - function areWhitelisted(address[] memory targetAddresses) public view returns (bool[] memory results) { - results = new bool[](targetAddresses.length); - for (uint256 i = 0; i < targetAddresses.length; ++i) { - results[i] = _isWhitelisted(targetAddresses[i]); - } - } - /*////////////////////////////////////////////////////////////// FROZENLIST MANAGEMENT //////////////////////////////////////////////////////////////*/ @@ -294,6 +169,103 @@ abstract contract RuleERC2980Base is emit RemoveFrozenlistAddress(targetAddress); } + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function transferred(address from, address to, uint256 value) + public + view + virtual + override(IERC3643IComplianceContract) + { + _transferred(from, to, value); + } + + function transferred(address spender, address from, address to, uint256 value) + public + view + virtual + override(IRuleEngine) + { + _transferredFrom(spender, from, to, value); + } + + function canReturnTransferRestrictionCode(uint8 restrictionCode) + public + pure + virtual + override(IRule) + returns (bool) + { + return restrictionCode == CODE_ADDRESS_FROM_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_IS_FROZEN + || restrictionCode == CODE_ADDRESS_SPENDER_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED; + } + + function messageForTransferRestriction(uint8 restrictionCode) + public + pure + virtual + override(IERC1404) + returns (string memory) + { + if (restrictionCode == CODE_ADDRESS_FROM_IS_FROZEN) { + return TEXT_ADDRESS_FROM_IS_FROZEN; + } else if (restrictionCode == CODE_ADDRESS_TO_IS_FROZEN) { + return TEXT_ADDRESS_TO_IS_FROZEN; + } else if (restrictionCode == CODE_ADDRESS_SPENDER_IS_FROZEN) { + return TEXT_ADDRESS_SPENDER_IS_FROZEN; + } else if (restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED) { + return TEXT_ADDRESS_TO_NOT_WHITELISTED; + } else { + return TEXT_CODE_NOT_FOUND; + } + } + + function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { + return RuleTransferValidation.supportsInterface(interfaceId); + } + + /** + * @notice Returns the number of whitelisted addresses. + */ + function whitelistAddressCount() public view returns (uint256) { + return _whitelistCount(); + } + + /** + * @notice Returns true if the address is in the whitelist. + */ + function isWhitelisted(address targetAddress) public view returns (bool) { + return _isWhitelisted(targetAddress); + } + + /** + * @notice ERC-2980 getter: returns true if the address is whitelisted. + */ + function whitelist(address _operator) public view virtual override(IERC2980) returns (bool) { + return _isWhitelisted(_operator); + } + + /** + * @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. + */ + function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { + return _isWhitelisted(targetAddress); + } + + /** + * @notice Checks multiple addresses for whitelist membership. + */ + function areWhitelisted(address[] memory targetAddresses) public view returns (bool[] memory results) { + results = new bool[](targetAddresses.length); + for (uint256 i = 0; i < targetAddresses.length; ++i) { + results[i] = _isWhitelisted(targetAddresses[i]); + } + } + /** * @notice Returns the number of frozen addresses. */ @@ -326,46 +298,62 @@ abstract contract RuleERC2980Base is } /*////////////////////////////////////////////////////////////// - INTERFACE SUPPORT + INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ - function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { - return RuleTransferValidation.supportsInterface(interfaceId); - } - - /*////////////////////////////////////////////////////////////// - ACCESS CONTROL - //////////////////////////////////////////////////////////////*/ - - modifier onlyWhitelistAdd() { - _authorizeWhitelistAdd(); - _; + function _detectTransferRestriction( + address from, + address to, + uint256 /* value */ + ) + internal + view + virtual + override + returns (uint8) + { + // Frozenlist check has priority + if (_isFrozen(from)) { + return CODE_ADDRESS_FROM_IS_FROZEN; + } else if (_isFrozen(to)) { + return CODE_ADDRESS_TO_IS_FROZEN; + } + // Whitelist check: only the recipient must be whitelisted + if (!_isWhitelisted(to)) { + return CODE_ADDRESS_TO_NOT_WHITELISTED; + } + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } - modifier onlyWhitelistRemove() { - _authorizeWhitelistRemove(); - _; + function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + internal + view + virtual + override + returns (uint8) + { + if (_isFrozen(spender)) { + return CODE_ADDRESS_SPENDER_IS_FROZEN; + } + return _detectTransferRestriction(from, to, value); } - modifier onlyFrozenlistAdd() { - _authorizeFrozenlistAdd(); - _; + function _transferred(address from, address to, uint256 value) internal view virtual override { + uint8 code = _detectTransferRestriction(from, to, value); + require( + code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), + RuleERC2980_InvalidTransfer(address(this), from, to, value, code) + ); } - modifier onlyFrozenlistRemove() { - _authorizeFrozenlistRemove(); - _; + function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { + uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); + require( + code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), + RuleERC2980_InvalidTransferFrom(address(this), spender, from, to, value, code) + ); } - function _authorizeWhitelistAdd() internal view virtual; - function _authorizeWhitelistRemove() internal view virtual; - function _authorizeFrozenlistAdd() internal view virtual; - function _authorizeFrozenlistRemove() internal view virtual; - - /*////////////////////////////////////////////////////////////// - ERC-2771 META TX - //////////////////////////////////////////////////////////////*/ - function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { return ERC2771Context._msgSender(); } diff --git a/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol b/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol index 31d400d..cbb2b95 100644 --- a/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol +++ b/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol @@ -16,12 +16,40 @@ import {IIdentityRegistryVerified} from "../../../interfaces/IIdentityRegistry.s abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegistryInvariantStorage { IIdentityRegistryVerified public identityRegistry; + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address identityRegistry_) { if (identityRegistry_ != address(0)) { identityRegistry = IIdentityRegistryVerified(identityRegistry_); } } + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + modifier onlyIdentityRegistryManager() { + _authorizeIdentityRegistryManager(); + _; + } + + function _authorizeIdentityRegistryManager() internal view virtual; + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + 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; + } + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function setIdentityRegistry(address newRegistry) public onlyIdentityRegistryManager { require(newRegistry != address(0), RuleIdentityRegistry_RegistryAddressZeroNotAllowed()); identityRegistry = IIdentityRegistryVerified(newRegistry); @@ -33,6 +61,34 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist emit IdentityRegistryUpdated(address(0)); } + function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { + _transferred(from, to, value); + } + + function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { + _transferredFrom(spender, from, to, value); + } + + function messageForTransferRestriction(uint8 restrictionCode) + public + pure + override(IERC1404) + returns (string memory) + { + if (restrictionCode == CODE_ADDRESS_FROM_NOT_VERIFIED) { + return TEXT_ADDRESS_FROM_NOT_VERIFIED; + } else if (restrictionCode == CODE_ADDRESS_TO_NOT_VERIFIED) { + return TEXT_ADDRESS_TO_NOT_VERIFIED; + } else if (restrictionCode == CODE_ADDRESS_SPENDER_NOT_VERIFIED) { + return TEXT_ADDRESS_SPENDER_NOT_VERIFIED; + } + return TEXT_CODE_NOT_FOUND; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _detectTransferRestriction( address from, address to, @@ -77,14 +133,6 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist return _detectTransferRestriction(from, to, value); } - function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { - _transferred(from, to, value); - } - - function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { - _transferredFrom(spender, from, to, value); - } - function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -101,35 +149,4 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist ); } - 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; - } - - function messageForTransferRestriction(uint8 restrictionCode) - public - pure - override(IERC1404) - returns (string memory) - { - if (restrictionCode == CODE_ADDRESS_FROM_NOT_VERIFIED) { - return TEXT_ADDRESS_FROM_NOT_VERIFIED; - } else if (restrictionCode == CODE_ADDRESS_TO_NOT_VERIFIED) { - return TEXT_ADDRESS_TO_NOT_VERIFIED; - } else if (restrictionCode == CODE_ADDRESS_SPENDER_NOT_VERIFIED) { - return TEXT_ADDRESS_SPENDER_NOT_VERIFIED; - } - return TEXT_CODE_NOT_FOUND; - } - - /*////////////////////////////////////////////////////////////// - ACCESS CONTROL - //////////////////////////////////////////////////////////////*/ - - modifier onlyIdentityRegistryManager() { - _authorizeIdentityRegistryManager(); - _; - } - - function _authorizeIdentityRegistryManager() internal view virtual; } diff --git a/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol b/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol index 55e07cd..8aebb27 100644 --- a/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol +++ b/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol @@ -17,12 +17,28 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal ITotalSupply public tokenContract; uint256 public maxTotalSupply; + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address tokenContract_, uint256 maxTotalSupply_) { require(tokenContract_ != address(0), RuleMaxTotalSupply_TokenAddressZeroNotAllowed()); tokenContract = ITotalSupply(tokenContract_); maxTotalSupply = maxTotalSupply_; } + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { + return restrictionCode == CODE_MAX_TOTAL_SUPPLY_EXCEEDED; + } + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function setMaxTotalSupply(uint256 newMaxTotalSupply) public onlyMaxTotalSupplyManager { maxTotalSupply = newMaxTotalSupply; emit MaxTotalSupplyUpdated(newMaxTotalSupply); @@ -34,6 +50,41 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal emit TokenContractUpdated(newTokenContract); } + function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { + _transferred(from, to, value); + } + + function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { + _transferredFrom(spender, from, to, value); + } + + function messageForTransferRestriction(uint8 restrictionCode) + public + pure + override(IERC1404) + returns (string memory) + { + if (restrictionCode == CODE_MAX_TOTAL_SUPPLY_EXCEEDED) { + return TEXT_MAX_TOTAL_SUPPLY_EXCEEDED; + } + return TEXT_CODE_NOT_FOUND; + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + modifier onlyMaxTotalSupplyManager() { + _authorizeMaxTotalSupplyManager(); + _; + } + + function _authorizeMaxTotalSupplyManager() internal view virtual; + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _detectTransferRestriction( address from, address, @@ -63,14 +114,6 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal return _detectTransferRestriction(from, to, value); } - function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { - _transferred(from, to, value); - } - - function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { - _transferredFrom(spender, from, to, value); - } - function _transferred(address from, address to, uint256 value) internal view virtual { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -86,31 +129,4 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal RuleMaxTotalSupply_InvalidTransferFrom(address(this), spender, from, to, value, code) ); } - - function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { - return restrictionCode == CODE_MAX_TOTAL_SUPPLY_EXCEEDED; - } - - function messageForTransferRestriction(uint8 restrictionCode) - public - pure - override(IERC1404) - returns (string memory) - { - if (restrictionCode == CODE_MAX_TOTAL_SUPPLY_EXCEEDED) { - return TEXT_MAX_TOTAL_SUPPLY_EXCEEDED; - } - return TEXT_CODE_NOT_FOUND; - } - - /*////////////////////////////////////////////////////////////// - ACCESS CONTROL - //////////////////////////////////////////////////////////////*/ - - modifier onlyMaxTotalSupplyManager() { - _authorizeMaxTotalSupplyManager(); - _; - } - - function _authorizeMaxTotalSupplyManager() internal view virtual; } diff --git a/src/rules/validation/abstract/base/RuleSanctionsListBase.sol b/src/rules/validation/abstract/base/RuleSanctionsListBase.sol index 66ddb78..586b949 100644 --- a/src/rules/validation/abstract/base/RuleSanctionsListBase.sol +++ b/src/rules/validation/abstract/base/RuleSanctionsListBase.sol @@ -17,6 +17,10 @@ import {IRule} from "RuleEngine/interfaces/IRule.sol"; abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapter, RuleSanctionsListInvariantStorage { ISanctionsList public sanctionsList; + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) MetaTxModuleStandalone(forwarderIrrevocable) { @@ -25,6 +29,67 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte } } + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + 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; + } + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function setSanctionListOracle(ISanctionsList sanctionContractOracle_) public virtual onlySanctionListManager { + require(address(sanctionContractOracle_) != address(0), RuleSanctionsList_OracleAddressZeroNotAllowed()); + _setSanctionListOracle(sanctionContractOracle_); + } + + function clearSanctionListOracle() public virtual onlySanctionListManager { + _setSanctionListOracle(ISanctionsList(address(0))); + } + + function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { + _transferred(from, to, value); + } + + function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { + _transferredFrom(spender, from, to, value); + } + + function messageForTransferRestriction(uint8 restrictionCode) + public + pure + override(IERC1404) + returns (string memory) + { + if (restrictionCode == CODE_ADDRESS_FROM_IS_SANCTIONED) { + return TEXT_ADDRESS_FROM_IS_SANCTIONED; + } else if (restrictionCode == CODE_ADDRESS_TO_IS_SANCTIONED) { + return TEXT_ADDRESS_TO_IS_SANCTIONED; + } else if (restrictionCode == CODE_ADDRESS_SPENDER_IS_SANCTIONED) { + return TEXT_ADDRESS_SPENDER_IS_SANCTIONED; + } + return TEXT_CODE_NOT_FOUND; + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + modifier onlySanctionListManager() { + _authorizeSanctionListManager(); + _; + } + + function _authorizeSanctionListManager() internal view virtual; + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _detectTransferRestriction( address from, address to, @@ -61,44 +126,6 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte return uint8(REJECTED_CODE_BASE.TRANSFER_OK); } - 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; - } - - function messageForTransferRestriction(uint8 restrictionCode) - public - pure - override(IERC1404) - returns (string memory) - { - if (restrictionCode == CODE_ADDRESS_FROM_IS_SANCTIONED) { - return TEXT_ADDRESS_FROM_IS_SANCTIONED; - } else if (restrictionCode == CODE_ADDRESS_TO_IS_SANCTIONED) { - return TEXT_ADDRESS_TO_IS_SANCTIONED; - } else if (restrictionCode == CODE_ADDRESS_SPENDER_IS_SANCTIONED) { - return TEXT_ADDRESS_SPENDER_IS_SANCTIONED; - } - return TEXT_CODE_NOT_FOUND; - } - - function setSanctionListOracle(ISanctionsList sanctionContractOracle_) public virtual onlySanctionListManager { - require(address(sanctionContractOracle_) != address(0), RuleSanctionsList_OracleAddressZeroNotAllowed()); - _setSanctionListOracle(sanctionContractOracle_); - } - - function clearSanctionListOracle() public virtual onlySanctionListManager { - _setSanctionListOracle(ISanctionsList(address(0))); - } - - function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { - _transferred(from, to, value); - } - - function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { - _transferredFrom(spender, from, to, value); - } - function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -119,15 +146,4 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte sanctionsList = sanctionContractOracle_; emit SetSanctionListOracle(sanctionContractOracle_); } - - /*////////////////////////////////////////////////////////////// - ACCESS CONTROL - //////////////////////////////////////////////////////////////*/ - - modifier onlySanctionListManager() { - _authorizeSanctionListManager(); - _; - } - - function _authorizeSanctionListManager() internal view virtual; } diff --git a/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol b/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol index 578e946..cb37bdd 100644 --- a/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol +++ b/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol @@ -14,29 +14,33 @@ import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; * @dev Direct transfers (`transferred(from,to,value)`) are intentionally no-op. */ abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, RuleSpenderWhitelistInvariantStorage { - constructor(address forwarderIrrevocable) RuleAddressSet(forwarderIrrevocable) {} + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ - function _detectTransferRestriction(address, address, uint256) internal pure virtual override returns (uint8) { - return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - } + constructor(address forwarderIrrevocable) RuleAddressSet(forwarderIrrevocable) {} - function _detectTransferRestrictionFrom(address spender, address, address, uint256) - internal - view - virtual - override - returns (uint8) - { - if (spender != address(0) && !_isAddressListed(spender)) { - return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; - } - return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - } + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { return restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED; } + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Regular transfers are always accepted by this rule. + */ + function transferred(address, address, uint256) public view override(IERC3643IComplianceContract) {} + + function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { + _transferredFrom(spender, from, to, value); + } + function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -49,13 +53,25 @@ abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, Ru return TEXT_CODE_NOT_FOUND; } - /** - * @dev Regular transfers are always accepted by this rule. - */ - function transferred(address, address, uint256) public view override(IERC3643IComplianceContract) {} + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ - function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { - _transferredFrom(spender, from, to, value); + 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) + internal + view + virtual + override + returns (uint8) + { + if (spender != address(0) && !_isAddressListed(spender)) { + return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; + } + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } function _transferred(address, address, uint256) internal view virtual override { diff --git a/src/rules/validation/abstract/base/RuleWhitelistBase.sol b/src/rules/validation/abstract/base/RuleWhitelistBase.sol index 570f6a9..cad4bb4 100644 --- a/src/rules/validation/abstract/base/RuleWhitelistBase.sol +++ b/src/rules/validation/abstract/base/RuleWhitelistBase.sol @@ -11,10 +11,52 @@ import {IIdentityRegistryVerified} from "../../../interfaces/IIdentityRegistry.s * @notice Core whitelist logic without access-control policy. */ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIdentityRegistryVerified { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address forwarderIrrevocable, bool checkSpender_) RuleAddressSet(forwarderIrrevocable) { checkSpender = checkSpender_; } + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function setCheckSpender(bool value) public virtual onlyCheckSpenderManager { + _setCheckSpender(value); + emit CheckSpenderUpdated(value); + } + + function isVerified(address targetAddress) + public + view + virtual + override(IIdentityRegistryVerified) + returns (bool isListed) + { + isListed = _isAddressListed(targetAddress); + } + + function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { + return RuleTransferValidation.supportsInterface(interfaceId); + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + modifier onlyCheckSpenderManager() { + _authorizeCheckSpenderManager(); + _; + } + + function _authorizeCheckSpenderManager() internal view virtual; + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _detectTransferRestriction( address from, address to, @@ -47,37 +89,7 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde return _detectTransferRestriction(from, to, value); } - function isVerified(address targetAddress) - public - view - virtual - override(IIdentityRegistryVerified) - returns (bool isListed) - { - isListed = _isAddressListed(targetAddress); - } - - function setCheckSpender(bool value) public virtual onlyCheckSpenderManager { - _setCheckSpender(value); - emit CheckSpenderUpdated(value); - } - function _setCheckSpender(bool value) internal virtual { checkSpender = value; } - - function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { - return RuleTransferValidation.supportsInterface(interfaceId); - } - - /*////////////////////////////////////////////////////////////// - ACCESS CONTROL - //////////////////////////////////////////////////////////////*/ - - modifier onlyCheckSpenderManager() { - _authorizeCheckSpenderManager(); - _; - } - - function _authorizeCheckSpenderManager() internal view virtual; } diff --git a/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol b/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol index f522e3a..720295f 100644 --- a/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol +++ b/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol @@ -35,7 +35,59 @@ abstract contract RuleWhitelistWrapperBase is checkSpender = checkSpender_; } - /* ============ View Functions ============ */ + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + modifier onlyCheckSpenderManager() { + _authorizeCheckSpenderManager(); + _; + } + + function _authorizeCheckSpenderManager() internal virtual; + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Sets whether the rule should enforce spender-based checks. + * @dev + * - Restricted to holders of the manager role. + * - Updates the internal `checkSpender` flag. + * - Emits a {CheckSpenderUpdated} event. + * @param value The new state of the `checkSpender` flag. + */ + function setCheckSpender(bool value) public virtual onlyCheckSpenderManager { + _setCheckSpender(value); + emit CheckSpenderUpdated(value); + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AccessControl, RuleTransferValidation) + returns (bool) + { + return AccessControl.supportsInterface(interfaceId) || RuleTransferValidation.supportsInterface(interfaceId); + } + + /** + * @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. + */ + function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { + address[] memory targets = new address[](1); + targets[0] = targetAddress; + bool[] memory result = _detectTransferRestrictionForTargets(targets); + return result[0]; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /** * @notice Go through all the whitelist rules to know if a restriction exists on the transfer * @param from the origin address @@ -99,42 +151,6 @@ abstract contract RuleWhitelistWrapperBase is // ERC-7943 tokenId overloads are provided by {RuleNFTAdapter} via RuleWhitelistShared. - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(AccessControl, RuleTransferValidation) - returns (bool) - { - return AccessControl.supportsInterface(interfaceId) || RuleTransferValidation.supportsInterface(interfaceId); - } - - /** - * @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. - */ - function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { - address[] memory targets = new address[](1); - targets[0] = targetAddress; - bool[] memory result = _detectTransferRestrictionForTargets(targets); - return result[0]; - } - - /* ============ Access control ============ */ - - /** - * @notice Sets whether the rule should enforce spender-based checks. - * @dev - * - Restricted to holders of the manager role. - * - Updates the internal `checkSpender` flag. - * - Emits a {CheckSpenderUpdated} event. - * @param value The new state of the `checkSpender` flag. - */ - function setCheckSpender(bool value) public virtual onlyCheckSpenderManager { - _setCheckSpender(value); - emit CheckSpenderUpdated(value); - } - function _transferred(address from, address to, uint256 value) internal view @@ -153,10 +169,6 @@ abstract contract RuleWhitelistWrapperBase is RuleWhitelistShared._transferredFrom(spender, from, to, value); } - /*////////////////////////////////////////////////////////////// - INTERNAL/PRIVATE FUNCTIONS - //////////////////////////////////////////////////////////////*/ - /** * @notice Evaluates target addresses across all child rules. * @param targetAddress Addresses to validate (from/to[/spender]). @@ -203,17 +215,6 @@ abstract contract RuleWhitelistWrapperBase is checkSpender = value; } - /*////////////////////////////////////////////////////////////// - ACCESS CONTROL - //////////////////////////////////////////////////////////////*/ - - modifier onlyCheckSpenderManager() { - _authorizeCheckSpenderManager(); - _; - } - - function _authorizeCheckSpenderManager() internal virtual; - /*////////////////////////////////////////////////////////////// ERC-2771 //////////////////////////////////////////////////////////////*/ diff --git a/src/rules/validation/abstract/core/RuleNFTAdapter.sol b/src/rules/validation/abstract/core/RuleNFTAdapter.sol index c9f50a8..db03caa 100644 --- a/src/rules/validation/abstract/core/RuleNFTAdapter.sol +++ b/src/rules/validation/abstract/core/RuleNFTAdapter.sol @@ -23,15 +23,10 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC bytes4(keccak256("transferred(address,address,uint256,uint256)")); bytes4 internal constant TRANSFERRED_SELECTOR_ERC7943_FROM = bytes4(keccak256("transferred(address,address,address,uint256,uint256)")); - /** - * @notice Internal hook for post-transfer validation or state updates. - */ - function _transferred(address from, address to, uint256 value) internal virtual; - /** - * @notice Internal hook for post-transfer validation or state updates (spender-aware). - */ - function _transferredFrom(address spender, address from, address to, uint256 value) internal virtual; + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ /** * @inheritdoc IERC7943NonFungibleComplianceExtend @@ -161,4 +156,18 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC _transferred(ctx.from, ctx.to, ctx.value); } } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Internal hook for post-transfer validation or state updates. + */ + function _transferred(address from, address to, uint256 value) internal virtual; + + /** + * @notice Internal hook for post-transfer validation or state updates (spender-aware). + */ + 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 d120135..ed0fa18 100644 --- a/src/rules/validation/abstract/core/RuleTransferValidation.sol +++ b/src/rules/validation/abstract/core/RuleTransferValidation.sol @@ -4,11 +4,14 @@ pragma solidity ^0.8.20; /* ==== CMTAT === */ import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; -import {IERC3643ComplianceRead} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; +import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; /* ==== RuleEngine === */ import {IRule} from "RuleEngine/interfaces/IRule.sol"; import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +/* ==== CMTAT libraries === */ +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; /* ==== Modules === */ import {VersionModule} from "../../../../modules/VersionModule.sol"; @@ -19,32 +22,9 @@ abstract contract RuleTransferValidation is IERC7551Compliance, IRule { - /** - * @notice Internal transfer restriction check. - * @param from the origin address - * @param to the destination address - * @param value amount to transfer - * @return restrictionCode The restriction code for this rule. - */ - function _detectTransferRestriction(address from, address to, uint256 value) - internal - view - virtual - returns (uint8 restrictionCode); - - /** - * @notice Internal transfer restriction check for spender-initiated transfers. - * @param spender the caller executing the transfer - * @param from the origin address - * @param to the destination address - * @param value amount to transfer - * @return restrictionCode The restriction code for this rule. - */ - function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - internal - view - virtual - returns (uint8 restrictionCode); + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ /** * @inheritdoc IERC1404 @@ -104,6 +84,41 @@ abstract contract RuleTransferValidation is } function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { - return interfaceId == type(IRule).interfaceId || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID; + return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID + || interfaceId == type(IERC7551Compliance).interfaceId + || interfaceId == type(IERC3643IComplianceContract).interfaceId; } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Internal transfer restriction check. + * @param from the origin address + * @param to the destination address + * @param value amount to transfer + * @return restrictionCode The restriction code for this rule. + */ + function _detectTransferRestriction(address from, address to, uint256 value) + internal + view + virtual + returns (uint8 restrictionCode); + + /** + * @notice Internal transfer restriction check for spender-initiated transfers. + * @param spender the caller executing the transfer + * @param from the origin address + * @param to the destination address + * @param value amount to transfer + * @return restrictionCode The restriction code for this rule. + */ + function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + internal + view + virtual + returns (uint8 restrictionCode); } diff --git a/src/rules/validation/abstract/core/RuleWhitelistShared.sol b/src/rules/validation/abstract/core/RuleWhitelistShared.sol index 73d819a..b53fbad 100644 --- a/src/rules/validation/abstract/core/RuleWhitelistShared.sol +++ b/src/rules/validation/abstract/core/RuleWhitelistShared.sol @@ -22,7 +22,10 @@ abstract contract RuleWhitelistShared is RuleNFTAdapter, RuleWhitelistInvariantS */ bool public checkSpender; - /* ============ View Functions ============ */ + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /** * @notice Checks whether a restriction code is recognized by this rule. * @dev @@ -60,7 +63,9 @@ abstract contract RuleWhitelistShared is RuleNFTAdapter, RuleWhitelistInvariantS } } - /* ============ State Functions ============ */ + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ /** * @notice ERC-3643 hook called when a transfer occurs. @@ -92,6 +97,10 @@ abstract contract RuleWhitelistShared is RuleNFTAdapter, RuleWhitelistInvariantS _transferredFrom(spender, from, to, value); } + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( diff --git a/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol index 5b6833e..e6adfd1 100644 --- a/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol @@ -4,12 +4,6 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "./RuleSharedInvariantStorage.sol"; abstract contract RuleIdentityRegistryInvariantStorage is RuleSharedInvariantStorage { - error RuleIdentityRegistry_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); - error RuleIdentityRegistry_InvalidTransferFrom( - address rule, address spender, address from, address to, uint256 value, uint8 code - ); - error RuleIdentityRegistry_RegistryAddressZeroNotAllowed(); - string constant TEXT_ADDRESS_FROM_NOT_VERIFIED = "The sender is not verified"; string constant TEXT_ADDRESS_TO_NOT_VERIFIED = "The recipient is not verified"; string constant TEXT_ADDRESS_SPENDER_NOT_VERIFIED = "The spender is not verified"; @@ -20,4 +14,10 @@ abstract contract RuleIdentityRegistryInvariantStorage is RuleSharedInvariantSto uint8 public constant CODE_ADDRESS_SPENDER_NOT_VERIFIED = 57; event IdentityRegistryUpdated(address indexed newRegistry); + + error RuleIdentityRegistry_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); + error RuleIdentityRegistry_InvalidTransferFrom( + address rule, address spender, address from, address to, uint256 value, uint8 code + ); + error RuleIdentityRegistry_RegistryAddressZeroNotAllowed(); } diff --git a/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol index ad3d3bd..310a1c5 100644 --- a/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol @@ -4,12 +4,6 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "./RuleSharedInvariantStorage.sol"; abstract contract RuleMaxTotalSupplyInvariantStorage is RuleSharedInvariantStorage { - error RuleMaxTotalSupply_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); - error RuleMaxTotalSupply_InvalidTransferFrom( - address rule, address spender, address from, address to, uint256 value, uint8 code - ); - error RuleMaxTotalSupply_TokenAddressZeroNotAllowed(); - string constant TEXT_MAX_TOTAL_SUPPLY_EXCEEDED = "Max total supply exceeded"; // It is very important that each rule uses an unique code @@ -17,4 +11,10 @@ abstract contract RuleMaxTotalSupplyInvariantStorage is RuleSharedInvariantStora event MaxTotalSupplyUpdated(uint256 newMaxTotalSupply); event TokenContractUpdated(address indexed newTokenContract); + + error RuleMaxTotalSupply_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); + error RuleMaxTotalSupply_InvalidTransferFrom( + address rule, address spender, address from, address to, uint256 value, uint8 code + ); + error RuleMaxTotalSupply_TokenAddressZeroNotAllowed(); } diff --git a/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol index 27d3c95..10d8f06 100644 --- a/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol @@ -6,14 +6,6 @@ import {RuleSharedInvariantStorage} from "./RuleSharedInvariantStorage.sol"; import {ISanctionsList} from "../../../interfaces/ISanctionsList.sol"; abstract contract RuleSanctionsListInvariantStorage is RuleSharedInvariantStorage { - /* ============ Event ============ */ - event SetSanctionListOracle(ISanctionsList newOracle); - /* ============ Custom errors ============ */ - error RuleSanctionsList_OracleAddressZeroNotAllowed(); - error RuleSanctionsList_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); - error RuleSanctionsList_InvalidTransferFrom( - address rule, address spender, address from, address to, uint256 value, uint8 code - ); /* ============ Role ============ */ bytes32 public constant SANCTIONLIST_ROLE = keccak256("SANCTIONLIST_ROLE"); @@ -27,4 +19,14 @@ abstract contract RuleSanctionsListInvariantStorage is RuleSharedInvariantStorag uint8 public constant CODE_ADDRESS_FROM_IS_SANCTIONED = 30; uint8 public constant CODE_ADDRESS_TO_IS_SANCTIONED = 31; uint8 public constant CODE_ADDRESS_SPENDER_IS_SANCTIONED = 32; + + /* ============ Event ============ */ + event SetSanctionListOracle(ISanctionsList newOracle); + + /* ============ Custom errors ============ */ + error RuleSanctionsList_OracleAddressZeroNotAllowed(); + error RuleSanctionsList_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); + error RuleSanctionsList_InvalidTransferFrom( + address rule, address spender, address from, address to, uint256 value, uint8 code + ); } diff --git a/src/rules/validation/deployment/RuleBlacklist.sol b/src/rules/validation/deployment/RuleBlacklist.sol index 6c4d20d..670ccfb 100644 --- a/src/rules/validation/deployment/RuleBlacklist.sol +++ b/src/rules/validation/deployment/RuleBlacklist.sol @@ -25,6 +25,10 @@ contract RuleBlacklist is RuleBlacklistBase, AccessControlModuleStandalone { AccessControlModuleStandalone(admin) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function supportsInterface(bytes4 interfaceId) public view @@ -36,10 +40,18 @@ contract RuleBlacklist is RuleBlacklistBase, AccessControlModuleStandalone { || RuleBlacklistBase.supportsInterface(interfaceId); } + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } diff --git a/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol b/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol index ac2cea9..f1ad431 100644 --- a/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol @@ -12,12 +12,24 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; * @notice Ownable2Step variant of RuleBlacklist with owner-based authorization hooks. */ contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address forwarderIrrevocable) RuleBlacklistBase(forwarderIrrevocable) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } diff --git a/src/rules/validation/deployment/RuleERC2980.sol b/src/rules/validation/deployment/RuleERC2980.sol index 1ae4077..ffc0f47 100644 --- a/src/rules/validation/deployment/RuleERC2980.sol +++ b/src/rules/validation/deployment/RuleERC2980.sol @@ -44,7 +44,7 @@ contract RuleERC2980 is RuleERC2980Base, AccessControlModuleStandalone { {} /*////////////////////////////////////////////////////////////// - INTERFACE SUPPORT + PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ function supportsInterface(bytes4 interfaceId) @@ -69,6 +69,10 @@ contract RuleERC2980 is RuleERC2980Base, AccessControlModuleStandalone { function _authorizeFrozenlistRemove() internal view virtual override onlyRole(FROZENLIST_REMOVE_ROLE) {} + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _msgSender() internal view virtual override(Context, RuleERC2980Base) returns (address sender) { return super._msgSender(); } diff --git a/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol b/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol index 0764662..b1fc9a8 100644 --- a/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol +++ b/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol @@ -12,8 +12,16 @@ import {RuleERC2980Base} from "../abstract/base/RuleERC2980Base.sol"; * @dev All whitelist and frozenlist management functions are restricted to the contract owner. */ contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address forwarderIrrevocable) RuleERC2980Base(forwarderIrrevocable) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeWhitelistAdd() internal view virtual override onlyOwner {} function _authorizeWhitelistRemove() internal view virtual override onlyOwner {} @@ -22,6 +30,10 @@ contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step { function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {} + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _msgSender() internal view virtual override(Context, RuleERC2980Base) returns (address sender) { return super._msgSender(); } diff --git a/src/rules/validation/deployment/RuleIdentityRegistry.sol b/src/rules/validation/deployment/RuleIdentityRegistry.sol index 440a8e2..13aba22 100644 --- a/src/rules/validation/deployment/RuleIdentityRegistry.sol +++ b/src/rules/validation/deployment/RuleIdentityRegistry.sol @@ -12,11 +12,19 @@ import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.so * @dev Burns (to == address(0)) are allowed even if the sender is not verified. */ contract RuleIdentityRegistry is AccessControlModuleStandalone, RuleIdentityRegistryBase { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address admin, address identityRegistry_) AccessControlModuleStandalone(admin) RuleIdentityRegistryBase(identityRegistry_) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function supportsInterface(bytes4 interfaceId) public view diff --git a/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol b/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol index 09d99ba..29b7632 100644 --- a/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol @@ -10,7 +10,15 @@ import {RuleIdentityRegistryBase} from "../abstract/base/RuleIdentityRegistryBas * @notice Ownable2Step variant of RuleIdentityRegistry. */ contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address identityRegistry_) RuleIdentityRegistryBase(identityRegistry_) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {} } diff --git a/src/rules/validation/deployment/RuleMaxTotalSupply.sol b/src/rules/validation/deployment/RuleMaxTotalSupply.sol index 797e68f..ea78396 100644 --- a/src/rules/validation/deployment/RuleMaxTotalSupply.sol +++ b/src/rules/validation/deployment/RuleMaxTotalSupply.sol @@ -11,6 +11,10 @@ import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.so * @notice Restricts minting so that total supply never exceeds a maximum value. */ contract RuleMaxTotalSupply is AccessControlModuleStandalone, RuleMaxTotalSupplyBase { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + /** * @param admin Address that receives the default admin role. * @param tokenContract_ Token contract that exposes totalSupply (must be non-zero). @@ -21,6 +25,10 @@ contract RuleMaxTotalSupply is AccessControlModuleStandalone, RuleMaxTotalSupply RuleMaxTotalSupplyBase(tokenContract_, maxTotalSupply_) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function supportsInterface(bytes4 interfaceId) public view diff --git a/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol b/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol index 9fc5c4a..dbfa4d9 100644 --- a/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol @@ -10,10 +10,18 @@ import {RuleMaxTotalSupplyBase} from "../abstract/base/RuleMaxTotalSupplyBase.so * @notice Ownable2Step variant of RuleMaxTotalSupply. */ contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address tokenContract_, uint256 maxTotalSupply_) RuleMaxTotalSupplyBase(tokenContract_, maxTotalSupply_) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {} } diff --git a/src/rules/validation/deployment/RuleSanctionsList.sol b/src/rules/validation/deployment/RuleSanctionsList.sol index 165600f..59d4f2a 100644 --- a/src/rules/validation/deployment/RuleSanctionsList.sol +++ b/src/rules/validation/deployment/RuleSanctionsList.sol @@ -14,6 +14,10 @@ import {ISanctionsList} from "../../interfaces/ISanctionsList.sol"; * @notice Compliance rule enforcing sanctions-screening for token transfers. */ contract RuleSanctionsList is AccessControlModuleStandalone, RuleSanctionsListBase { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + /** * @param admin Address of the contract (Access Control) * @param forwarderIrrevocable Address of the forwarder, required for the gasless support @@ -23,6 +27,10 @@ contract RuleSanctionsList is AccessControlModuleStandalone, RuleSanctionsListBa RuleSanctionsListBase(forwarderIrrevocable, sanctionContractOracle_) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function supportsInterface(bytes4 interfaceId) public view @@ -41,7 +49,7 @@ contract RuleSanctionsList is AccessControlModuleStandalone, RuleSanctionsListBa function _authorizeSanctionListManager() internal view virtual override onlyRole(SANCTIONLIST_ROLE) {} /*////////////////////////////////////////////////////////////// - ERC-2771 + INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { diff --git a/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol b/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol index 2f8018d..572eb5d 100644 --- a/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol @@ -13,13 +13,25 @@ import {ISanctionsList} from "../../interfaces/ISanctionsList.sol"; * @notice Ownable2Step variant of RuleSanctionsList. */ contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) RuleSanctionsListBase(forwarderIrrevocable, sanctionContractOracle_) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeSanctionListManager() internal view virtual override onlyOwner {} + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { return ERC2771Context._msgSender(); } diff --git a/src/rules/validation/deployment/RuleSpenderWhitelist.sol b/src/rules/validation/deployment/RuleSpenderWhitelist.sol index ee1bb70..ad6ea8d 100644 --- a/src/rules/validation/deployment/RuleSpenderWhitelist.sol +++ b/src/rules/validation/deployment/RuleSpenderWhitelist.sol @@ -13,11 +13,19 @@ import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.so * @notice AccessControlEnumerable deployment variant of spender whitelist rule. */ contract RuleSpenderWhitelist is RuleSpenderWhitelistBase, AccessControlModuleStandalone { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address admin, address forwarderIrrevocable) RuleSpenderWhitelistBase(forwarderIrrevocable) AccessControlModuleStandalone(admin) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function supportsInterface(bytes4 interfaceId) public view @@ -29,10 +37,18 @@ contract RuleSpenderWhitelist is RuleSpenderWhitelistBase, AccessControlModuleSt || RuleTransferValidation.supportsInterface(interfaceId); } + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } diff --git a/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol b/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol index 18ab65e..fa89183 100644 --- a/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol @@ -12,15 +12,27 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; * @notice Ownable2Step deployment variant of spender whitelist rule. */ contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address forwarderIrrevocable) RuleSpenderWhitelistBase(forwarderIrrevocable) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } diff --git a/src/rules/validation/deployment/RuleWhitelist.sol b/src/rules/validation/deployment/RuleWhitelist.sol index dc4c75e..5d9aff7 100644 --- a/src/rules/validation/deployment/RuleWhitelist.sol +++ b/src/rules/validation/deployment/RuleWhitelist.sol @@ -33,7 +33,9 @@ contract RuleWhitelist is RuleWhitelistBase, AccessControlModuleStandalone { // no-op } - /* ============ View Functions ============ */ + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ /** * @notice Indicates whether this contract supports a given interface. @@ -61,6 +63,10 @@ contract RuleWhitelist is RuleWhitelistBase, AccessControlModuleStandalone { function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } diff --git a/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol b/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol index e5cc721..dcaeee7 100644 --- a/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol @@ -12,17 +12,29 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; * @notice Ownable2Step variant of RuleWhitelist with owner-based authorization hooks. */ contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(address owner, address forwarderIrrevocable, bool checkSpender_) RuleWhitelistBase(forwarderIrrevocable, checkSpender_) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} function _authorizeAddressListRemove() internal view virtual override onlyOwner {} function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } diff --git a/src/rules/validation/deployment/RuleWhitelistWrapper.sol b/src/rules/validation/deployment/RuleWhitelistWrapper.sol index 7c4b750..351465a 100644 --- a/src/rules/validation/deployment/RuleWhitelistWrapper.sol +++ b/src/rules/validation/deployment/RuleWhitelistWrapper.sol @@ -26,7 +26,9 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt AccessControlModuleStandalone(admin) {} - /* ============ Access control ============ */ + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ /** * @dev Returns `true` if `account` has been granted `role`. @@ -41,6 +43,21 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt return AccessControlModuleStandalone.hasRole(role, account); } + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AccessControlEnumerable, RuleWhitelistWrapperBase) + returns (bool) + { + return RuleWhitelistWrapperBase.supportsInterface(interfaceId) + || AccessControlEnumerable.supportsInterface(interfaceId); + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeCheckSpenderManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} /** @@ -49,7 +66,7 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} /*////////////////////////////////////////////////////////////// - ERC-2771 + INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ function _msgSender() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (address sender) { @@ -70,17 +87,6 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt return RuleWhitelistWrapperBase._contextSuffixLength(); } - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(AccessControlEnumerable, RuleWhitelistWrapperBase) - returns (bool) - { - return RuleWhitelistWrapperBase.supportsInterface(interfaceId) - || AccessControlEnumerable.supportsInterface(interfaceId); - } - function _grantRole(bytes32 role, address account) internal virtual diff --git a/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol b/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol index 9db5af3..18b2296 100644 --- a/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol @@ -25,6 +25,10 @@ contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2S Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} /** @@ -33,7 +37,7 @@ contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2S function _onlyRulesManager() internal view virtual override onlyOwner {} /*////////////////////////////////////////////////////////////// - ERC-2771 + INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ function _msgSender() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (address sender) { diff --git a/test/Coverage/DeploymentOperationCoverage.t.sol b/test/Coverage/DeploymentOperationCoverage.t.sol index b4e692d..d04a210 100644 --- a/test/Coverage/DeploymentOperationCoverage.t.sol +++ b/test/Coverage/DeploymentOperationCoverage.t.sol @@ -4,8 +4,11 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {HelperContract} from "../HelperContract.sol"; import {IERC165} from "OZ/utils/introspection/IERC165.sol"; -import {IRule} from "RuleEngine/interfaces/IRule.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"; import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; import {RuleConditionalTransferLight} from "src/rules/operation/RuleConditionalTransferLight.sol"; import { @@ -86,8 +89,11 @@ contract OperationCoverageExtraTest is Test, HelperContract { rule.created(ADDRESS1, 1); rule.destroyed(ADDRESS1, 1); - assertTrue(rule.supportsInterface(type(IRule).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)); + assertTrue(rule.supportsInterface(type(IERC3643ComplianceFull).interfaceId)); assertFalse(rule.supportsInterface(bytes4(0xdeadbeef))); } @@ -96,9 +102,12 @@ contract OperationCoverageExtraTest is Test, HelperContract { new RuleConditionalTransferLightOwnable2Step(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS); vm.prank(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS); - rule.approveTransfer(ADDRESS1, ADDRESS2, 77); + rule.bindToken(ADDRESS3); vm.prank(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS); + rule.approveTransfer(ADDRESS1, ADDRESS2, 77); + + vm.prank(ADDRESS3); rule.transferred(ADDRESS3, ADDRESS1, ADDRESS2, 77); assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 77), 0); @@ -114,8 +123,11 @@ contract OperationCoverageExtraTest is Test, HelperContract { assertEq(rule.messageForTransferRestriction(CODE_NONEXISTENT), TEXT_CODE_NOT_FOUND); assertTrue(rule.supportsInterface(type(IERC165).interfaceId)); - assertTrue(rule.supportsInterface(type(IRule).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)); + assertTrue(rule.supportsInterface(type(IERC3643ComplianceFull).interfaceId)); assertFalse(rule.supportsInterface(bytes4(0xdeadbeef))); } } diff --git a/test/RuleConditionalTransferLight/Ownable/RuleConditionalTransferLightOwnable2StepAccessControl.t.sol b/test/RuleConditionalTransferLight/Ownable/RuleConditionalTransferLightOwnable2StepAccessControl.t.sol index f44cd09..f0427e5 100644 --- a/test/RuleConditionalTransferLight/Ownable/RuleConditionalTransferLightOwnable2StepAccessControl.t.sol +++ b/test/RuleConditionalTransferLight/Ownable/RuleConditionalTransferLightOwnable2StepAccessControl.t.sol @@ -15,6 +15,8 @@ contract RuleConditionalTransferLightOwnable2StepAccessControl is Test, HelperCo function setUp() public { rule = new RuleConditionalTransferLightOwnable2Step(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS); + vm.prank(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS); + rule.bindToken(ADDRESS3); } function testOwnerCanApproveAndExecuteTransfer() public { @@ -22,7 +24,7 @@ contract RuleConditionalTransferLightOwnable2StepAccessControl is Test, HelperCo rule.approveTransfer(ADDRESS1, ADDRESS2, 10); assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 10), 1); - vm.prank(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS); + vm.prank(ADDRESS3); rule.transferred(ADDRESS1, ADDRESS2, 10); assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 10), 0); } @@ -35,7 +37,9 @@ contract RuleConditionalTransferLightOwnable2StepAccessControl is Test, HelperCo vm.prank(CONDITIONAL_TRANSFER_OPERATOR_ADDRESS); rule.approveTransfer(ADDRESS1, ADDRESS2, 10); - vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, ATTACKER)); + vm.expectRevert( + abi.encodeWithSelector(RuleConditionalTransferLight_TransferExecutorUnauthorized.selector, ATTACKER) + ); vm.prank(ATTACKER); rule.transferred(ADDRESS1, ADDRESS2, 10); } diff --git a/test/RuleConditionalTransferLight/RuleConditionalTransferLightApproveAndTransfer.t.sol b/test/RuleConditionalTransferLight/RuleConditionalTransferLightApproveAndTransfer.t.sol index 2449845..d79434c 100644 --- a/test/RuleConditionalTransferLight/RuleConditionalTransferLightApproveAndTransfer.t.sol +++ b/test/RuleConditionalTransferLight/RuleConditionalTransferLightApproveAndTransfer.t.sol @@ -28,17 +28,18 @@ contract RuleConditionalTransferLightApproveAndTransfer is Test, HelperContract token.approve(address(rule), 10); vm.prank(DEFAULT_ADMIN_ADDRESS); - rule.approveAndTransferIfAllowed(address(token), ADDRESS1, ADDRESS2, 10); + rule.approveAndTransferIfAllowed(ADDRESS1, ADDRESS2, 10); assertEq(token.balanceOf(ADDRESS1), 90); assertEq(token.balanceOf(ADDRESS2), 10); assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 10), 0); } - function testApproveAndTransferIfAllowedRevertsOnZeroToken() public { - vm.expectRevert(RuleConditionalTransferLight_TokenAddressZeroNotAllowed.selector); + function testApproveAndTransferIfAllowedRevertsWhenNoTokenBound() public { + RuleConditionalTransferLight freshRule = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert(RuleConditionalTransferLight_TokenNotBound.selector); vm.prank(DEFAULT_ADMIN_ADDRESS); - rule.approveAndTransferIfAllowed(ZERO_ADDRESS, ADDRESS1, ADDRESS2, 10); + freshRule.approveAndTransferIfAllowed(ADDRESS1, ADDRESS2, 10); } function testApproveAndTransferIfAllowedRevertsOnInsufficientAllowance() public { @@ -48,15 +49,20 @@ contract RuleConditionalTransferLightApproveAndTransfer is Test, HelperContract ) ); vm.prank(DEFAULT_ADMIN_ADDRESS); - rule.approveAndTransferIfAllowed(address(token), ADDRESS1, ADDRESS2, 10); + rule.approveAndTransferIfAllowed(ADDRESS1, ADDRESS2, 10); } function testApproveAndTransferIfAllowedRevertsOnTransferFailure() public { MockERC20TransferFromFalse failingToken = new MockERC20TransferFromFalse(); failingToken.setAllowance(ADDRESS1, address(rule), 10); + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + rule.unbindToken(address(token)); + rule.bindToken(address(failingToken)); + vm.stopPrank(); + vm.expectRevert(RuleConditionalTransferLight_TransferFailed.selector); vm.prank(DEFAULT_ADMIN_ADDRESS); - rule.approveAndTransferIfAllowed(address(failingToken), ADDRESS1, ADDRESS2, 10); + rule.approveAndTransferIfAllowed(ADDRESS1, ADDRESS2, 10); } } diff --git a/test/RuleConditionalTransferLight/RuleConditionalTransferLightUnit.t.sol b/test/RuleConditionalTransferLight/RuleConditionalTransferLightUnit.t.sol index d19f79b..882cacd 100644 --- a/test/RuleConditionalTransferLight/RuleConditionalTransferLightUnit.t.sol +++ b/test/RuleConditionalTransferLight/RuleConditionalTransferLightUnit.t.sol @@ -19,6 +19,19 @@ contract RuleConditionalTransferLightUnit is Test, HelperContract { new RuleConditionalTransferLight(address(0)); } + function testBindToken_RevertsIfAlreadyBound() public { + vm.expectRevert(RuleConditionalTransferLight_TokenAlreadyBound.selector); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + } + + function testBindToken_RevertsForUnauthorizedCaller() public { + RuleConditionalTransferLight freshRule = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert(); + vm.prank(ADDRESS1); + freshRule.bindToken(ADDRESS3); + } + function testApproveTransfer_OnlyOperator() public { vm.expectRevert(); vm.prank(ADDRESS1); diff --git a/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol b/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol index 5bd1f2c..46be983 100644 --- a/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol +++ b/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol @@ -6,7 +6,9 @@ import {HelperContract} from "../HelperContract.sol"; import {RuleSpenderWhitelist} from "src/rules/validation/deployment/RuleSpenderWhitelist.sol"; import {AccessControlModuleStandalone} from "src/modules/AccessControlModuleStandalone.sol"; import {IAccessControl} from "OZ/access/IAccessControl.sol"; -import {IRule} from "RuleEngine/interfaces/IRule.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; import {RuleSpenderWhitelistHarness} from "src/mocks/harness/RuleSpenderWhitelistHarnesses.sol"; contract RuleSpenderWhitelistTest is Test, HelperContract { @@ -88,7 +90,10 @@ contract RuleSpenderWhitelistTest is Test, HelperContract { function testSupportsInterface() public view { assertTrue(rule.supportsInterface(type(IAccessControl).interfaceId)); - assertTrue(rule.supportsInterface(type(IRule).interfaceId)); + assertTrue(rule.supportsInterface(RuleInterfaceId.IRULE_INTERFACE_ID)); + assertTrue(rule.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID)); + assertTrue(rule.supportsInterface(RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID)); + assertFalse(rule.supportsInterface(bytes4(0xdeadbeef))); } function testMetaTxOverridesAreReachable() public view {