Skip to content

Commit 1ea7bb2

Browse files
committed
Update to CMTAT v3.3.0-rc1
1 parent d28b470 commit 1ea7bb2

20 files changed

Lines changed: 214 additions & 43 deletions

AGENTS.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,19 @@ function _checkRule(address rule_) internal view virtual override {
127127
### Rule Execution Flow
128128

129129
```
130-
Token.transfer() → RuleEngine.transferred(from, to, value)
131-
├── onlyBoundToken modifier (caller must be bound)
132-
└── for each rule in _rules:
133-
rule.transferred(from, to, value) // reverts if disallowed
130+
Token operation → RuleEngine.transferred(spender, from, to, value) ← used by CMTAT v3.3.0+ for all operations
131+
├── onlyBoundToken modifier (caller must be bound)
132+
└── for each rule in _rules:
133+
rule.transferred(spender, from, to, value) // reverts if disallowed
134+
135+
RuleEngine.transferred(from, to, value) ← 3-arg fallback (spender == address(0))
136+
├── onlyBoundToken modifier
137+
└── for each rule in _rules:
138+
rule.transferred(from, to, value)
134139
```
135140

141+
Since CMTAT v3.3.0, mint (`from == address(0)`) and burn (`to == address(0)`) also go through the 4-argument overload with the operator as `spender`. Rules that check `spender` must skip or adapt that check for mint/burn to avoid blocking those operations unintentionally.
142+
136143
View path: `detectTransferRestriction()` iterates rules, returns first non-zero code.
137144

138145
### Storage: EnumerableSet

CLAUDE.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,19 @@ function _checkRule(address rule_) internal view virtual override {
127127
### Rule Execution Flow
128128

129129
```
130-
Token.transfer() → RuleEngine.transferred(from, to, value)
131-
├── onlyBoundToken modifier (caller must be bound)
132-
└── for each rule in _rules:
133-
rule.transferred(from, to, value) // reverts if disallowed
130+
Token operation → RuleEngine.transferred(spender, from, to, value) ← used by CMTAT v3.3.0+ for all operations
131+
├── onlyBoundToken modifier (caller must be bound)
132+
└── for each rule in _rules:
133+
rule.transferred(spender, from, to, value) // reverts if disallowed
134+
135+
RuleEngine.transferred(from, to, value) ← 3-arg fallback (spender == address(0))
136+
├── onlyBoundToken modifier
137+
└── for each rule in _rules:
138+
rule.transferred(from, to, value)
134139
```
135140

141+
Since CMTAT v3.3.0, mint (`from == address(0)`) and burn (`to == address(0)`) also go through the 4-argument overload with the operator as `spender`. Rules that check `spender` must skip or adapt that check for mint/burn to avoid blocking those operations unintentionally.
142+
136143
View path: `detectTransferRestriction()` iterates rules, returns first non-zero code.
137144

138145
### Storage: EnumerableSet

README.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ This diagram illustrates how a transfer with a CMTAT or ERC-3643 token with a Ru
7777

7878
| RuleEngine version | Compatible Versions |
7979
| ------------------------------------------------------------ | ------------------------------------------------------------ |
80-
| **v3.0.0-rc3** | CMTAT ≥ v3.0.0<br />CMTAT target version: [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0) |
80+
| **v3.0.0-rc3** | CMTAT ≥ v3.0.0<br />CMTAT target version: [v3.3.0](https://github.com/CMTA/CMTAT/releases/tag/v3.3.0) |
8181
| **v3.0.0-rc2** | CMTAT ≥ v3.0.0<br />CMTAT target version: [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0) |
8282
| **v3.0.0-rc1** | CMTAT ≥ v3.0.0<br />CMTAT target version: [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0) |
8383
| **v3.0.0-rc0** | CMTAT ≥ v3.0.0<br /> |
@@ -147,17 +147,29 @@ If you need non-standard helper functions (batch bind/unbind, self-binding appro
147147

148148
### Like CMTAT
149149

150-
Before each ERC-20 transfer, the CMTAT calls the function `transferred` which is the entrypoint for the RuleEngine.
150+
Before each ERC-20 transfer, mint, or burn, CMTAT calls the RuleEngine through the internal function `_checkTransferred`, which dispatches to one of two `transferred` overloads depending on whether a non-zero spender is present.
151151

152152
```solidity
153-
function transferred(address from,address to,uint256 value)
153+
// Called when spender == address(0)
154+
function transferred(address from, address to, uint256 value)
155+
156+
// Called when spender != address(0)
157+
function transferred(address spender, address from, address to, uint256 value)
154158
```
155159

156-
If you want to apply restrictions on the spender address, you have to call the `transferred` function which takes the spender argument in your ERC-20 function `transferFrom`.
160+
#### CMTAT v3.3.0 — mint and burn use the spender path
157161

158-
```solidity
159-
function transferred(address spender,address from,address to,uint256 value)
160-
```
162+
Since CMTAT v3.3.0, **mint and burn operations also go through the 4-argument overload**, with the operator (minter or burner) passed as `spender`:
163+
164+
| Operation | `spender` | `from` | `to` |
165+
|-----------|-----------|--------|------|
166+
| `transfer` / `transferFrom` | caller / approved spender | token holder | recipient |
167+
| `mint` | minter (`_msgSender()`) | `address(0)` | recipient |
168+
| `burn` | burner (`_msgSender()`) | token holder | `address(0)` |
169+
170+
The 3-argument overload is only called when `spender == address(0)`, which does not occur in normal CMTAT v3.3.0 flows.
171+
172+
> **Rule authoring note:** Rules that check the `spender` argument in `transferred(spender, from, to, value)` must explicitly handle the mint (`from == address(0)`) and burn (`to == address(0)`) cases. A spender check that is intended only for `transferFrom` will also fire for mints and burns unless the rule skips it when `from` or `to` is the zero address. See `RuleWhitelist` and `RuleSpenderWhitelist` in the Rules repository for reference implementations.
161173
162174
For example, CMTAT defines the interaction with the RuleEngine inside a specific module, [ValidationModuleRuleEngine](https://github.com/CMTA/CMTAT/blob/master/contracts/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol) and [CMTATBaseRuleEngine](https://github.com/CMTA/CMTAT/blob/master/contracts/modules/1_CMTATBaseRuleEngine.sol).
163175

@@ -169,7 +181,7 @@ For example, CMTAT defines the interaction with the RuleEngine inside a specific
169181

170182
![checkTransferred](./doc/other/CMTAT/checkTransferred.png)
171183

172-
This function `_transferred` is called before each transfer/burn/mint through the internal function `_checkTransferred` defined in [CMTAT_BASE](https://github.com/CMTA/CMTAT/blob/23a1e59f913d079d0c09d32fafbd95ab2d426093/contracts/modules/CMTAT_BASE.sol#L198).
184+
This function `_transferred` is called before each transfer/burn/mint through the internal function `_checkTransferred`.
173185

174186
### Like ERC-3643
175187

script/CMTATWithRuleEngineScript.s.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
pragma solidity ^0.8.20;
66

77
import {Script, console} from "forge-std/Script.sol";
8-
import {ICMTATConstructor, CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol";
8+
import {ICMTATConstructor, CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol";
99
import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol";
1010
import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
1111
import {RuleEngine} from "src/deployment/RuleEngine.sol";
@@ -35,8 +35,8 @@ contract CMTATWithRuleEngineScript is Script {
3535
"CMTAT_info"
3636
);
3737
ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(address(0)));
38-
CMTATStandalone cmtatContract =
39-
new CMTATStandalone(trustedForwarder, admin, erc20Attributes, extraInformationAttributes, engines);
38+
CMTATStandardStandalone cmtatContract =
39+
new CMTATStandardStandalone(trustedForwarder, admin, erc20Attributes, extraInformationAttributes, engines);
4040
console.log("CMTAT cmtatContract : ", address(cmtatContract));
4141
// whitelist
4242
RuleWhitelist ruleWhitelist = new RuleWhitelist(admin, trustedForwarder);
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
pragma solidity ^0.8.20;
3+
4+
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
5+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
6+
import {IRule} from "../../../interfaces/IRule.sol";
7+
import {RuleInterfaceId} from "../../../modules/library/RuleInterfaceId.sol";
8+
import {RuleMintAllowanceInvariantStorage} from "./abstract/RuleMintAllowanceInvariantStorage.sol";
9+
10+
/**
11+
* @title RuleMintAllowance
12+
* @notice Rule that enforces per-minter mint allowances set by the contract admin.
13+
* The admin grants each minter address a maximum amount they may mint in total.
14+
* Each mint deducts from the minter's remaining allowance.
15+
* Burns and regular transfers are unrestricted by this rule.
16+
*/
17+
contract RuleMintAllowance is AccessControl, RuleMintAllowanceInvariantStorage, IRule {
18+
bytes4 private constant RULE_ENGINE_INTERFACE_ID = 0x20c49ce7;
19+
bytes4 private constant ERC1404EXTEND_INTERFACE_ID = 0x78a8de7d;
20+
21+
mapping(address minter => uint256 allowance) public mintAllowance;
22+
23+
/**
24+
* @param admin Address granted DEFAULT_ADMIN_ROLE
25+
*/
26+
constructor(address admin) {
27+
require(admin != address(0), "RuleMintAllowance: zero admin");
28+
_grantRole(DEFAULT_ADMIN_ROLE, admin);
29+
}
30+
31+
/* ============ ERC-165 ============ */
32+
33+
function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, IERC165) returns (bool) {
34+
return interfaceId == RULE_ENGINE_INTERFACE_ID || interfaceId == ERC1404EXTEND_INTERFACE_ID
35+
|| interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || AccessControl.supportsInterface(interfaceId);
36+
}
37+
38+
/* ============ Admin ============ */
39+
40+
/**
41+
* @notice Set the mint allowance for a minter.
42+
* @param minter Address of the minter.
43+
* @param amount Maximum amount the minter is allowed to mint.
44+
*/
45+
function setMintAllowance(address minter, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) {
46+
mintAllowance[minter] = amount;
47+
emit MintAllowanceSet(minter, amount);
48+
}
49+
50+
/* ============ IRule — state-changing ============ */
51+
52+
/**
53+
* @notice Called for transfers where no spender context is available.
54+
* Mint allowance cannot be enforced without a spender; passes through.
55+
*/
56+
function transferred(address from, address to, uint256 value) public view {
57+
// no-op: spender unknown, enforcement requires transferred(spender,...)
58+
}
59+
60+
/**
61+
* @notice Called for every token operation (transfer, mint, burn) with spender context.
62+
* Deducts from the minter's allowance for mints; passes through for burns and transfers.
63+
*/
64+
function transferred(address spender, address from, address /* to */, uint256 value) public {
65+
if (from == address(0)) {
66+
uint256 allowance = mintAllowance[spender];
67+
if (allowance < value) {
68+
revert RuleMintAllowance_InsufficientAllowance(spender, allowance, value);
69+
}
70+
mintAllowance[spender] = allowance - value;
71+
emit MintAllowanceConsumed(spender, value, mintAllowance[spender]);
72+
}
73+
}
74+
75+
/* ============ IRule — view ============ */
76+
77+
/**
78+
* @notice Returns TRANSFER_OK; without spender context mint allowance cannot be evaluated.
79+
*/
80+
function detectTransferRestriction(address, address, uint256) public pure override returns (uint8) {
81+
return uint8(REJECTED_CODE_BASE.TRANSFER_OK);
82+
}
83+
84+
/**
85+
* @notice Returns CODE_MINTER_INSUFFICIENT_ALLOWANCE when a minter's allowance would be
86+
* exceeded. Burns and regular transfers always return TRANSFER_OK.
87+
*/
88+
function detectTransferRestrictionFrom(address spender, address from, address, uint256 value)
89+
public
90+
view
91+
override
92+
returns (uint8)
93+
{
94+
if (from == address(0) && mintAllowance[spender] < value) {
95+
return CODE_MINTER_INSUFFICIENT_ALLOWANCE;
96+
}
97+
return uint8(REJECTED_CODE_BASE.TRANSFER_OK);
98+
}
99+
100+
function canTransfer(address from, address to, uint256 value) public pure override returns (bool) {
101+
return detectTransferRestriction(from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK);
102+
}
103+
104+
function canTransferFrom(address spender, address from, address to, uint256 value)
105+
public
106+
view
107+
override
108+
returns (bool)
109+
{
110+
return detectTransferRestrictionFrom(spender, from, to, value) == uint8(REJECTED_CODE_BASE.TRANSFER_OK);
111+
}
112+
113+
function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) {
114+
return restrictionCode == CODE_MINTER_INSUFFICIENT_ALLOWANCE;
115+
}
116+
117+
function messageForTransferRestriction(uint8 restrictionCode) external pure override returns (string memory) {
118+
if (restrictionCode == CODE_MINTER_INSUFFICIENT_ALLOWANCE) {
119+
return TEXT_MINTER_INSUFFICIENT_ALLOWANCE;
120+
}
121+
return TEXT_CODE_NOT_FOUND;
122+
}
123+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
pragma solidity ^0.8.20;
3+
4+
// forge-lint: disable-next-line(unaliased-plain-import)
5+
import "../../validation/abstract/RuleCommonInvariantStorage.sol";
6+
7+
abstract contract RuleMintAllowanceInvariantStorage is RuleCommonInvariantStorage {
8+
/* ============ Error ============ */
9+
error RuleMintAllowance_InsufficientAllowance(address minter, uint256 allowance, uint256 value);
10+
11+
/* ============ Events ============ */
12+
event MintAllowanceSet(address indexed minter, uint256 allowance);
13+
event MintAllowanceConsumed(address indexed minter, uint256 consumed, uint256 remaining);
14+
15+
/* ============ Restriction codes ============ */
16+
// It is very important that each rule uses a unique code
17+
uint8 public constant CODE_MINTER_INSUFFICIENT_ALLOWANCE = 81;
18+
19+
/* ============ Restriction messages ============ */
20+
string constant TEXT_MINTER_INSUFFICIENT_ALLOWANCE = "MintAllowance: Insufficient allowance for minter";
21+
}

src/mocks/rules/validation/RuleWhitelist.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ contract RuleWhitelist is RuleAddressList, RuleWhitelistCommon {
8484
override
8585
returns (uint8)
8686
{
87-
if (!addressIsListed(spender)) {
87+
// Mint (from == address(0)) and burn (to == address(0)) are exempt from spender check
88+
if (from != address(0) && to != address(0) && !addressIsListed(spender)) {
8889
return CODE_ADDRESS_SPENDER_NOT_WHITELISTED;
8990
} else {
9091
return detectTransferRestriction(from, to, value);

test/HelperContract.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.20;
33

44
// forge-lint: disable-next-line(unused-import)
55
import {Test} from "forge-std/Test.sol";
6-
import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol";
6+
import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol";
77

88
import {RuleEngineInvariantStorage} from "src/modules/library/RuleEngineInvariantStorage.sol";
99
import {RulesManagementModuleInvariantStorage} from "src/modules/library/RulesManagementModuleInvariantStorage.sol";
@@ -72,7 +72,7 @@ abstract contract HelperContract is
7272

7373
// CMTAT
7474
CMTATDeployment cmtatDeployment;
75-
CMTATStandalone cmtatContract;
75+
CMTATStandardStandalone cmtatContract;
7676

7777
// RuleEngine Mock
7878
RuleEngine public ruleEngineMock;

test/HelperContractOwnable.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.20;
33

44
// forge-lint: disable-next-line(unused-import)
55
import {Test} from "forge-std/Test.sol";
6-
import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol";
6+
import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol";
77

88
import {RuleEngineInvariantStorage} from "src/modules/library/RuleEngineInvariantStorage.sol";
99
import {RulesManagementModuleInvariantStorage} from "src/modules/library/RulesManagementModuleInvariantStorage.sol";
@@ -69,7 +69,7 @@ abstract contract HelperContractOwnable is
6969

7070
// CMTAT
7171
CMTATDeployment cmtatDeployment;
72-
CMTATStandalone cmtatContract;
72+
CMTATStandardStandalone cmtatContract;
7373

7474
// RuleEngineOwnable Mock
7575
RuleEngineOwnable public ruleEngineMock;

test/RuleEngine/RulesManagementModuleTest/CMTATIntegration.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import "./CMTATIntegrationBase.sol";
88
* @title General functions of the RuleEngine (v3.2.0-rc2)
99
*/
1010
contract RuleEngineCMTATIntegrationTest is RuleEngineCMTATIntegrationBase {
11-
function _deployCmtat() internal override returns (CMTATStandalone) {
11+
function _deployCmtat() internal override returns (CMTATStandardStandalone) {
1212
cmtatDeployment = new CMTATDeployment();
1313
return cmtatDeployment.cmtat();
1414
}

0 commit comments

Comments
 (0)