diff --git a/AGENTS.md b/AGENTS.md
index b151852..315ea72 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -36,7 +36,7 @@ Modular compliance-rule library for CMTAT / ERC-3643 security tokens. Each rule
- `openzeppelin-contracts` v5.6.1 — `AccessControl`, `Ownable2Step`, `EnumerableSet`, `ERC2771Context`
- `openzeppelin-contracts-upgradeable` v5.6.1
- `CMTAT` v3.0.0 — `IERC1404`, `IERC3643`, `IRuleEngine` interfaces
-- `RuleEngine` v3.0.0-rc2 — `IRule`, `RulesManagementModule`
+- `RuleEngine` v3.0.0-rc3 — `IRule`, `RulesManagementModule`
- `forge-std` — Foundry test utilities
Remappings are in `remappings.txt`; aliases used in source: `OZ/`, `CMTAT/`, `RuleEngine/`.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68490c9..5f3af1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,7 +43,38 @@ Custom changelog tag: `Dependencies`, `Documentation`, `Testing`
- Update surya doc by running the 3 scripts in [./doc/script](./doc/script)
- Update changelog
-## v0.3.0 -
+
+
+## v0.4.0
+
+### Added
+
+- `RuleConditionalTransferLightMultiToken` and `RuleConditionalTransferLightMultiTokenOwnable2Step` — multi-token conditional transfer rules with token-scoped approvals keyed by `(token, from, to, value)`.
+
+### Changed
+
+- Update contract version in `VersionModule` to `0.4.0`.
+- Ownable2Step rule deployments now explicitly advertise ERC-165 `IERC165` (`0x01ffc9a7`), ERC-173 (`0x7f5828d0`), and Ownable2Step (`0x9ab669ef`) interface IDs.
+
+### Dependencies
+
+- Update RuleEngine to `v3.0.0-rc3`.
+
+### Documentation
+
+- Added technical documentation: `doc/technical/RuleConditionalTransferLightMultiToken.md`.
+- Updated README operation-rule sections and tables to include `RuleConditionalTransferLightMultiToken`.
+
+### Testing
+
+- Added `RuleConditionalTransferLightMultiToken` tests proving approvals are token-scoped and cannot be consumed cross-token.
+- Added explicit RuleEngine integration tests for `RuleConditionalTransferLightMultiToken` documenting caller-context behavior in shared RuleEngine topology.
+- Added `Ownable2StepERC165Support` test covering all Ownable2Step rule deployments.
+- Extended `Ownable2StepERC165Support` with negative assertions to ensure Ownable2Step rule deployments do not advertise unrelated interfaces (`IAccessControl`, `0xdeadbeef`).
+
+## v0.3.0 - 2026-04-16
+
+Commit: `91c21c1191e84ff938892267ec443b0d1bb9efb0`
### Security
diff --git a/CLAUDE.md b/CLAUDE.md
index b151852..315ea72 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -36,7 +36,7 @@ Modular compliance-rule library for CMTAT / ERC-3643 security tokens. Each rule
- `openzeppelin-contracts` v5.6.1 — `AccessControl`, `Ownable2Step`, `EnumerableSet`, `ERC2771Context`
- `openzeppelin-contracts-upgradeable` v5.6.1
- `CMTAT` v3.0.0 — `IERC1404`, `IERC3643`, `IRuleEngine` interfaces
-- `RuleEngine` v3.0.0-rc2 — `IRule`, `RulesManagementModule`
+- `RuleEngine` v3.0.0-rc3 — `IRule`, `RulesManagementModule`
- `forge-std` — Foundry test utilities
Remappings are in `remappings.txt`; aliases used in source: `OZ/`, `CMTAT/`, `RuleEngine/`.
diff --git a/README.md b/README.md
index 097c66c..88b09ac 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,7 @@ forge test
| Component | Compatible Versions |
| ---------------- | ----------------------------------------- |
-| **Rules v0.1.0** | CMTAT ≥ v3.0.0
RuleEngine v3.0.0-rc2 |
+| **Rules v0.1.0** | CMTAT ≥ v3.0.0
RuleEngine v3.0.0-rc3 |
Each Rule implements the interface `IRuleEngine` defined in CMTAT.
@@ -306,7 +306,7 @@ Available validation rules: `RuleWhitelist`, `RuleWhitelistWrapper`, `RuleSpende
Operation rules modify blockchain state during transfer execution. Their `transferred()` function is state-mutating: it consumes or updates stored data as part of the transfer flow.
-Available operation rules: `RuleConditionalTransferLight`.
+Available operation rules: `RuleConditionalTransferLight`, `RuleConditionalTransferLightMultiToken`.
A full-featured variant, `RuleConditionalTransfer`, is maintained as a separate experimental repository at [CMTA/RuleConditionalTransfer](https://github.com/CMTA/RuleConditionalTransfer).
@@ -362,6 +362,7 @@ Several rules are available in multiple access-control variants. Use the simples
| RuleSpenderWhitelist | Read-Only | ✔ | ✔ | ✔ | This rule blocks `transferFrom` when the spender is not in the whitelist. Direct transfers are always allowed. |
| RuleERC2980 | Read-Only | ✔ | ✔ | ✔ | ERC-2980 Swiss Compliant rule combining a whitelist (recipient-only) and a frozenlist (blocks sender, recipient, and spender for `transferFrom`). Frozenlist takes priority over whitelist. |
| RuleConditionalTransferLight | Read-Write | ✘ | ✔ | ✔ | This rule requires that transfers have to be approved by an operator before being executed. Each approval is consumed once and the same transfer can be approved multiple times. |
+| RuleConditionalTransferLightMultiToken | Read-Write | ✘ | ✔ | ✔ | Multi-token variant of ConditionalTransferLight. Approvals are token-scoped with key `(token, from, to, value)` so one token cannot consume another token's approvals. |
| [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. |
@@ -382,6 +383,7 @@ Detailed technical documentation for each rule is available in [`doc/technical/`
| RuleSpenderWhitelist | [RuleSpenderWhitelist.md](./doc/technical/RuleSpenderWhitelist.md) |
| RuleERC2980 | [RuleERC2980.md](./doc/technical/RuleERC2980.md) |
| RuleConditionalTransferLight | [RuleConditionalTransferLight.md](./doc/technical/RuleConditionalTransferLight.md) |
+| RuleConditionalTransferLightMultiToken | [RuleConditionalTransferLightMultiToken.md](./doc/technical/RuleConditionalTransferLightMultiToken.md) |
### Operational Notes
@@ -422,13 +424,22 @@ Detailed technical documentation for each rule is available in [`doc/technical/`
- `RuleConditionalTransferLight`: `transferred()` is restricted to the single token bound via `bindToken`; second bind reverts with `RuleConditionalTransferLight_TokenAlreadyBound` until `unbindToken`.
- `RuleConditionalTransferLight`: mints (`from == address(0)`) and burns (`to == address(0)`) are exempt from approval checks; `created` and `destroyed` delegate to `_transferred`.
+#### RuleConditionalTransferLightMultiToken
+
+- `RuleConditionalTransferLightMultiToken`: approvals are keyed by `(token, from, to, value)` and are not nonce-based.
+- `RuleConditionalTransferLightMultiToken`: operator functions are token-scoped (`approveTransfer(token, ...)`, `cancelTransferApproval(token, ...)`, `approvedCount(token, ...)`, `approveAndTransferIfAllowed(token, ...)`).
+- `RuleConditionalTransferLightMultiToken`: execution is restricted to bound tokens; only the calling bound token can consume approvals for its own key space.
+- `RuleConditionalTransferLightMultiToken`: mints (`from == address(0)`) and burns (`to == address(0)`) are exempt from approval checks; `created` and `destroyed` delegate to `_transferred`.
+- `RuleConditionalTransferLightMultiToken`: with a shared `RuleEngine`, the caller seen by the rule is the engine address (not the underlying token). In that topology, token-scoped approvals are not visible unless approvals are keyed to the engine address, which is not per-token scoping.
+- **Warning**: `RuleConditionalTransferLightMultiToken` supports several tokens when integrated directly with each token contract. It must not be used for per-token approval isolation through a shared `RuleEngine`.
+
#### General notes
- All validation rules: read-only rules still implement `transferred()` for ERC-3643 and RuleEngine compatibility, but do not change state.
- All AccessControl variants: use `onlyRole(ROLE)` in `_authorize*()` and mark internal helpers `virtual`.
- All AccessControl variants: use `AccessControlEnumerable`, so role members can be enumerated with `getRoleMember` / `getRoleMemberCount`; default admin is treated as having all roles via `hasRole`, but may not appear in role member lists unless explicitly granted.
- All meta-tx-enabled rules: `forwarderIrrevocable` is accepted as-is (including `address(0)`) and is not validated against ERC-165 because some forwarders do not implement it.
-- All rules: implement `IERC3643Version` via `VersionModule` and expose `version()` returning `"0.3.0"`.
+- All rules: implement `IERC3643Version` via `VersionModule` and expose `version()` returning `"0.4.0"`.
### Read-only (validation) rule
@@ -579,7 +590,7 @@ The operator calls `setIdentityRegistry(registry)`. The issuer attempts a transf
### Read-Write (Operation) rule
-For the moment, there is only one operation rule available: ConditionalTransferLight.
+There are two operation rules available: `RuleConditionalTransferLight` and `RuleConditionalTransferLightMultiToken`.
#### Conditional transfer (light)
@@ -591,6 +602,14 @@ This rule requires that transfers must be approved by an operator before being e
An operator calls `approveTransfer(from, to, value)`. The compliance manager binds exactly one token with `bindToken(token)`; attempting to bind a second token reverts. The token calls `detectTransferRestriction` (passes) and later `transferred` to consume the approval. Without approval, `detectTransferRestriction` returns code 46 and the transfer is rejected. The operator can revoke with `cancelTransferApproval`. To migrate to a different token, the compliance manager must first call `unbindToken` before binding the new one.
+#### Conditional transfer (light, multi-token)
+
+This variant scopes approvals by token address. It hashes `(token, from, to, value)` and supports multiple bound tokens in a single rule instance. Each successful transfer consumes one approval in the calling token namespace. Mints (`from == address(0)`) and burns (`to == address(0)`) remain exempt.
+
+**Usage scenario**
+
+An operator calls `approveTransfer(tokenA, from, to, value)` for `tokenA`. A transfer on `tokenA` succeeds and consumes the approval. The same `(from, to, value)` transfer on `tokenB` is still rejected until separately approved with `approveTransfer(tokenB, from, to, value)`.
+
## Access Control
The module `AccessControlModuleStandalone` implements RBAC access control by inheriting from OpenZeppelin's `AccessControlEnumerable`.
@@ -618,8 +637,8 @@ See also [docs.openzeppelin.com - AccessControl](https://docs.openzeppelin.com/c
| `ADDRESS_LIST_REMOVE_ROLE` | `0x1b94c92b564251ed6b49246d9a82eb7a486b6490f3b3a3bf3b28d2e99801f3ec` | `removeAddress`, `removeAddresses` (RuleWhitelist, RuleBlacklist) |
| `SANCTIONLIST_ROLE` | `0x30842281ac34bdc7d568c7ab276f84ba6fc1a1de1ae858b0afd35e716fb0650d` | `setSanctionListOracle`, `clearSanctionListOracle` (RuleSanctionsList) |
| `RULES_MANAGEMENT_ROLE` | `0xea5f4eb72290e50c32abd6c23e45de3d8300b3286e1cbc2e293114b92e034e5e` | `setRules`, `clearRules`, `addRule`, `removeRule` (RuleWhitelistWrapper) |
-| `OPERATOR_ROLE` | `0x97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b929` | `approveTransfer`, `cancelTransferApproval` (RuleConditionalTransferLight) |
-| `COMPLIANCE_MANAGER_ROLE` | `0xe5c50d0927e06141e032cb9a67e1d7092dc85c0b0825191f7e1cede600028568` | `bindToken`, `unbindToken` (RuleConditionalTransferLight) |
+| `OPERATOR_ROLE` | `0x97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b929` | `approveTransfer`, `cancelTransferApproval` (RuleConditionalTransferLight / RuleConditionalTransferLightMultiToken) |
+| `COMPLIANCE_MANAGER_ROLE` | `0xe5c50d0927e06141e032cb9a67e1d7092dc85c0b0825191f7e1cede600028568` | `bindToken`, `unbindToken` (RuleConditionalTransferLight / RuleConditionalTransferLightMultiToken) |
| `WHITELIST_ADD_ROLE` | `0x77c0b4c0975a0b0417d8ce295502737b95fee8923755fed0cce952907a1861ed` | `addWhitelistAddress`, `addWhitelistAddresses` (RuleERC2980) |
| `WHITELIST_REMOVE_ROLE` | `0xf4d11a530c5b90f459c6ab1e335d3d77156b8ff3093308e4fca6d100ee87ade9` | `removeWhitelistAddress`, `removeWhitelistAddresses` (RuleERC2980) |
| `FROZENLIST_ADD_ROLE` | `0xc52c49807a071974b9260f4b553ee09bd9fd85f687d8d4cc3232de7104ff7835` | `addFrozenlistAddress`, `addFrozenlistAddresses` (RuleERC2980) |
@@ -637,9 +656,11 @@ For simpler ownership-based control, `Ownable2Step` variants (two-step ownership
- `RuleMaxTotalSupplyOwnable2Step`
- `RuleERC2980Ownable2Step`
- `RuleConditionalTransferLightOwnable2Step`
+- `RuleConditionalTransferLightMultiTokenOwnable2Step`
`RuleConditionalTransferLightOwnable2Step` now grants approval and execution permissions exclusively to the owner.
All `Ownable2Step` variants enforce access using OpenZeppelin's `onlyOwner` modifier.
+All `Ownable2Step` variants also advertise ERC-165 support for `IERC165` (`0x01ffc9a7`), ERC-173 ownership (`0x7f5828d0`), and Ownable2Step handover (`0x9ab669ef`).
### Address List
@@ -678,7 +699,7 @@ Here are the settings for [Hardhat](https://hardhat.org) and [Foundry](https://g
- CMTAT [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0)
- - RuleEngine [v3.0.0-rc2](https://github.com/CMTA/RuleEngine/releases/tag/v3.0.0-rc2)
+ - RuleEngine [v3.0.0-rc3](https://github.com/CMTA/RuleEngine/releases/tag/v3.0.0-rc3)
### Toolchain installation
diff --git a/doc/specification/RulesSpecificationv0.3.0.pdf b/doc/specification/RulesSpecificationv0.3.0.pdf
new file mode 100644
index 0000000..53754ab
Binary files /dev/null and b/doc/specification/RulesSpecificationv0.3.0.pdf differ
diff --git a/doc/specification/RulesSpecificationv0.2.0.pdf b/doc/specification/archive/RulesSpecificationv0.2.0.pdf
similarity index 100%
rename from doc/specification/RulesSpecificationv0.2.0.pdf
rename to doc/specification/archive/RulesSpecificationv0.2.0.pdf
diff --git a/doc/specification/cover_page.odg b/doc/specification/cover_page.odg
index 855eff6..4645f13 100644
Binary files a/doc/specification/cover_page.odg and b/doc/specification/cover_page.odg differ
diff --git a/doc/specification/cover_page.pdf b/doc/specification/cover_page.pdf
index 0b3519c..8dd7fe2 100644
Binary files a/doc/specification/cover_page.pdf and b/doc/specification/cover_page.pdf differ
diff --git a/doc/technical/RuleConditionalTransferLightMultiToken.md b/doc/technical/RuleConditionalTransferLightMultiToken.md
new file mode 100644
index 0000000..c7cbcd2
--- /dev/null
+++ b/doc/technical/RuleConditionalTransferLightMultiToken.md
@@ -0,0 +1,53 @@
+# Rule Conditional Transfer Light MultiToken
+
+[TOC]
+
+`RuleConditionalTransferLightMultiToken` is an operation rule that requires explicit operator approval before each transfer, with approvals scoped per token.
+
+Approval key:
+
+- `keccak256(token, from, to, value)`
+
+This prevents approval reuse across tokens when the rule receives token-specific caller context (for example, direct token callbacks to the rule).
+
+## Restriction codes
+
+| Constant | Code | Meaning |
+| --- | --- | --- |
+| `CODE_TRANSFER_REQUEST_NOT_APPROVED` | 46 | No approval exists for this `(token, from, to, value)` tuple |
+
+## Access control
+
+| Role | Description |
+| --- | --- |
+| `DEFAULT_ADMIN_ROLE` | Manages all roles (AccessControl variant) |
+| `OPERATOR_ROLE` | Approve/cancel approvals and call `approveAndTransferIfAllowed` |
+| `COMPLIANCE_MANAGER_ROLE` | Bind/unbind token contracts |
+
+## Methods
+
+### `approveTransfer(address token, address from, address to, uint256 value)`
+
+Approves one transfer for a specific token key.
+
+### `cancelTransferApproval(address token, address from, address to, uint256 value)`
+
+Removes one approval for a specific token key. Reverts if none exists.
+
+### `approvedCount(address token, address from, address to, uint256 value) -> uint256`
+
+Returns the remaining count for a specific token key.
+
+### `approveAndTransferIfAllowed(address token, address from, address to, uint256 value) -> bool`
+
+Approves and executes `safeTransferFrom` on the specified token, requiring allowance for this rule as spender.
+
+### `transferred(...)`
+
+Only bound tokens can call transfer execution hooks. Approval consumption uses the caller token (`msg.sender`) as the token key.
+
+## Notes
+
+- Mints and burns are exempt from approval consumption (`from == address(0)` or `to == address(0)`).
+- This rule is ERC-20 operation-focused, like `RuleConditionalTransferLight`.
+- In a shared `RuleEngine` topology, rule calls are made by the `RuleEngine` address, so `msg.sender` is the engine (not the token). In that case, token-scoped approval keys are not observable through current ERC-3643 / RuleEngine function signatures.
diff --git a/lib/RuleEngine b/lib/RuleEngine
index ec4a24a..5827604 160000
--- a/lib/RuleEngine
+++ b/lib/RuleEngine
@@ -1 +1 @@
-Subproject commit ec4a24a96ca30e2ef8f79a06e49846a431e9b4b1
+Subproject commit 5827604be4e38f65c055a929c7b62462a20f4bbd
diff --git a/src/modules/Ownable2StepERC165Module.sol b/src/modules/Ownable2StepERC165Module.sol
new file mode 100644
index 0000000..96b808d
--- /dev/null
+++ b/src/modules/Ownable2StepERC165Module.sol
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
+import {OwnableInterfaceId} from "RuleEngine/modules/library/OwnableInterfaceId.sol";
+import {Ownable2StepInterfaceId} from "RuleEngine/modules/library/Ownable2StepInterfaceId.sol";
+
+/**
+ * @title Ownable2StepERC165Module
+ * @notice Shared ERC-165 advertisement for Ownable2Step deployments.
+ */
+abstract contract Ownable2StepERC165Module is ERC165 {
+ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
+ return interfaceId == OwnableInterfaceId.IERC173_INTERFACE_ID
+ || interfaceId == Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID
+ || ERC165.supportsInterface(interfaceId);
+ }
+}
diff --git a/src/modules/VersionModule.sol b/src/modules/VersionModule.sol
index 8da4a78..c4922ae 100644
--- a/src/modules/VersionModule.sol
+++ b/src/modules/VersionModule.sol
@@ -8,7 +8,7 @@ import {IERC3643Version} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol
* @notice Exposes the contract version as required by ERC-3643.
*/
abstract contract VersionModule is IERC3643Version {
- string private constant VERSION = "0.3.0";
+ string private constant VERSION = "0.4.0";
/*//////////////////////////////////////////////////////////////
PUBLIC FUNCTIONS
diff --git a/src/rules/operation/RuleConditionalTransferLight.sol b/src/rules/operation/RuleConditionalTransferLight.sol
index 787427a..df88d51 100644
--- a/src/rules/operation/RuleConditionalTransferLight.sol
+++ b/src/rules/operation/RuleConditionalTransferLight.sol
@@ -52,4 +52,7 @@ contract RuleConditionalTransferLight is AccessControlModuleStandalone, RuleCond
function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {}
function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+
+ function _authorizeComplianceBindingChange(address) internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE)
+ {}
}
diff --git a/src/rules/operation/RuleConditionalTransferLightMultiToken.sol b/src/rules/operation/RuleConditionalTransferLightMultiToken.sol
new file mode 100644
index 0000000..ce30493
--- /dev/null
+++ b/src/rules/operation/RuleConditionalTransferLightMultiToken.sol
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol";
+import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
+import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
+import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
+import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
+import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol";
+import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol";
+import {RuleConditionalTransferLightMultiTokenBase} from "./abstract/RuleConditionalTransferLightMultiTokenBase.sol";
+
+contract RuleConditionalTransferLightMultiToken is
+ AccessControlModuleStandalone,
+ RuleConditionalTransferLightMultiTokenBase
+{
+ constructor(address admin) AccessControlModuleStandalone(admin) {}
+
+ 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);
+ }
+
+ function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {}
+
+ function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+}
diff --git a/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol b/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol
new file mode 100644
index 0000000..fd12100
--- /dev/null
+++ b/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
+import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
+import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
+import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
+import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
+import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
+import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol";
+import {RuleConditionalTransferLightMultiTokenBase} from "./abstract/RuleConditionalTransferLightMultiTokenBase.sol";
+import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol";
+
+contract RuleConditionalTransferLightMultiTokenOwnable2Step is
+ RuleConditionalTransferLightMultiTokenBase,
+ Ownable2Step,
+ Ownable2StepERC165Module
+{
+ constructor(address owner) Ownable(owner) {}
+
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ override(Ownable2StepERC165Module, IERC165)
+ returns (bool)
+ {
+ return Ownable2StepERC165Module.supportsInterface(interfaceId)
+ || interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID
+ || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID
+ || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID
+ || interfaceId == type(IERC7551Compliance).interfaceId
+ || interfaceId == type(IERC3643ComplianceFull).interfaceId;
+ }
+
+ function _authorizeTransferApproval() internal view virtual override onlyOwner {}
+
+ function _onlyComplianceManager() internal virtual override onlyOwner {}
+}
diff --git a/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol b/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol
index 0ba8e65..d19b61a 100644
--- a/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol
+++ b/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol
@@ -3,19 +3,20 @@ pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
-import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol";
import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransferLightBase.sol";
+import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol";
/**
* @title RuleConditionalTransferLightOwnable2Step
* @notice Ownable2Step variant of RuleConditionalTransferLight.
*/
-contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLightBase, Ownable2Step {
+contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLightBase, Ownable2Step, Ownable2StepERC165Module {
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -26,8 +27,13 @@ contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLigh
PUBLIC FUNCTIONS
//////////////////////////////////////////////////////////////*/
- function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
- return interfaceId == type(IERC165).interfaceId
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ override(Ownable2StepERC165Module, IERC165)
+ returns (bool)
+ {
+ return Ownable2StepERC165Module.supportsInterface(interfaceId)
|| interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID
|| interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID
|| interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID
@@ -42,4 +48,6 @@ contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLigh
function _authorizeTransferApproval() internal view virtual override onlyOwner {}
function _onlyComplianceManager() internal virtual override onlyOwner {}
+
+ function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {}
}
diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol
index 7b2bfca..69e19db 100644
--- a/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol
+++ b/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol
@@ -173,4 +173,5 @@ abstract contract RuleConditionalTransferLightBase is
function _authorizeTransferExecution() internal view override {
require(isTokenBound(_msgSender()), RuleConditionalTransferLight_TransferExecutorUnauthorized(_msgSender()));
}
+
}
diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol
new file mode 100644
index 0000000..0e656ab
--- /dev/null
+++ b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol
@@ -0,0 +1,235 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
+import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol";
+import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol";
+import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
+import {IRule} from "RuleEngine/interfaces/IRule.sol";
+import {ERC3643ComplianceModule} from "RuleEngine/modules/ERC3643ComplianceModule.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import {VersionModule} from "../../../modules/VersionModule.sol";
+import {RuleConditionalTransferLightMultiTokenInvariantStorage} from "./RuleConditionalTransferLightMultiTokenInvariantStorage.sol";
+import {ITransferContext} from "../../interfaces/ITransferContext.sol";
+
+abstract contract RuleConditionalTransferLightMultiTokenBase is
+ VersionModule,
+ ERC3643ComplianceModule,
+ RuleConditionalTransferLightMultiTokenInvariantStorage,
+ IRule
+{
+ using SafeERC20 for IERC20;
+
+ mapping(bytes32 => uint256) public approvalCounts;
+
+ modifier onlyTransferApprover() {
+ _authorizeTransferApproval();
+ _;
+ }
+
+ modifier onlyTransferExecutor() {
+ _authorizeTransferExecution();
+ _;
+ }
+
+ function _authorizeTransferApproval() internal view virtual;
+
+ 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 created(address to, uint256 value) external onlyBoundToken {
+ _transferred(_msgSender(), address(0), to, value);
+ }
+
+ function destroyed(address from, uint256 value) external onlyBoundToken {
+ _transferred(_msgSender(), from, address(0), value);
+ }
+
+ function approveTransfer(address token, address from, address to, uint256 value) public onlyTransferApprover {
+ _approveTransfer(token, from, to, value);
+ }
+
+ function cancelTransferApproval(address token, address from, address to, uint256 value) public onlyTransferApprover {
+ _cancelTransferApproval(token, from, to, value);
+ }
+
+ function approvedCount(address token, address from, address to, uint256 value) public view returns (uint256) {
+ bytes32 transferHash = _transferHash(token, from, to, value);
+ return approvalCounts[transferHash];
+ }
+
+ function approveAndTransferIfAllowed(address token, address from, address to, uint256 value)
+ public
+ onlyTransferApprover
+ returns (bool)
+ {
+ require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken());
+
+ _approveTransfer(token, from, to, value);
+
+ uint256 allowed = IERC20(token).allowance(from, address(this));
+ require(
+ allowed >= value,
+ RuleConditionalTransferLightMultiToken_InsufficientAllowance(token, from, allowed, value)
+ );
+
+ IERC20(token).safeTransferFrom(from, to, value);
+ return true;
+ }
+
+ function transferred(address from, address to, uint256 value)
+ public
+ override(IERC3643IComplianceContract)
+ onlyTransferExecutor
+ {
+ _transferred(_msgSender(), from, to, value);
+ }
+
+ function transferred(
+ address,
+ /* spender */
+ address from,
+ address to,
+ uint256 value
+ )
+ public
+ override(IRuleEngine)
+ onlyTransferExecutor
+ {
+ _transferred(_msgSender(), from, to, value);
+ }
+
+ function transferred(ITransferContext.FungibleTransferContext calldata ctx) external onlyTransferExecutor {
+ _transferred(_msgSender(), ctx.from, ctx.to, ctx.value);
+ }
+
+ function detectTransferRestriction(address from, address to, uint256 value)
+ public
+ view
+ override(IERC1404)
+ returns (uint8)
+ {
+ if (from == address(0) || to == address(0)) {
+ return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+ }
+
+ address token = _msgSender();
+ if (!isTokenBound(token)) {
+ return CODE_TRANSFER_REQUEST_NOT_APPROVED;
+ }
+
+ bytes32 transferHash = _transferHash(token, from, to, value);
+ if (approvalCounts[transferHash] == 0) {
+ return CODE_TRANSFER_REQUEST_NOT_APPROVED;
+ }
+
+ return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+ }
+
+ function detectTransferRestrictionFrom(
+ address,
+ /* spender */
+ address from,
+ address to,
+ uint256 value
+ )
+ public
+ view
+ override(IERC1404Extend)
+ returns (uint8)
+ {
+ return detectTransferRestriction(from, to, value);
+ }
+
+ function canTransfer(address from, address to, uint256 value)
+ public
+ view
+ override(IERC3643ComplianceRead)
+ returns (bool)
+ {
+ return detectTransferRestriction(from, to, value) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+ }
+
+ function canTransferFrom(address spender, address from, address to, uint256 value)
+ public
+ view
+ override(IERC7551Compliance)
+ returns (bool)
+ {
+ return detectTransferRestrictionFrom(spender, from, to, value)
+ == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+ }
+
+ function _authorizeTransferExecution() internal view virtual {
+ require(
+ isTokenBound(_msgSender()),
+ RuleConditionalTransferLightMultiToken_TransferExecutorUnauthorized(_msgSender())
+ );
+ }
+
+ function _authorizeComplianceBindingChange(address) internal virtual override {
+ _onlyComplianceManager();
+ }
+
+ function _approveTransfer(address token, address from, address to, uint256 value) internal virtual {
+ require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken());
+ bytes32 transferHash = _transferHash(token, from, to, value);
+ approvalCounts[transferHash] += 1;
+ emit TransferApproved(token, from, to, value, approvalCounts[transferHash]);
+ }
+
+ function _cancelTransferApproval(address token, address from, address to, uint256 value) internal virtual {
+ require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken());
+ bytes32 transferHash = _transferHash(token, from, to, value);
+ uint256 count = approvalCounts[transferHash];
+
+ require(count != 0, RuleConditionalTransferLightMultiToken_TransferApprovalNotFound());
+
+ approvalCounts[transferHash] = count - 1;
+ emit TransferApprovalCancelled(token, from, to, value, approvalCounts[transferHash]);
+ }
+
+ function _transferred(address token, address from, address to, uint256 value) internal virtual {
+ if (from == address(0) || to == address(0)) {
+ return;
+ }
+
+ bytes32 transferHash = _transferHash(token, from, to, value);
+ uint256 count = approvalCounts[transferHash];
+
+ require(count != 0, RuleConditionalTransferLightMultiToken_TransferNotApproved());
+
+ approvalCounts[transferHash] = count - 1;
+ emit TransferExecuted(token, from, to, value, approvalCounts[transferHash]);
+ }
+
+ function _transferHash(address token, address from, address to, uint256 value)
+ internal
+ pure
+ virtual
+ returns (bytes32 hash)
+ {
+ assembly ("memory-safe") {
+ let ptr := mload(0x40)
+ mstore(ptr, shl(96, token))
+ mstore(add(ptr, 0x20), shl(96, from))
+ mstore(add(ptr, 0x40), shl(96, to))
+ mstore(add(ptr, 0x60), value)
+ hash := keccak256(ptr, 0x80)
+ }
+ }
+}
diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol
new file mode 100644
index 0000000..0466287
--- /dev/null
+++ b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {RuleSharedInvariantStorage} from "../../validation/abstract/invariant/RuleSharedInvariantStorage.sol";
+
+abstract contract RuleConditionalTransferLightMultiTokenInvariantStorage is RuleSharedInvariantStorage {
+ /* ============ Role ============ */
+ bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
+
+ /* ============ State variables ============ */
+ string constant TEXT_TRANSFER_REQUEST_NOT_APPROVED = "ConditionalTransferLightMultiToken: The request is not approved";
+ uint8 public constant CODE_TRANSFER_REQUEST_NOT_APPROVED = 46;
+
+ /* ============ Events ============ */
+ event TransferApproved(address indexed token, address indexed from, address indexed to, uint256 value, uint256 count);
+ event TransferExecuted(address indexed token, address indexed from, address indexed to, uint256 value, uint256 remaining);
+ event TransferApprovalCancelled(
+ address indexed token, address indexed from, address indexed to, uint256 value, uint256 remaining
+ );
+
+ /* ============ Custom error ============ */
+ error RuleConditionalTransferLightMultiToken_TransferExecutorUnauthorized(address account);
+ error RuleConditionalTransferLightMultiToken_InsufficientAllowance(
+ address token, address owner, uint256 allowance, uint256 required
+ );
+ error RuleConditionalTransferLightMultiToken_InvalidToken();
+ error RuleConditionalTransferLightMultiToken_TransferNotApproved();
+ error RuleConditionalTransferLightMultiToken_TransferApprovalNotFound();
+}
diff --git a/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol b/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol
index f299e82..3b4f7da 100644
--- a/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol
+++ b/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol
@@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
+import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol";
import {RuleBlacklistBase} from "../abstract/base/RuleBlacklistBase.sol";
import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol";
@@ -11,7 +12,7 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol";
* @title RuleBlacklistOwnable2Step
* @notice Ownable2Step variant of RuleBlacklist with owner-based authorization hooks.
*/
-contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step {
+contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step, Ownable2StepERC165Module {
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -26,6 +27,17 @@ contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step {
function _authorizeAddressListRemove() internal view virtual override onlyOwner {}
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ virtual
+ override(RuleBlacklistBase, Ownable2StepERC165Module)
+ returns (bool)
+ {
+ return Ownable2StepERC165Module.supportsInterface(interfaceId)
+ || RuleBlacklistBase.supportsInterface(interfaceId);
+ }
+
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
diff --git a/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol b/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol
index 072adbe..e56490b 100644
--- a/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol
+++ b/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol
@@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
+import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol";
import {RuleERC2980Base} from "../abstract/base/RuleERC2980Base.sol";
/**
@@ -11,7 +12,7 @@ import {RuleERC2980Base} from "../abstract/base/RuleERC2980Base.sol";
* @notice Ownable2Step variant of RuleERC2980 with owner-based authorization hooks.
* @dev All whitelist and frozenlist management functions are restricted to the contract owner.
*/
-contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step {
+contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step, Ownable2StepERC165Module {
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -38,6 +39,17 @@ contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step {
function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {}
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ virtual
+ override(RuleERC2980Base, Ownable2StepERC165Module)
+ returns (bool)
+ {
+ return Ownable2StepERC165Module.supportsInterface(interfaceId)
+ || RuleERC2980Base.supportsInterface(interfaceId);
+ }
+
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
diff --git a/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol b/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol
index c46ff38..3dc4def 100644
--- a/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol
+++ b/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol
@@ -3,13 +3,15 @@ pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
+import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol";
+import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol";
import {RuleIdentityRegistryBase} from "../abstract/base/RuleIdentityRegistryBase.sol";
/**
* @title RuleIdentityRegistryOwnable2Step
* @notice Ownable2Step variant of RuleIdentityRegistry.
*/
-contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step {
+contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step, Ownable2StepERC165Module {
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -21,4 +23,15 @@ contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2S
//////////////////////////////////////////////////////////////*/
function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {}
+
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ virtual
+ override(RuleTransferValidation, Ownable2StepERC165Module)
+ returns (bool)
+ {
+ return Ownable2StepERC165Module.supportsInterface(interfaceId)
+ || RuleTransferValidation.supportsInterface(interfaceId);
+ }
}
diff --git a/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol b/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol
index 46a78d4..4cd9e77 100644
--- a/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol
+++ b/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol
@@ -3,13 +3,15 @@ pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
+import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol";
+import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol";
import {RuleMaxTotalSupplyBase} from "../abstract/base/RuleMaxTotalSupplyBase.sol";
/**
* @title RuleMaxTotalSupplyOwnable2Step
* @notice Ownable2Step variant of RuleMaxTotalSupply.
*/
-contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step {
+contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step, Ownable2StepERC165Module {
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -24,4 +26,15 @@ contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step
//////////////////////////////////////////////////////////////*/
function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {}
+
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ virtual
+ override(RuleTransferValidation, Ownable2StepERC165Module)
+ returns (bool)
+ {
+ return Ownable2StepERC165Module.supportsInterface(interfaceId)
+ || RuleTransferValidation.supportsInterface(interfaceId);
+ }
}
diff --git a/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol b/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol
index 47b40fe..69744d4 100644
--- a/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol
+++ b/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol
@@ -4,7 +4,9 @@ pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
+import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol";
import {ERC2771Context} from "../../../modules/MetaTxModuleStandalone.sol";
+import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol";
import {RuleSanctionsListBase} from "../abstract/base/RuleSanctionsListBase.sol";
import {ISanctionsList} from "../../interfaces/ISanctionsList.sol";
@@ -12,7 +14,7 @@ import {ISanctionsList} from "../../interfaces/ISanctionsList.sol";
* @title RuleSanctionsListOwnable2Step
* @notice Ownable2Step variant of RuleSanctionsList.
*/
-contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step {
+contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step, Ownable2StepERC165Module {
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -28,6 +30,17 @@ contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step {
function _authorizeSanctionListManager() internal view virtual override onlyOwner {}
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ virtual
+ override(RuleTransferValidation, Ownable2StepERC165Module)
+ returns (bool)
+ {
+ return Ownable2StepERC165Module.supportsInterface(interfaceId)
+ || RuleTransferValidation.supportsInterface(interfaceId);
+ }
+
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
diff --git a/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol b/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol
index f9c2de8..8948e83 100644
--- a/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol
+++ b/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol
@@ -4,6 +4,8 @@ pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
+import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol";
+import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol";
import {RuleSpenderWhitelistBase} from "../abstract/base/RuleSpenderWhitelistBase.sol";
import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol";
@@ -11,7 +13,7 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol";
* @title RuleSpenderWhitelistOwnable2Step
* @notice Ownable2Step deployment variant of spender whitelist rule.
*/
-contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step {
+contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step, Ownable2StepERC165Module {
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -29,6 +31,17 @@ contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2S
function _authorizeAddressListRemove() internal view virtual override onlyOwner {}
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ virtual
+ override(RuleTransferValidation, Ownable2StepERC165Module)
+ returns (bool)
+ {
+ return Ownable2StepERC165Module.supportsInterface(interfaceId)
+ || RuleTransferValidation.supportsInterface(interfaceId);
+ }
+
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
diff --git a/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol b/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol
index d2f8aaf..0e85c82 100644
--- a/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol
+++ b/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol
@@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
+import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol";
import {RuleWhitelistBase} from "../abstract/base/RuleWhitelistBase.sol";
import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol";
@@ -11,7 +12,7 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol";
* @title RuleWhitelistOwnable2Step
* @notice Ownable2Step variant of RuleWhitelist with owner-based authorization hooks.
*/
-contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step {
+contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step, Ownable2StepERC165Module {
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -37,6 +38,17 @@ contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step {
function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {}
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ virtual
+ override(RuleWhitelistBase, Ownable2StepERC165Module)
+ returns (bool)
+ {
+ return Ownable2StepERC165Module.supportsInterface(interfaceId)
+ || RuleWhitelistBase.supportsInterface(interfaceId);
+ }
+
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
diff --git a/src/rules/validation/deployment/RuleWhitelistWrapper.sol b/src/rules/validation/deployment/RuleWhitelistWrapper.sol
index 99297e1..595eed0 100644
--- a/src/rules/validation/deployment/RuleWhitelistWrapper.sol
+++ b/src/rules/validation/deployment/RuleWhitelistWrapper.sol
@@ -64,6 +64,8 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt
*/
function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {}
+ function _onlyRulesLimitManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {}
+
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
diff --git a/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol b/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol
index d5f7bad..d16380a 100644
--- a/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol
+++ b/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol
@@ -6,13 +6,14 @@ pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
+import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol";
/* ==== Abstract contracts === */
import {RuleWhitelistWrapperBase} from "../abstract/base/RuleWhitelistWrapperBase.sol";
/**
* @title Wrapper to call several different whitelist rules (Ownable2Step)
*/
-contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2Step {
+contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2Step, Ownable2StepERC165Module {
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -36,6 +37,19 @@ contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2S
*/
function _onlyRulesManager() internal view virtual override onlyOwner {}
+ function _onlyRulesLimitManager() internal view virtual override onlyOwner {}
+
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ virtual
+ override(RuleWhitelistWrapperBase, Ownable2StepERC165Module)
+ returns (bool)
+ {
+ return Ownable2StepERC165Module.supportsInterface(interfaceId)
+ || RuleWhitelistWrapperBase.supportsInterface(interfaceId);
+ }
+
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
diff --git a/test/Ownable2Step/Ownable2StepERC165Support.t.sol b/test/Ownable2Step/Ownable2StepERC165Support.t.sol
new file mode 100644
index 0000000..fd3908d
--- /dev/null
+++ b/test/Ownable2Step/Ownable2StepERC165Support.t.sol
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
+import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+import {OwnableInterfaceId} from "RuleEngine/modules/library/OwnableInterfaceId.sol";
+import {Ownable2StepInterfaceId} from "RuleEngine/modules/library/Ownable2StepInterfaceId.sol";
+
+import {RuleBlacklistOwnable2Step} from "src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol";
+import {RuleWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol";
+import {RuleWhitelistWrapperOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol";
+import {RuleSpenderWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol";
+import {RuleERC2980Ownable2Step} from "src/rules/validation/deployment/RuleERC2980Ownable2Step.sol";
+import {RuleSanctionsListOwnable2Step} from "src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol";
+import {RuleIdentityRegistryOwnable2Step} from "src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol";
+import {RuleMaxTotalSupplyOwnable2Step} from "src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol";
+import {RuleConditionalTransferLightOwnable2Step} from "src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol";
+import {
+ RuleConditionalTransferLightMultiTokenOwnable2Step
+} from "src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol";
+import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol";
+
+contract Ownable2StepERC165SupportTest is Test {
+ address internal constant OWNER = address(0xA11CE);
+
+ function testAllOwnable2StepRulesAdvertiseOwnableInterfaces() public {
+ RuleBlacklistOwnable2Step blacklist = new RuleBlacklistOwnable2Step(OWNER, address(0));
+ RuleWhitelistOwnable2Step whitelist = new RuleWhitelistOwnable2Step(OWNER, address(0), false, false);
+ RuleWhitelistWrapperOwnable2Step wrapper = new RuleWhitelistWrapperOwnable2Step(OWNER, address(0), false);
+ RuleSpenderWhitelistOwnable2Step spenderWhitelist = new RuleSpenderWhitelistOwnable2Step(OWNER, address(0));
+ RuleERC2980Ownable2Step erc2980 = new RuleERC2980Ownable2Step(OWNER, address(0), false);
+ RuleSanctionsListOwnable2Step sanctions =
+ new RuleSanctionsListOwnable2Step(OWNER, address(0), ISanctionsList(address(0)));
+ RuleIdentityRegistryOwnable2Step identity = new RuleIdentityRegistryOwnable2Step(OWNER, address(0));
+ RuleMaxTotalSupplyOwnable2Step maxSupply = new RuleMaxTotalSupplyOwnable2Step(OWNER, address(1), 1);
+ RuleConditionalTransferLightOwnable2Step conditional = new RuleConditionalTransferLightOwnable2Step(OWNER);
+ RuleConditionalTransferLightMultiTokenOwnable2Step conditionalMulti =
+ new RuleConditionalTransferLightMultiTokenOwnable2Step(OWNER);
+
+ _assertOwnable2StepInterfaces(address(blacklist));
+ _assertOwnable2StepInterfaces(address(whitelist));
+ _assertOwnable2StepInterfaces(address(wrapper));
+ _assertOwnable2StepInterfaces(address(spenderWhitelist));
+ _assertOwnable2StepInterfaces(address(erc2980));
+ _assertOwnable2StepInterfaces(address(sanctions));
+ _assertOwnable2StepInterfaces(address(identity));
+ _assertOwnable2StepInterfaces(address(maxSupply));
+ _assertOwnable2StepInterfaces(address(conditional));
+ _assertOwnable2StepInterfaces(address(conditionalMulti));
+ }
+
+ function _assertOwnable2StepInterfaces(address target) internal view {
+ IERC165 i = IERC165(target);
+ assertTrue(i.supportsInterface(type(IERC165).interfaceId));
+ assertTrue(i.supportsInterface(OwnableInterfaceId.IERC173_INTERFACE_ID));
+ assertTrue(i.supportsInterface(Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID));
+ assertFalse(i.supportsInterface(type(IAccessControl).interfaceId));
+ assertFalse(i.supportsInterface(bytes4(0xdeadbeef)));
+ }
+}
diff --git a/test/RuleConditionalTransferLightMultiToken/CMTATIntegrationDirectMultiToken.t.sol b/test/RuleConditionalTransferLightMultiToken/CMTATIntegrationDirectMultiToken.t.sol
new file mode 100644
index 0000000..5019fca
--- /dev/null
+++ b/test/RuleConditionalTransferLightMultiToken/CMTATIntegrationDirectMultiToken.t.sol
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {HelperContract} from "../HelperContract.sol";
+import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol";
+import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol";
+import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
+import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol";
+
+contract CMTATIntegrationDirectMultiToken is Test, HelperContract {
+ CMTATStandalone private tokenA;
+ CMTATStandalone private tokenB;
+ RuleConditionalTransferLightMultiToken private rule;
+
+ function setUp() public {
+ CMTATDeployment deploymentA = new CMTATDeployment();
+ CMTATDeployment deploymentB = new CMTATDeployment();
+
+ tokenA = deploymentA.cmtat();
+ tokenB = deploymentB.cmtat();
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS);
+
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ rule.bindToken(address(tokenA));
+ rule.bindToken(address(tokenB));
+ tokenA.setRuleEngine(IRuleEngine(address(rule)));
+ tokenB.setRuleEngine(IRuleEngine(address(rule)));
+
+ tokenA.mint(ADDRESS1, 100);
+ tokenB.mint(ADDRESS1, 100);
+ vm.stopPrank();
+ }
+
+ function testDetectRestriction_IsTokenScoped() public {
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveTransfer(address(tokenA), ADDRESS1, ADDRESS2, 10);
+
+ resUint8 = tokenA.detectTransferRestriction(ADDRESS1, ADDRESS2, 10);
+ assertEq(resUint8, TRANSFER_OK);
+
+ resUint8 = tokenB.detectTransferRestriction(ADDRESS1, ADDRESS2, 10);
+ assertEq(resUint8, CODE_TRANSFER_REQUEST_NOT_APPROVED);
+ }
+
+ function testTransferConsumption_IsTokenScoped() public {
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveTransfer(address(tokenA), ADDRESS1, ADDRESS2, 10);
+
+ vm.prank(ADDRESS1);
+ tokenA.transfer(ADDRESS2, 10);
+
+ assertEq(rule.approvedCount(address(tokenA), ADDRESS1, ADDRESS2, 10), 0);
+ assertEq(rule.approvedCount(address(tokenB), ADDRESS1, ADDRESS2, 10), 0);
+
+ vm.prank(ADDRESS1);
+ vm.expectRevert();
+ tokenB.transfer(ADDRESS2, 10);
+ }
+}
diff --git a/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol
new file mode 100644
index 0000000..9cbd950
--- /dev/null
+++ b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {HelperContract} from "../HelperContract.sol";
+import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol";
+import {MockERC20WithTransferContext} from "src/mocks/MockERC20WithTransferContext.sol";
+
+contract RuleConditionalTransferLightMultiTokenTest is Test, HelperContract {
+ RuleConditionalTransferLightMultiToken private rule;
+ MockERC20WithTransferContext private tokenA;
+ MockERC20WithTransferContext private tokenB;
+
+ function setUp() public {
+ tokenA = new MockERC20WithTransferContext("Token A", "TKNA");
+ tokenB = new MockERC20WithTransferContext("Token B", "TKNB");
+
+ rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS);
+
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ rule.bindToken(address(tokenA));
+ rule.bindToken(address(tokenB));
+ vm.stopPrank();
+
+ tokenA.setRule(address(rule));
+ tokenB.setRule(address(rule));
+
+ tokenA.mint(ADDRESS1, 100);
+ tokenB.mint(ADDRESS1, 100);
+ }
+
+ function testApprovalForTokenADoesNotAuthorizeTokenB() public {
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveTransfer(address(tokenA), ADDRESS1, ADDRESS2, 10);
+
+ vm.prank(ADDRESS1);
+ tokenA.transfer(ADDRESS2, 10);
+
+ assertEq(tokenA.balanceOf(ADDRESS1), 90);
+ assertEq(tokenA.balanceOf(ADDRESS2), 10);
+
+ vm.expectRevert();
+ vm.prank(ADDRESS1);
+ tokenB.transfer(ADDRESS2, 10);
+ }
+
+ function testApproveAndTransferIfAllowedUsesTokenScopedApproval() public {
+ vm.prank(ADDRESS1);
+ tokenA.approve(address(rule), 10);
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveAndTransferIfAllowed(address(tokenA), ADDRESS1, ADDRESS2, 10);
+
+ assertEq(tokenA.balanceOf(ADDRESS1), 90);
+ assertEq(tokenA.balanceOf(ADDRESS2), 10);
+ assertEq(rule.approvedCount(address(tokenA), ADDRESS1, ADDRESS2, 10), 0);
+ assertEq(rule.approvedCount(address(tokenB), ADDRESS1, ADDRESS2, 10), 0);
+ }
+
+ function testApproveTransferRevertsForUnboundToken() public {
+ vm.expectRevert();
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveTransfer(ADDRESS3, ADDRESS1, ADDRESS2, 10);
+ }
+}
diff --git a/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiTokenRuleEngineIntegration.t.sol b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiTokenRuleEngineIntegration.t.sol
new file mode 100644
index 0000000..e652b66
--- /dev/null
+++ b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiTokenRuleEngineIntegration.t.sol
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {HelperContract} from "../HelperContract.sol";
+import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol";
+import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol";
+
+contract RuleConditionalTransferLightMultiTokenRuleEngineIntegrationTest is Test, HelperContract {
+ RuleConditionalTransferLightMultiToken private rule;
+ RuleEngine private sharedRuleEngine;
+
+ function setUp() public {
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ sharedRuleEngine = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS);
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS);
+
+ // In RuleEngine path, rule sees only msg.sender == RuleEngine.
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.bindToken(address(sharedRuleEngine));
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.bindToken(ADDRESS1);
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.bindToken(ADDRESS2);
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ sharedRuleEngine.addRule(rule);
+ }
+
+ function testTokenScopedApprovalIsNotVisibleThroughSharedRuleEngineCallerContext() public {
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10);
+
+ resUint8 = sharedRuleEngine.detectTransferRestriction(ADDRESS2, ADDRESS3, 10);
+ assertEq(resUint8, CODE_TRANSFER_REQUEST_NOT_APPROVED);
+ }
+
+ function testRuleEngineScopedApprovalIsConsumableButNotTokenScoped() public {
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveTransfer(address(sharedRuleEngine), ADDRESS2, ADDRESS3, 10);
+
+ resUint8 = sharedRuleEngine.detectTransferRestriction(ADDRESS2, ADDRESS3, 10);
+ assertEq(resUint8, TRANSFER_OK);
+
+ vm.prank(address(sharedRuleEngine));
+ rule.transferred(ADDRESS2, ADDRESS3, 10);
+
+ resUint8 = sharedRuleEngine.detectTransferRestriction(ADDRESS2, ADDRESS3, 10);
+ assertEq(resUint8, CODE_TRANSFER_REQUEST_NOT_APPROVED);
+ }
+}
diff --git a/test/Version.t.sol b/test/Version.t.sol
index 9bc8030..54580f4 100644
--- a/test/Version.t.sol
+++ b/test/Version.t.sol
@@ -13,7 +13,7 @@ import {RuleERC2980} from "src/rules/validation/deployment/RuleERC2980.sol";
import {RuleConditionalTransferLight} from "src/rules/operation/RuleConditionalTransferLight.sol";
contract VersionTest is Test, HelperContract {
- string constant EXPECTED_VERSION = "0.3.0";
+ string constant EXPECTED_VERSION = "0.4.0";
function testVersionRuleWhitelist() public {
RuleWhitelist rule = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, false);