Skip to content

Commit b0bb65d

Browse files
committed
test(compliance): add full batch bind/unbind behavior coverage and document
IERC3643ComplianceExtended interface split
1 parent 23d3d5b commit b0bb65d

14 files changed

Lines changed: 392 additions & 45 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ forge lint
8484
- `supportsInterface` advertisement now explicitly includes `IERC1404` in addition to `IERC1404Extend`.
8585
- `RuleEngineOwnable2Step.supportsInterface` now advertises the Ownable2Step-specific interface ID in addition to inherited RuleEngine/Ownable interfaces.
8686
- `ERC3643ComplianceModule` authorization logic now requires explicit per-token approval for token-driven self-bind/self-unbind flows.
87+
- Split compliance interfaces between standard and extensions:
88+
- `IERC3643Compliance` now contains the base ERC-3643 compliance surface.
89+
- supplementary functions are grouped in `IERC3643ComplianceExtended` and advertised through a dedicated ERC-165 extension interface ID.
8790

8891
### Testing
8992

src/RuleEngineBase.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfa
1414
import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
1515

1616
/* ==== Modules === */
17-
import {ERC3643ComplianceModule, IERC3643Compliance} from "./modules/ERC3643ComplianceModule.sol";
17+
import {ERC3643ComplianceModule} from "./modules/ERC3643ComplianceModule.sol";
1818
import {VersionModule} from "./modules/VersionModule.sol";
1919
import {RulesManagementModule} from "./modules/RulesManagementModule.sol";
2020

2121
/* ==== Interface and other library === */
22+
import {IERC3643Compliance} from "./interfaces/IERC3643Compliance.sol";
2223
import {IRule} from "./interfaces/IRule.sol";
2324
import {ComplianceInterfaceId} from "./modules/library/ComplianceInterfaceId.sol";
2425
import {ERC1404InterfaceId} from "./modules/library/ERC1404InterfaceId.sol";
@@ -207,6 +208,7 @@ abstract contract RuleEngineBase is
207208
|| interfaceId == ERC1404InterfaceId.IERC1404_INTERFACE_ID
208209
|| interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID
209210
|| interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID
211+
|| interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_EXTENDED_INTERFACE_ID
210212
|| interfaceId == ComplianceInterfaceId.IERC7551_COMPLIANCE_INTERFACE_ID;
211213
}
212214
}

src/interfaces/IERC3643Compliance.sol

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,6 @@ interface IERC3643Compliance is IERC3643ComplianceRead, IERC3643IComplianceContr
1818
* @param token The address of the token that was unbound.
1919
*/
2020
event TokenUnbound(address token);
21-
/**
22-
* @notice Emitted when self-binding permission is updated for a token.
23-
* @param token The token address whose self-binding permission changed.
24-
* @param approved True if token self-bind/unbind is allowed, false otherwise.
25-
*/
26-
event TokenSelfBindingApprovalSet(address token, bool approved);
2721

2822
/* ============ Functions ============ */
2923
/**
@@ -53,29 +47,6 @@ interface IERC3643Compliance is IERC3643ComplianceRead, IERC3643IComplianceContr
5347
*/
5448
function unbindToken(address token) external;
5549

56-
/**
57-
* @notice Sets whether a token is allowed to self-bind and self-unbind.
58-
* @dev Must be restricted by implementation-specific compliance manager access control.
59-
* @param token The token address to configure.
60-
* @param approved Whether self-binding is approved for `token`.
61-
*/
62-
function setTokenSelfBindingApproval(address token, bool approved) external;
63-
64-
/**
65-
* @notice Sets self-binding approval for multiple tokens in one transaction.
66-
* @dev Must be restricted by implementation-specific compliance manager access control.
67-
* Reverts if any token in `tokens` is the zero address.
68-
* @param tokens The token addresses to configure.
69-
* @param approved Whether self-binding is approved for all provided tokens.
70-
*/
71-
function setTokenSelfBindingApprovalBatch(address[] calldata tokens, bool approved) external;
72-
73-
/**
74-
* @notice Returns whether a token is approved to self-bind and self-unbind.
75-
* @param token The token address to query.
76-
* @return approved True if self-binding is approved for `token`, false otherwise.
77-
*/
78-
function isTokenSelfBindingApproved(address token) external view returns (bool approved);
7950

