Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Commit: `TBD`
- Transfer-context struct API: `MultiTokenTransferContext` / `FungibleTransferContext` with an extra `data` field.
- Explicit sanctions oracle clearing via `clearSanctionListOracle()`.
- CMTAT deployment scripts for whitelist and blacklist configurations.
- `DeployCMTATWithBlacklistAndSanctionsList` deployment script — deploys a CMTAT token wired to a `RuleEngine` configured with both `RuleBlacklist` and `RuleSanctionsList`.
- Technical documentation for all rules in `doc/technical/`: added `RuleMaxTotalSupply.md`, `RuleIdentityRegistry.md`, `RuleERC2980.md`, `RuleConditionalTransferLight.md`; updated `RuleWhitelist.md`, `RuleBlacklist.md`, `RuleSanctionList.md`, `RuleWhitelistWrapper.md`.

### Changed

Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ A full-featured variant, `RuleConditionalTransfer`, is maintained as a separate
Deployment scripts:
- `script/DeployCMTATWithWhitelist.s.sol`
- `script/DeployCMTATWithBlacklist.s.sol`
- `script/DeployCMTATWithBlacklistAndSanctionsList.s.sol` — CMTAT + RuleEngine with blacklist and sanctions rules

### Choosing a Rule Variant

Expand Down Expand Up @@ -358,6 +359,21 @@ Several rules are available in multiple access-control variants. Use the simples

All rules are compatible with CMTAT, as noted earlier in this README.

### Technical documentation

Detailed technical documentation for each rule is available in [`doc/technical/`](doc/technical/):

| Rule | Document |
| ---- | -------- |
| RuleWhitelist | [RuleWhitelist.md](doc/technical/RuleWhitelist.md) |
| RuleWhitelistWrapper | [RuleWhitelistWrapper.md](doc/technical/RuleWhitelistWrapper.md) |
| RuleBlacklist | [RuleBlacklist.md](doc/technical/RuleBlacklist.md) |
| RuleSanctionsList | [RuleSanctionList.md](doc/technical/RuleSanctionList.md) |
| RuleMaxTotalSupply | [RuleMaxTotalSupply.md](doc/technical/RuleMaxTotalSupply.md) |
| RuleIdentityRegistry | [RuleIdentityRegistry.md](doc/technical/RuleIdentityRegistry.md) |
| RuleERC2980 | [RuleERC2980.md](doc/technical/RuleERC2980.md) |
| RuleConditionalTransferLight | [RuleConditionalTransferLight.md](doc/technical/RuleConditionalTransferLight.md) |

### Operational Notes

- `RuleIdentityRegistry` allows burns (`to == address(0)`) even if the sender is not verified. This matters only if the token allows self-burn.
Expand All @@ -371,6 +387,7 @@ All rules are compatible with CMTAT, as noted earlier in this README.
- `RuleConditionalTransferLight` approvals are keyed by `(from, to, value)` and are not nonce-based.
- `RuleConditionalTransferLight` provides `approveAndTransferIfAllowed` to approve and immediately execute `transferFrom` when this rule has allowance; it assumes the token calls back `transferred()` during the transfer.
- `RuleConditionalTransferLight` restricts `transferred()` to tokens bound via `bindToken` (ERC3643ComplianceModule).
- `RuleConditionalTransferLight` exempts mints (`from == address(0)`) and burns (`to == address(0)`) from the approval requirement; `created` and `destroyed` delegate to `_transferred`, which returns early for those cases.
- AccessControl variants use `onlyRole(ROLE)` in `_authorize*()` and internal helpers are marked `virtual`.
- AccessControl variants use `AccessControlEnumerable`, so role members can be enumerated with `getRoleMember` / `getRoleMemberCount`. The default admin is treated as having all roles via `hasRole`, but may not appear in role member lists unless explicitly granted.
- `forwarderIrrevocable` is accepted as-is (including `address(0)`), and is not validated against ERC-165 because some forwarders do not implement it.
Expand Down Expand Up @@ -509,7 +526,7 @@ For the moment, there is only one operation rule available: ConditionalTransferL

#### Conditional transfer (light)

This rule requires that transfers must be approved by an operator before being executed. It hashes `(from, to, value)` to track approvals and allows the same transfer to be approved multiple times. Each successful transfer consumes one approval, applying a write operation on the blockchain.
This rule requires that transfers must be approved by an operator before being executed. It hashes `(from, to, value)` to track approvals and allows the same transfer to be approved multiple times. Each successful transfer consumes one approval, applying a write operation on the blockchain. Mints (`from == address(0)`) and burns (`to == address(0)`) are exempt and always pass without requiring approval.

![surya_inheritance_RuleConditionalTransferLight.sol](./doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png)

Expand Down Expand Up @@ -736,9 +753,13 @@ $ anvil

#### Deploy

