Rules is a collection of on-chain compliance and transfer-restriction rules designed for use with the CMTA RuleEngine and the CMTAT token standard.
Each rule can be used standalone, directly plugged into a CMTAT token, or managed collectively via a RuleEngine.
Status: Repository under active development
The RuleEngine is an external smart contract that applies transfer restrictions to security tokens such as CMTAT or ERC-3643-compatible tokens through a RuleEngine.
Rules are modular validator contracts that the RuleEngine or CMTAT compatible token can call on every transfer to ensure regulatory and business-logic compliance.
- Rules are controllers that validate or modify token transfers.
- They can be applied:
- Directly on CMTAT (no RuleEngine required), or
- Through the RuleEngine (for multi-rule orchestration).
- Rules enforce conditions such as:
- Whitelisting / blacklisting
- Sanctions checks
- Multi-party operator-managed lists
- Conditional approvals
- Arbitrary compliance logic
| Component | Compatible Versions |
|---|---|
| Rules v0.1.0 | CMTAT ≥ v3.0.0 RuleEngine v3.0.0-rc0 |
Each Rule implements the interface IRuleEngine defined in CMTAT.
This interface declares the ERC-3643 functions transferred(read-write) and canTransfer(ready-only) with several other functions related to ERC-1404, ERC-7551 and ERC-3643.
Each rule implements the following functions from the ERC-3643 IComplianceinterface
function canTransfer(address _from, address _to, uint256 _amount) external view returns (bool);
function transferred(address _from, address _to, uint256 _amount) external;However, contrary to the RuleEngine, the whole interface is currently not implemented (e.g. createdand destroyed) and as a result, the rule can not directly supported ERC-3643 token.
The alternative to use a Rule with an ERC-3643 token is trough the RuleEngine, which implements the whole ICompliance interface.
It is very important that each rule uses an unique code
Here the list of codes used by the different rules
| Contract | Constant name | Value |
|---|---|---|
| All | TRANSFER_OK (from CMTAT) | 0 |
| RuleWhitelist | CODE_ADDRESS_FROM_NOT_WHITELISTED | 21 |
| CODE_ADDRESS_TO_NOT_WHITELISTED | 22 | |
| CODE_ADDRESS_SPENDER_NOT_WHITELISTED | 23 | |
| Free slot | 24-29 | |
| RuleSanctionList | CODE_ADDRESS_FROM_IS_SANCTIONED | 30 |
| CODE_ADDRESS_TO_IS_SANCTIONED | 31 | |
| CODE_ADDRESS_SPENDER_IS_SANCTIONED | 32 | |
| Free slot | 33-35 | |
| RuleBlacklist | CODE_ADDRESS_FROM_IS_BLACKLISTED | 36 |
| CODE_ADDRESS_TO_IS_BLACKLISTED | 37 | |
| CODE_ADDRESS_SPENDER_IS_BLACKLISTED | 38 | |
| Free slot | 39-44 | |
| RuleConditionalTransfer | CODE_TRANSFER_REQUEST_NOT_APPROVED | 45 |
| Free slot | 46-50 |
Note:
- The CMTAT already uses the code 0-6 and the code 7-12 should be left free to allow further additions in the CMTAT.
- If you decide to create your own rules, we encourage you to use code > 100 to leave free the other restriction codes for future rules added in this project.
Every rule implements the minimal interface expected by CMTAT, notably:
function transferred(address from, address to, uint256 value)
function transferred(address spender, address from, address to, uint256 value)This makes rules directly pluggable into CMTAT without any intermediary RuleEngine.
When used through the RuleEngine, a rule must also implement:
interface IRule is IRuleEngine {
function canReturnTransferRestrictionCode(uint8 restrictionCode)
external
view
returns (bool);
}The RuleEngine can then:
- Aggregate multiple rules
- Execute them sequentially on each transfer
- Return restriction codes
- Mutate rule state (operation rules)
Each rule can be directly plugged to a CMTAT token similar to a RuleEngine.
Indeed, each rules implements the required interface (IRuleEngine) with notably the following function as entrypoint.
function transferred(address from,address to,uint256 value)
function transferred(address spender,address from,address to,uint256 value)/*
* @title Minimum interface to define a RuleEngine
*/
interface IRuleEngine is IERC1404Extend, IERC7551Compliance, IERC3643IComplianceContract {
/**
* @notice
* Function called whenever tokens are transferred from one wallet to another
* @dev
* Must revert if the transfer is invalid
* Same name as ERC-3643 but with one supplementary argument `spender`
* This function can be used to update state variables of the RuleEngine contract
* This function can be called ONLY by the token contract bound to the RuleEngine
* @param spender spender address (sender)
* @param from token holder address
* @param to receiver address
* @param value value of tokens involved in the transfer
*/
function transferred(address spender, address from, address to, uint256 value) external;
}For a RuleEngine, each rule implements also the required entry point similar to CMTAT, and as well some specific interface for the RuleEngine through the implementation of IRuleinterface dfeined in the RuleEngine repository
interface IRule is IRuleEngine {
/**
* @dev Returns true if the restriction code exists, and false otherwise.
*/
function canReturnTransferRestrictionCode(
uint8 restrictionCode
) external view returns (bool);
}
There are two categories of rules: validation rules (Read-only) and operation rules (read-write).
- Cannot modify blockchain state during transfers.
- Used for simple eligibility checks.
- Examples:
- Whitelist
- Whitelist Wrapper
- Blacklist
- Sanction list (Chainalysis)
- Can update state during transfer calls.
- Example:
- Conditional Transfer (approval-based)
Currently, there are four validation rules: whitelist, whitelistWrapper, blacklist, and sanctionlist.
Only whitelisted addresses may hold or receive tokens. Transfers are rejected if:
fromis not whitelistedtois not whitelisted
The rule is read-only: it only checks stored state.
Example
During a transfer, this rule, called by the RuleEngine, will check if the address concerned is in the list, applying a read operation on the blockchain.
Allows independent whitelist groups managed by different operators.
- Each operator manages a dedicated whitelist.
- A transfer is allowed only if both addresses belong to at least one operator-managed list.
- Enables multi-party compliance
This rule inherits from RuleEngineValidationCommon. Thus the whitelist rules are managed with the same architecture and code than for the ruleEngine. For example, rules are added with the functions setRules or addRule.
Opposite of whitelist:
- Transfer fails if either address is blacklisted.
Uses the Chainalysis Oracle to reject transfers involving sanctioned addresses.
- Checks lists for: US, EU, and UN sanctions.
- Documentation: Chainalysis Oracle for sanctions screening
- If
fromortois sanctioned, transfer is rejected.
Documentation and the contracts addresses are available here: Chainalysis oracle for sanctions screening.
Example
During a transfer, if either address (from or to) is in the sanction list of the Oracle, the rule will return false, and the transfer will be rejected by the CMTAT.
For the moment, there is only one operation rule available: ConditionalTransfer.
This rule has been moved to a dedicated repository: RuleConditionalTransfer
This rule requires that transfers must be approved before being executed by the token holders. During the transfer call, the rule will check if the transfer has been approved. If it has, the approval will be removed since the transfer has been processed, applying a write operation on the blockchain.
This rule requires that transfers be approved by the token holders before being executed.
Initially, this rule was designed to implement a specific requirement in Swiss law (Vinkulierung), but it has since been generalized to be more flexible.
According to Swiss law, if a transfer is not approved or denied within three months, the request is considered approved. This option can be activated by setting the option AUTOMATIC_APPROVAL in the rule.
We have added another option, not required by Swiss law, to automatically perform a transfer if the transfer request is approved. This option can be activated by setting the option AUTOMATIC_TRANSFER in the rule.
Reference: Taurus - Token Transfer Management: How to Apply Restrictions with CMTAT and ERC-1404
The modules AccessControlModuleStandalone allows to implement RBAC access control by inheriting from the contract AccessControlfrom OpenZeppelin.
This module overrides the OpenZeppelin function hasRoleto give by default all the roles to the admin.
Each rule implements its own access control by inheriting from the module AccessControlModuleStandalone.
For all rules, the default admin is the address put in argument(admin) inside the constructor and set when the contract is deployed.
See also docs.openzeppelin.com - AccessControl
Common access control between blacklistRuleand WhitelistRule
Here a schema of the Access Control.