8051
/**
8152
* @notice Checks whether a token is currently bound to this compliance contract.
@@ -95,16 +66,6 @@ interface IERC3643Compliance is IERC3643ComplianceRead, IERC3643IComplianceContr
9566
*/
9667
function getTokenBound() external view returns (address token);
9768

98-
/**
99-
* @notice Returns all tokens currently bound to this compliance contract.
100-
* @dev This is a view-only function and does not modify state.
101-
* This function is not part of the original ERC-3643 specification
102-
* This operation will copy the entire storage to memory, which can be quite expensive.
103-
* This is designed to mostly be used by view accessors that are queried without any gas fees.
104-
* @return tokens An array of addresses of bound token contracts.
105-
*/
106-
function getTokenBounds() external view returns (address[] memory tokens);
107-
10869
/**
10970
* @notice Updates the compliance contract state when tokens are created (minted).
11071
* @dev Called by the token contract when new tokens are issued to an account.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//SPDX-License-Identifier: MPL-2.0
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {IERC3643Compliance} from "./IERC3643Compliance.sol";
6+
7+
interface IERC3643ComplianceExtended is IERC3643Compliance {
8+
/**
9+
* @notice Emitted when self-binding permission is updated for a token.
10+
* @param token The token address whose self-binding permission changed.
11+
* @param approved True if token self-bind/unbind is allowed, false otherwise.
12+
*/
13+
event TokenSelfBindingApprovalSet(address token, bool approved);
14+
15+
/**
16+
* @notice Associates multiple token contracts with this compliance contract.
17+
* @dev This function is not part of the original ERC-3643 compliance interface.
18+
* Must be restricted by implementation-specific compliance manager access control.
19+
* Reverts if any token is invalid or already bound.
20+
* @param tokens The token addresses to bind.
21+
*/
22+
function bindTokens(address[] calldata tokens) external;
23+
24+
/**
25+
* @notice Removes the association of multiple token contracts from this compliance contract.
26+
* @dev This function is not part of the original ERC-3643 compliance interface.
27+
* Must be restricted by implementation-specific compliance manager access control.
28+
* Reverts if any token is not currently bound.
29+
* @param tokens The token addresses to unbind.
30+
*/
31+
function unbindTokens(address[] calldata tokens) external;
32+
33+
/**
34+
* @notice Sets whether a token is allowed to self-bind and self-unbind.
35+
* @dev This function is not part of the original ERC-3643 compliance interface.
36+
* Must be restricted by implementation-specific compliance manager access control.
37+
* @param token The token address to configure.
38+
* @param approved Whether self-binding is approved for `token`.
39+
*/
40+
function setTokenSelfBindingApproval(address token, bool approved) external;
41+
42+
/**
43+
* @notice Sets self-binding approval for multiple tokens in one transaction.
44+
* @dev This function is not part of the original ERC-3643 compliance interface.
45+
* Must be restricted by implementation-specific compliance manager access control.
46+
* Reverts if any token in `tokens` is the zero address.
47+
* @param tokens The token addresses to configure.
48+
* @param approved Whether self-binding is approved for all provided tokens.
49+
*/
50+
function setTokenSelfBindingApprovalBatch(address[] calldata tokens, bool approved) external;
51+
52+
/**
53+
* @notice Returns whether a token is approved to self-bind and self-unbind.
54+
* @dev This function is not part of the original ERC-3643 compliance interface.
55+
* @param token The token address to query.
56+
* @return approved True if self-binding is approved for `token`, false otherwise.
57+
*/
58+
function isTokenSelfBindingApproved(address token) external view returns (bool approved);
59+
60+
/**
61+
* @notice Returns all tokens currently bound to this compliance contract.
62+
* @dev This function is not part of the original ERC-3643 compliance interface.
63+
* This operation copies the entire storage set to memory and is mainly intended for off-chain reads.
64+
* @return tokens An array of bound token addresses.
65+
*/
66+
function getTokenBounds() external view returns (address[] memory tokens);
67+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
pragma solidity ^0.8.20;
3+
4+
interface IERC3643ComplianceExtendedSubset {
5+
function bindTokens(address[] calldata tokens) external;
6+
function unbindTokens(address[] calldata tokens) external;
7+
function setTokenSelfBindingApproval(address token, bool approved) external;
8+
function setTokenSelfBindingApprovalBatch(address[] calldata tokens, bool approved) external;
9+
function isTokenSelfBindingApproved(address token) external view returns (bool approved);
10+
function getTokenBounds() external view returns (address[] memory tokens);
11+
}