> **Warning — private key security**
> Passing `--private-key` directly on the command line is **not recommended** in production: the key is visible in your shell history and to any process that can read `/proc`. Prefer hardware wallets (`--ledger`, `--trezor`), encrypted keystores (`--account <keystore>`), or environment-variable signers. See [Foundry best practices](https://www.getfoundry.sh/best-practices) for details.

```shell
$ forge script script/DeployCMTATWithWhitelist.s.sol --rpc-url <your_rpc_url> --private-key <your_private_key>
$ forge script script/DeployCMTATWithBlacklist.s.sol --rpc-url <your_rpc_url> --private-key <your_private_key>
$ forge script script/DeployCMTATWithBlacklistAndSanctionsList.s.sol --rpc-url <your_rpc_url> --private-key <your_private_key>
```

#### Cast
Expand Down
2 changes: 2 additions & 0 deletions doc/technical/RuleConditionalTransferLight.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This rule requires that each transfer be explicitly approved by an operator befo

Each approval is tracked by a hash of `(from, to, value)`. The same transfer tuple can be approved multiple times (approval count > 1) to allow repeated transfers between the same parties for the same amount.

Mints (`from == address(0)`) and burns (`to == address(0)`) are **exempt**: they always pass without requiring an approval. `created` and `destroyed` follow the same path as `transferred` and return immediately for those cases.

## Schema

### Graph
Expand Down
91 changes: 91 additions & 0 deletions script/DeployCMTATWithBlacklistAndSanctionsList.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: MPL-2.0
pragma solidity ^0.8.20;

import {Script} from "forge-std/Script.sol";
import {ICMTATConstructor, CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol";
import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol";
import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
import {RuleEngine} from "RuleEngine/RuleEngine.sol";
import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol";
import {RuleSanctionsList} from "src/rules/validation/deployment/RuleSanctionsList.sol";
import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol";

/**
* @title DeployCMTATWithBlacklistAndSanctionsList
* @notice Deploys a CMTAT token with a RuleEngine enforcing two validation rules:
* a blacklist (RuleBlacklist) and a sanctions screening (RuleSanctionsList).
*
* Deployment order:
* 1. CMTATStandalone — token contract (deployer as temporary admin)
* 2. RuleBlacklist — blocks blacklisted senders / recipients
* 3. RuleSanctionsList — blocks sanctioned addresses via Chainalysis oracle
* 4. RuleEngine — aggregates both rules; token bound at construction
* 5. Wire RuleEngine → CMTAT via setRuleEngine
* 6. Hand over all admin roles to `admin`
*/
contract DeployCMTATWithBlacklistAndSanctionsList is Script {
function deploy(address admin, address forwarder, ISanctionsList sanctionsOracle)
public
returns (
CMTATStandalone token,
RuleEngine ruleEngine,
RuleBlacklist ruleBlacklist,
RuleSanctionsList ruleSanctionsList
)
{
ICMTATConstructor.ERC20Attributes memory erc20Attributes =
ICMTATConstructor.ERC20Attributes("CMTA Token", "CMTAT", 0);
ICMTATConstructor.ExtraInformationAttributes memory extraInformationAttributes =
ICMTATConstructor.ExtraInformationAttributes(
"CMTAT_ISIN",
IERC1643CMTAT.DocumentInfo(
"Terms", "https://cmta.ch", 0x9ff867f6592aa9d6d039e7aad6bd71f1659720cbc4dd9eae1554f6eab490098b
),
"CMTAT_info"
);
ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(address(0)));

// Deploy CMTAT with the deployer as temporary admin so we can configure it.
token = new CMTATStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines);

// Deploy rules; each rule is owned directly by the intended admin.
ruleBlacklist = new RuleBlacklist(admin, address(0));
ruleSanctionsList = new RuleSanctionsList(admin, address(0), sanctionsOracle);

// Deploy RuleEngine with the deployer as temporary admin so we can add rules.
// The token is bound at construction so it is authorised to call transferred().
ruleEngine = new RuleEngine(address(this), forwarder, address(token));

// Register both rules in evaluation order: blacklist first, sanctions second.
ruleEngine.addRule(ruleBlacklist);
ruleEngine.addRule(ruleSanctionsList);

// Connect the RuleEngine to the token.
token.setRuleEngine(IRuleEngine(address(ruleEngine)));

// Transfer admin rights to the intended admin and remove the deployer.
if (admin != address(this)) {
ruleEngine.grantRole(bytes32(0), admin);
ruleEngine.renounceRole(bytes32(0), address(this));
token.grantRole(bytes32(0), admin);
token.renounceRole(bytes32(0), address(this));
}
}

function run()
external
returns (
CMTATStandalone token,
RuleEngine ruleEngine,
RuleBlacklist ruleBlacklist,
RuleSanctionsList ruleSanctionsList
)
{
vm.startBroadcast();
// Pass address(0) for sanctionsOracle to deploy without an oracle configured.
// The oracle can be set post-deployment via RuleSanctionsList.setSanctionListOracle().
(token, ruleEngine, ruleBlacklist, ruleSanctionsList) =
deploy(msg.sender, address(0), ISanctionsList(address(0)));
vm.stopBroadcast();
}
}
8 changes: 6 additions & 2 deletions src/rules/operation/RuleConditionalTransferLight.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ contract RuleConditionalTransferLight is
|| AccessControlEnumerable.supportsInterface(interfaceId);
}

function created(address /* to */, uint256 /* value */) external onlyBoundToken {}
function created(address to, uint256 value) external onlyBoundToken {
_transferred(address(0), to, value);
}

function destroyed(address /* from */, uint256 /* value */) external onlyBoundToken {}
function destroyed(address from, uint256 value) external onlyBoundToken {
_transferred(from, address(0), value);
}

function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ abstract contract RuleConditionalTransferLightBase is VersionModule, RuleConditi
override(IERC1404)
returns (uint8)
{
if (from == address(0) || to == address(0)) {
return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
}
bytes32 transferHash = _transferHash(from, to, value);
if (approvalCounts[transferHash] == 0) {
return CODE_TRANSFER_REQUEST_NOT_APPROVED;
Expand Down Expand Up @@ -149,6 +152,9 @@ abstract contract RuleConditionalTransferLightBase is VersionModule, RuleConditi
}

function _transferred(address from, address to, uint256 value) internal virtual {
if (from == address(0) || to == address(0)) {
return;
}
bytes32 transferHash = _transferHash(from, to, value);
uint256 count = approvalCounts[transferHash];

Expand Down
Loading