Explain how it works.
Here are the settings for Hardhat and Foundry.
-
hardhat.config.js- Solidity v0.8.30
- EVM version: Prague (Pectra upgrade)
- Optimizer: true, 200 runs
-
foundry.toml- Solidity v0.8.30
- EVM version: Prague (Pectra upgrade)
- Optimizer: true, 200 runs
-
Library
-
Foundry v1.5.0
-
Forge std v1.12.0
-
OpenZeppelin Contracts (submodule) v5.5.0
-
CMTAT v3.0.0
-
RuleEngine v3.0.0-rc0
-
The contracts are developed and tested with Foundry, a smart contract development toolchain.
To install the Foundry suite, please refer to the official instructions in the Foundry book.
You must first initialize the submodules, with
forge install
See also the command's documentation.
Later you can update all the submodules with:
forge update
See also the command's documentation.
You also have to install OpenZeppelin inside CMTAT repository (submodule)
cd CMTAT
npm installThe official documentation is available in the Foundry website
forge build
forge compile --sizesYou can run the tests with
forge testTo run a specific test, use
forge test --match-contract <contract name> --match-test <function name>Generate gas report
forge test --gas-reportSee also the test framework's official documentation, and that of the test commands.
A code coverage is available in index.html.
- Perform a code coverage
forge coverage
- Generate LCOV report
forge coverage --report lcov
- Generate
index.html
forge coverage --no-match-coverage "(script|mocks|test)" --report lcov && genhtml lcov.info --branch-coverage --output-dir coverageSee Solidity Coverage in VS Code with Foundry & [Foundry forge coverage](
Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.*
Foundry consists of:
- Forge: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- Cast: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- Anvil: Local Ethereum node, akin to Ganache, Hardhat Network.
- Chisel: Fast, utilitarian, and verbose solidity REPL.
$ forge fmt$ forge snapshot$ anvil$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>$ cast <subcommand>$ forge --help
$ anvil --help
$ cast --helpEach rule implements the IRuleEngine interface
function transferred(address spender, address from, address to, uint256 value)
external;
Called during an ERC-20 token transfer Used by rules to update internal state or enforce operation-based restrictions.
| Name | Type | Description |
|---|---|---|
spender |
address |
Address executing the transfer (owner, operator, or approved). |
from |
address |
Current token holder. |
to |
address |
Recipient address. |
value |
uint256 |
Amount transferred. |
function detectTransferRestriction(address from, address to, uint256 value)
external
view
returns (uint8);
Returns a restriction code describing why a transfer is blocked.
| Name | Type | Description |
|---|---|---|
from |
address |
Sender address. |
to |
address |
Recipient address. |
value |
uint256 |
Amount being transferred. |
| Name | Type | Description |
|---|---|---|
0 |
uint8 |
Transfer allowed. |
| other | uint8 |
Implementation-defined restriction code. |
function messageForTransferRestriction(uint8 restrictionCode)
external
view
returns (string memory);
Returns a human-readable message associated with a restriction code.
| Name | Type | Description |
|---|---|---|
restrictionCode |
uint8 |
Restriction code returned by detectTransferRestriction. |
| Name | Type | Description |
|---|---|---|
message |
string |
Explanation for the restriction code. |
enum REJECTED_CODE_BASE {
TRANSFER_OK,
TRANSFER_REJECTED_DEACTIVATED,
TRANSFER_REJECTED_PAUSED,
TRANSFER_REJECTED_FROM_FROZEN,
TRANSFER_REJECTED_TO_FROZEN,
TRANSFER_REJECTED_SPENDER_FROZEN,
TRANSFER_REJECTED_FROM_INSUFFICIENT_ACTIVE_BALANCE
}
Base transfer restriction codes used by ERC-1404 extensions.
function detectTransferRestrictionFrom(
address spender,
address from,
address to,
uint256 value
)
external
view
returns (uint8);
Restriction code for transfers performed by a spender (approved operator).
| Name | Type | Description |
|---|---|---|
spender |
address |
Address performing the transfer. |
from |
address |
Current token owner. |
to |
address |
Recipient address. |
value |
uint256 |
Transfer amount. |
| Name | Type | Description |
|---|---|---|
code |
uint8 |
0 if transfer allowed, otherwise a restriction code. |
function canTransferFrom(address spender, address from, address to, uint256 value)
external
view
returns (bool);
Determines if a spender-initiated transfer is permitted.
| Name | Type | Description |
|---|---|---|
spender |
address |
Caller executing transfer. |
from |
address |
Token owner. |
to |
address |
Recipient. |
value |
uint256 |
Amount. |
| Name | Type | Description |
|---|---|---|
allowed |
bool |
true if transfer permitted. |
function canTransfer(address from, address to, uint256 value)
external
view
returns (bool isValid);
Returns whether a transfer is compliant.
| Name | Type | Description |
|---|---|---|
from |
address |
Sender. |
to |
address |
Receiver. |
value |
uint256 |
Transfer amount. |
| Name | Type | Description |
|---|---|---|
isValid |
bool |
true if compliant. |
function transferred(address from, address to, uint256 value)
external;
Hook invoked during an ERC-20 token transfer.
| Name | Type | Description |
|---|---|---|
from |
address |
Previous owner. |
to |
address |
New owner. |
value |
uint256 |
Amount transferred. |
This API is common to whitelist and blacklist rules
function addAddresses(address[] calldata targetAddresses)
public
onlyRole(ADDRESS_LIST_ADD_ROLE)
Adds multiple addresses to the internal address set.
- Does not revert if one or more addresses are already listed.
- Restricted to callers holding the
ADDRESS_LIST_ADD_ROLE. - Emits an
AddAddressesevent.
| Name | Type | Description |
|---|---|---|
targetAddresses |
address[] |
Array of addresses to be added to the set. |
function removeAddresses(address[] calldata targetAddresses)
public
onlyRole(ADDRESS_LIST_REMOVE_ROLE)
Removes multiple addresses from the internal set.
- Does not revert if an address is not currently listed.
- Restricted to callers holding the
ADDRESS_LIST_REMOVE_ROLE. - Emits a
RemoveAddressesevent.
| Name | Type | Description |
|---|---|---|
targetAddresses |
address[] |
Array of addresses to be removed. |
function addAddress(address targetAddress)
public
onlyRole(ADDRESS_LIST_ADD_ROLE)
Adds a single address to the set.
- Reverts if the address is already listed.
- Restricted to callers holding the
ADDRESS_LIST_ADD_ROLE. - Emits an
AddAddressevent.
| Name | Type | Description |
|---|---|---|
targetAddress |
address |
Address to add. |
function removeAddress(address targetAddress)
public
onlyRole(ADDRESS_LIST_REMOVE_ROLE)
Removes a single address from the set.
- Reverts if the address is not listed.
- Restricted to callers holding the
ADDRESS_LIST_REMOVE_ROLE. - Emits a
RemoveAddressevent.
| Name | Type | Description |
|---|---|---|
targetAddress |
address |
Address to remove. |
function listedAddressCount() public view returns (uint256 count)
Returns the total number of addresses currently listed in the internal set.
| Name | Type | Description |
|---|---|---|
count |
uint256 |
Total number of listed addresses. |
function contains(address targetAddress)
public
view
override(IIdentityRegistryContains)
returns (bool isListed)
Checks whether a specific address is listed.
Implements IIdentityRegistryContains.
| Name | Type | Description |
|---|---|---|
targetAddress |
address |
Address to check. |
| Name | Type | Description |
|---|---|---|
isListed |
bool |
true if the address is listed, otherwise false. |
function isAddressListed(address targetAddress)
public
view
returns (bool isListed)
Returns whether a given address is included in the internal set.
| Name | Type | Description |
|---|---|---|
targetAddress |
address |
Address to check. |
| Name | Type | Description |
|---|---|---|
isListed |
bool |
Listing status. |
function areAddressesListed(address[] memory targetAddresses)
public
view
returns (bool[] memory results)
Checks the listing status of multiple addresses in a single call.
| Name | Type | Description |
|---|---|---|
targetAddresses |
address[] |
Array of addresses to check. |
| Name | Type | Description |
|---|---|---|
results |
bool[] |
Array of boolean listing results, aligned by index. |
It is possible to add the null address (0x0) to the blacklist. If it is the case, it will not be possible to mint and burn tokens.
addAddress If the address already exists, the transaction is reverted to save gas. addAddresses If one of addresses already exist, there is no change for this address. The transaction remains valid (no revert).
removeAddress If the address does not exist in the whitelist, the transaction is reverted to save gas. removeAddresses If the address does not exist in the whitelist, there is no change for this address. The transaction remains valid (no revert).
Compliance interface for ERC-721 / ERC-1155–style non-fungible assets.
For ERC-721, amount must always be 1.
| Name | Description |
|---|---|
| canTransfer | Verifies whether a transfer is permitted according to the token’s compliance rules. |
function canTransfer(
address from,
address to,
uint256 tokenId,
uint256 amount
) external view returns (bool allowed)
Verifies whether a token transfer is permitted according to the rule-based compliance logic.
- Must not modify state.
- May enforce checks such as allowlists, blocklists, freezing, transfer limits, regulatory rules.
- Must return
falseif the transfer is not permitted.
| Name | Type | Description |
|---|---|---|
from |
address |
Current token owner. |
to |
address |
Receiving address. |
tokenId |
uint256 |
Token ID. |
amount |
uint256 |
Transfer amount (always 1 for ERC-721). |
| Name | Type | Description |
|---|---|---|
allowed |
bool |
true if transfer is allowed; otherwise false. |
Extended compliance interface for ERC-721 / ERC-1155 non-fungible assets. Adds restriction-code reporting, spender-aware checks, and a post-transfer hook.
For ERC-721, amount / value must always be 1.
| Name | Description |
|---|---|
| detectTransferRestriction | Returns a restriction code indicating why a transfer is blocked. |
| detectTransferRestrictionFrom | Returns a restriction code for a spender-initiated transfer. |
| canTransferFrom | Checks whether a spender-initiated transfer is allowed. |
| transferred | Notifies the compliance engine that a transfer has occurred. |
function detectTransferRestriction(
address from,
address to,
uint256 tokenId,
uint256 amount
) external view returns (uint8 code)
Returns a restriction code describing whether and why a transfer is blocked.
- Must not modify state.
- Must return
0when the transfer is allowed. - Non-zero codes should follow ERC-1404 or similar standards.
| Name | Type | Description |
|---|---|---|
from |
address |
Current token holder. |
to |
address |
Receiving address. |
tokenId |
uint256 |
Token ID. |
amount |
uint256 |
Transfer amount (1 for ERC-721). |
| Name | Type | Description |
|---|---|---|
code |
uint8 |
0 if allowed; otherwise a restriction code. |
function detectTransferRestrictionFrom(
address spender,
address from,
address to,
uint256 tokenId,
uint256 value
) external view returns (uint8 code)
Returns a restriction code for a transfer initiated by a spender (approved operator or owner).
- Must not modify state.
- Must return
0when the transfer is permitted.
| Name | Type | Description |
|---|---|---|
spender |
address |
Address performing the transfer. |
from |
address |
Current owner. |
to |
address |
Recipient address. |
tokenId |
uint256 |
Token ID being checked. |
value |
uint256 |
Transfer amount (1 for ERC-721). |
| Name | Type | Description |
|---|---|---|
code |
uint8 |
0 if allowed; otherwise restriction code. |
function canTransferFrom(
address spender,
address from,
address to,
uint256 tokenId,
uint256 value
) external view returns (bool allowed)
Checks whether a spender-initiated transfer is allowed under the compliance rules.
- Must not modify state.
- Should internally use
detectTransferRestrictionFrom.
| Name | Type | Description |
|---|---|---|
spender |
address |
Address executing the transfer. |
from |
address |
Current owner. |
to |
address |
Recipient. |
tokenId |
uint256 |
Token ID. |
value |
uint256 |
Transfer amount (1 for ERC-721 token). |
| Name | Type | Description |
|---|---|---|
allowed |
bool |
true if transfer is allowed. |
function transferred(
address spender,
address from,
address to,
uint256 tokenId,
uint256 value
) external
Signals to the compliance engine that a transfer has successfully occurred.
- May modify compliance state.
- Must be called by the token contract after a successful transfer.
- Must revert if invoked by unauthorized callers.
| Name | Type | Description |
|---|---|---|
spender |
address |
Address executing the transfer. |
from |
address |
Previous owner. |
to |
address |
New owner. |
tokenId |
uint256 |
Token transferred. |
value |
uint256 |
Transfer amount (1 for ERC-721 token). |
Compliance rule enforcing sanctions-screening for token transfers. Integrates a sanctions-oracle (e.g., Chainalysis) to block transfers when the sender, recipient, or spender is sanctioned.
constructor(address admin, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_)Initializes access control, meta-transaction forwarder, and optionally the sanctions oracle.
function setSanctionListOracle(ISanctionsList sanctionContractOracle_)
public
virtual
onlyRole(SANCTIONLIST_ROLE)Set the sanctions-oracle contract used for transfer-restriction checks.
| Name | Type | Description |
|---|---|---|
sanctionContractOracle_ |
ISanctionsList |
Address of the sanctions-oracle. Passing the zero address disables sanctions checks. |
Updates the sanctions-oracle contract reference.
This function may only be called by accounts granted the SANCTIONLIST_ROLE.
Setting the oracle to the zero address is permitted and effectively disables all sanctions-based transfer restrictions.
| Event | Description |
|---|---|
SetSanctionListOracle(address) |
Emitted when the sanctions-oracle address is updated. |
The code is copyright (c) Capital Market and Technology Association, 2022-2025, and is released under Mozilla Public License 2.0.