src/modules/ERC3643ComplianceModule.sol

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet
77
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
88
/* ==== Interface and other library === */
99
import {IERC3643Compliance} from "../interfaces/IERC3643Compliance.sol";
10+
import {IERC3643ComplianceExtended} from "../interfaces/IERC3643ComplianceExtended.sol";
1011

11-
abstract contract ERC3643ComplianceModule is Context, IERC3643Compliance {
12+
abstract contract ERC3643ComplianceModule is Context, IERC3643ComplianceExtended {
1213
/* ==== Type declaration === */
1314
using EnumerableSet for EnumerableSet.AddressSet;
1415
/* ==== State Variables === */
@@ -58,6 +59,13 @@ abstract contract ERC3643ComplianceModule is Context, IERC3643Compliance {
5859
_bindToken(token);
5960
}
6061

62+
/// @inheritdoc IERC3643ComplianceExtended
63+
function bindTokens(address[] calldata tokens) public virtual override onlyComplianceManager {
64+
for (uint256 i = 0; i < tokens.length; ++i) {
65+
_bindToken(tokens[i]);
66+
}
67+
}
68+
6169
/**
6270
* @inheritdoc IERC3643Compliance
6371
* @dev Operator warning: unbinding is an administrative operation and does not
@@ -72,14 +80,21 @@ abstract contract ERC3643ComplianceModule is Context, IERC3643Compliance {
7280
_unbindToken(token);
7381
}
7482

75-
/// @inheritdoc IERC3643Compliance
83+
/// @inheritdoc IERC3643ComplianceExtended
84+
function unbindTokens(address[] calldata tokens) public virtual override onlyComplianceManager {
85+
for (uint256 i = 0; i < tokens.length; ++i) {
86+
_unbindToken(tokens[i]);
87+
}
88+
}
89+
90+
/// @inheritdoc IERC3643ComplianceExtended
7691
function setTokenSelfBindingApproval(address token, bool approved) public virtual override onlyComplianceManager {
7792
require(token != address(0), RuleEngine_ERC3643Compliance_InvalidTokenAddress());
7893
_tokenSelfBindingApproval[token] = approved;
7994
emit TokenSelfBindingApprovalSet(token, approved);
8095
}
8196

82-
/// @inheritdoc IERC3643Compliance
97+
/// @inheritdoc IERC3643ComplianceExtended
8398
function setTokenSelfBindingApprovalBatch(address[] calldata tokens, bool approved)
8499
public
85100
virtual
@@ -94,7 +109,7 @@ abstract contract ERC3643ComplianceModule is Context, IERC3643Compliance {
94109
}
95110
}
96111

97-
/// @inheritdoc IERC3643Compliance
112+
/// @inheritdoc IERC3643ComplianceExtended
98113
function isTokenSelfBindingApproved(address token) public view virtual override returns (bool) {
99114
return _tokenSelfBindingApproval[token];
100115
}
@@ -115,7 +130,7 @@ abstract contract ERC3643ComplianceModule is Context, IERC3643Compliance {
115130
}
116131
}
117132

118-
/// @inheritdoc IERC3643Compliance
133+
/// @inheritdoc IERC3643ComplianceExtended
119134
function getTokenBounds() public view override returns (address[] memory) {
120135
return _boundTokens.values();
121136
}

src/modules/library/ComplianceInterfaceId.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ pragma solidity ^0.8.20;
88
*/
99
library ComplianceInterfaceId {
1010
bytes4 public constant ERC3643_COMPLIANCE_INTERFACE_ID = 0x3144991c;
11+
bytes4 public constant ERC3643_COMPLIANCE_EXTENDED_INTERFACE_ID = 0x646ba2be;
1112
bytes4 public constant IERC7551_COMPLIANCE_INTERFACE_ID = 0x7157797f;
1213
}

test/RuleEngine/ERC3643Compliance.t.sol

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,101 @@ contract RuleEngineTest is Test, HelperContract {
319319
ruleEngine.setTokenSelfBindingApprovalBatch(tokens, true);
320320
}
321321

322+
function testCanBindTokensBatch() public {
323+
address[] memory tokens = new address[](2);
324+
tokens[0] = address(token1);
325+
tokens[1] = address(token2);
326+
327+
vm.prank(operator);
328+
ruleEngine.bindTokens(tokens);
329+
330+
assertTrue(ruleEngine.isTokenBound(address(token1)));
331+
assertTrue(ruleEngine.isTokenBound(address(token2)));
332+
}
333+
334+
function testCanUnbindTokensBatch() public {
335+
address[] memory tokens = new address[](2);
336+
tokens[0] = address(token1);
337+
tokens[1] = address(token2);
338+
339+
vm.startPrank(operator);
340+
ruleEngine.bindTokens(tokens);
341+
ruleEngine.unbindTokens(tokens);
342+
vm.stopPrank();
343+
344+
assertFalse(ruleEngine.isTokenBound(address(token1)));
345+
assertFalse(ruleEngine.isTokenBound(address(token2)));
346+
}
347+
348+
function testOnlyComplianceManagerCanBindTokensBatch() public {
349+
address[] memory tokens = new address[](1);
350+
tokens[0] = address(token1);
351+
352+
vm.expectRevert(
353+
abi.encodeWithSelector(
354+
ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR,
355+
user1,
356+
ruleEngine.COMPLIANCE_MANAGER_ROLE()
357+
)
358+
);
359+
vm.prank(user1);
360+
ruleEngine.bindTokens(tokens);
361+
}
362+
363+
function testOnlyComplianceManagerCanUnbindTokensBatch() public {
364+
address[] memory tokens = new address[](1);
365+
tokens[0] = address(token1);
366+
367+
vm.prank(operator);
368+
ruleEngine.bindTokens(tokens);
369+
370+
vm.expectRevert(
371+
abi.encodeWithSelector(
372+
ACCESS_CONTROL_UNAUTHORIZED_ACCOUNT_SELECTOR,
373+
user1,
374+
ruleEngine.COMPLIANCE_MANAGER_ROLE()
375+
)
376+
);
377+
vm.prank(user1);
378+
ruleEngine.unbindTokens(tokens);
379+
}
380+
381+
function testCannotBindTokensBatchWithZeroAddress() public {
382+
address[] memory tokens = new address[](2);
383+
tokens[0] = address(token1);
384+
tokens[1] = address(0);
385+
386+
vm.expectRevert(ERC3643ComplianceModule.RuleEngine_ERC3643Compliance_InvalidTokenAddress.selector);
387+
vm.prank(operator);
388+
ruleEngine.bindTokens(tokens);
389+
}
390+
391+
function testCannotBindTokensBatchWithAlreadyBoundToken() public {
392+
address[] memory tokens = new address[](2);
393+
tokens[0] = address(token1);
394+
tokens[1] = address(token2);
395+
396+
vm.prank(operator);
397+
ruleEngine.bindToken(address(token1));
398+
399+
vm.expectRevert(ERC3643ComplianceModule.RuleEngine_ERC3643Compliance_TokenAlreadyBound.selector);
400+
vm.prank(operator);
401+
ruleEngine.bindTokens(tokens);
402+
}
403+
404+
function testCannotUnbindTokensBatchWithTokenNotBound() public {
405+
address[] memory tokens = new address[](2);
406+
tokens[0] = address(token1);
407+
tokens[1] = address(token2);
408+
409+
vm.prank(operator);
410+
ruleEngine.bindToken(address(token1));
411+
412+
vm.expectRevert(ERC3643ComplianceModule.RuleEngine_ERC3643Compliance_TokenNotBound.selector);
413+
vm.prank(operator);
414+
ruleEngine.unbindTokens(tokens);
415+
}
416+
322417
function testCannotCreatedIfNotBound() public {
323418
vm.expectRevert(ERC3643ComplianceModule.RuleEngine_ERC3643Compliance_UnauthorizedCaller.selector);
324419
ruleEngine.created(user1, 100);

test/RuleEngine/RuleEngineCoverage.t.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import "../HelperContract.sol";
88
import {RuleEngineExposed} from "src/mocks/RuleEngineExposed.sol";
99
import {RuleInvalidMock} from "src/mocks/RuleInvalidMock.sol";
1010
import {ICompliance} from "src/mocks/ICompliance.sol";
11+
import {IERC3643ComplianceExtendedSubset} from "src/mocks/IERC3643ComplianceExtendedSubset.sol";
1112
import {IERC1404Subset} from "src/mocks/IERC1404Subset.sol";
1213
import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol";
14+
import {ComplianceInterfaceId} from "src/modules/library/ComplianceInterfaceId.sol";
1315
import {ERC1404InterfaceId} from "src/modules/library/ERC1404InterfaceId.sol";
1416

1517
/**
@@ -50,6 +52,11 @@ contract RuleEngineCoverageTest is Test, HelperContract {
5052
assertTrue(ruleEngineMock.supportsInterface(type(ICompliance).interfaceId));
5153
}
5254

55+
function testSupportsERC3643ComplianceExtendedSubsetInterface() public view {
56+
assertTrue(ruleEngineMock.supportsInterface(ComplianceInterfaceId.ERC3643_COMPLIANCE_EXTENDED_INTERFACE_ID));
57+
assertTrue(ruleEngineMock.supportsInterface(type(IERC3643ComplianceExtendedSubset).interfaceId));
58+
}
59+
5360
function testSupportsIERC7551ComplianceSubsetInterface() public view {
5461
assertTrue(ruleEngineMock.supportsInterface(type(IERC7551ComplianceSubset).interfaceId));
5562
}

test/RuleEngine/RuleEngineDeployment.t.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import {IAccessControlEnumerable} from "@openzeppelin/contracts/access/extension
1111
import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
1212
import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
1313
import {ICompliance} from "src/mocks/ICompliance.sol";
14+
import {IERC3643ComplianceExtendedSubset} from "src/mocks/IERC3643ComplianceExtendedSubset.sol";
1415
import {IERC7551ComplianceSubset} from "src/mocks/IERC7551ComplianceSubset.sol";
1516
import {IERC1404Subset} from "src/mocks/IERC1404Subset.sol";
17+
import {ComplianceInterfaceId} from "src/modules/library/ComplianceInterfaceId.sol";
1618
import {ERC1404InterfaceId} from "src/modules/library/ERC1404InterfaceId.sol";
1719

1820
/**
@@ -68,6 +70,8 @@ contract RuleEngineTest is Test, HelperContract {
6870
assertTrue(ruleEngineMock.supportsInterface(type(IERC1404Subset).interfaceId));
6971
assertTrue(ruleEngineMock.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID));
7072
assertTrue(ruleEngineMock.supportsInterface(type(ICompliance).interfaceId));
73+
assertTrue(ruleEngineMock.supportsInterface(ComplianceInterfaceId.ERC3643_COMPLIANCE_EXTENDED_INTERFACE_ID));
74+
assertTrue(ruleEngineMock.supportsInterface(type(IERC3643ComplianceExtendedSubset).interfaceId));
7175
assertTrue(ruleEngineMock.supportsInterface(type(IERC7551ComplianceSubset).interfaceId));
7276
}
7377

0 commit comments

Comments
 (0)