When tokenizing securities or debt, issuers may want to enforce restrictions on token ownership and transfer. For instance, they might want to:
-
Allow only addresses on a whitelist to hold tokens.
-
Forbid addresses on a sanction list from holding tokens.
This article describes one of these restrictions called conditional transfer. This restriction requires that transfers must be approved before being executed by the token holders.
In the Swiss law, this allows compliance with a legal restriction called Vinkulierung. This rule states that joint-stock companies can decide to restrict the fundamentally free transferability of membership. Transferability means that the company can refuse to transfer the registered shares based on a legal provision. You can find more information on the legal topic in this article: University of Zürich - Vinkulierung von Namenaktien.
This article presents the implementation of this restriction for CMTAT, a standard to tokenize security and debt on the blockchain. We used for that the reference Solidity implementation of CMTAT (v2.5.0).
If you're not familiar with CMTAT, you can read our detailed explanation articles on the blog: Security Token Standards: A Closer Look at CMTAT and Token Transfer Management: How to Apply Restrictions with CMTAT and ERC-1404.
As indicated in one of our previous articles, CMTAT allows us to add configurable restrictions based on the issuer’s needs through an external contract called RuleEngine. Each restriction is implemented as a rule inside this smart contract.
Rules can be either:
-
Read-only (validation rules): used purely to validate a transfer without modifying the blockchain state, or
-
Read-write (operation rules): used to modify the state during a token transfer.
The ConditionalTransfer rule has been implemented as an operation rule within the RuleEngine, allowing it to modify the storage of the rule during a transfer.
Why use a dedicated rule instead of implementing the solution directly in the CMTAT?
-
This feature is not a standard feature common to all tokens.
-
The CMTAT is currently too “heavy”, and its code size is approaching the maximum limit imposed by Ethereum (24 KB). As a result, adding new features directly into the contract has become increasingly difficult. This limit could however be increased to 256KB in the future if the EIP-7907 is implemented in Ethereum.
-
Thanks to recent updates in the RuleEngine, it is now possible to update the storage of a rule during a transfer call from the CMTAT, using an operation rule. This allows us to extend functionality without inflating the main contract.
To ensure everything is clear, our architecture is built around three smart contracts:
-
CMTAT, an extension of the well-known ERC-20 standard, represents the tokens.
-
The RuleEngine is an external contract used to apply transfer restrictions to the CMTAT. Acting as a controller, it can call different contract rules and apply these rules on each transfer. In our specific case, it will call our ConditionalTransfer rule.
-
The ConditionalTransfer rule to store and manage the approvals for each transfer. When the transfer is made through CMTAT, the approval is revoked in the same transaction as the transfer.
Global Architecture
When a transfer is made through the CMTAT smart contract, CMTAT calls the RuleEngine, if it is set.
The RuleEngine then iterates through all the configured rules and calls the ConditionalTransfer rule.
As already said, the rule for the ConditionalTransfer is part of the RuleEngineOperation
, since this rule updates its own storage.
-
-
The token holder initiates the transfer by calling the
transfer
function. A transfer is defined by three main properties: the token holder (from
), the recipient (to
), and the number of tokens to be transferred (value
). -
If a RuleEngine is configured, the CMTAT smart contract calls the
operateOnTransfer
function of the RuleEngine. -
The RuleEngine goes through the configured rules and calls the ConditionalTransfer rule.
-
The ConditionalTransfer rule checks whether an approval exists for the transfer. If an approval is found, it is removed, and the rule returns
true
to the RuleEngine. The RuleEngine then returnstrue
to CMTAT. -
Once the transfer is validated by the RuleEngine, CMTAT proceeds to execute the remaining code.
-
CMTAT with RuleEngine
ConditionalTransfer rule
But how does the conditional rule work?
Implementation details
In this section, we outline the implementation details of the ConditionalTransfer rule.
Transfer request
A transfer request corresponds to a request to transfer a certain number of tokens (value) from an origin address (from) to a destination address (to).
Each transfer request includes a status and a list of attributes, with the default status set to NONE.
Each request has a status, which changes depending on the decision of the operator.
Status
Status |
Id |
Description |
---|---|---|
NONE |
0 |
Default status or the request has been reset |
WAIT |
1 |
The request is waiting for an approval |
APPROVED |
2 |
The request has been approved |
DENIED |
3 |
The request has been denied |
EXECUTED |
4 |
The request has been executed, which means a successful CMTAT transfer |
Below is a schema of the different statuses:
ConditionalTransfer - Request Status
Workflow example
There are several steps before the token holder can perform a transfer.
When it is initiated by the tokenHolder
-
Firstly, the token holder creates a Transfer Request by calling the corresponding function.
The request status is set to Wait.
-
Then, the operator approves the request.
The status changes to Approved.
-
Finally, the token holder performs the transfer through CMTAT.
The status of the transferRequest is set to Executed.
Remark
-
In our schema, the token holder performs the request of transfer themself. But the operator can also decide to create and approve the request directly in the name of the token holder.
-
When the operator approves the request, they have the possibility to approve only part of the amount. In this case, the request created by the user is set to DENIED, and a new approved request with the amount specified by the operator is created in the same transaction.
Attributes
Name |
Description |
---|---|
key |
Each request is identified by three elements: from, to, and value. From these three elements, a key is computed using the one-way hash function Keccak-256. |
id |
Each request has a unique identifier. Starts from zero. |
from |
Token holder, transfer sender |
to |
Token transfer recipient |
value |
Amount of tokens to transfer |
askTime |
Date where the request has been created by the token holder |
maxTime |
Date limit to perform the transfer after approval |
status |
request status |

RuleEngine v2.0.5
Options and parameters
At the rule issuance, the operator has different options to customize the behavior of the rule.
-
The option AUTOMATIC_APPROVAL defines that if the transfer is not approved or denied within a configurable time limit, the request is considered approved.
-
The option AUTOMATIC_TRANSFER will allow to perform automatically a transfer if the transfer request is approved.
All these parameters can be updated after deployment.
Struct name |
Parameter |
Description |
---|---|---|
AUTOMATIC_TRANSFER |
|
Manage automatic transfer |
|
isActivate |
Activate automatic transfer |
|
cmtat |
CMTAT token contract |
ISSUANCE |
|
|
|
authorizedMintWithoutApproval |
Authorize mint without the need of approval |
|
authorizedBurnWithoutApproval |
Authorize burn without the need of approval |
TIME_LIMIT |
|
|
|
timeLimitToApprove |
Time to approve a request(e.g 7 days) |
|
timeLimitToTransfer |
Time to perform a transfer after the approval (e.g 30 days) |
AUTOMATIC_APPROVAL |
|
|
|
isActivate |
Activate automatic approval |
|
timeLimitBeforeAutomaticApproval |
Time limit before an approval is “automatically” approved. In this case, it is possible to perform the transfer with a status request to WAITING |
This option, if activated, will automatically perform the transfer once the request is approved by the rule operator.
To allow this, the token holder must approve the rule to spend tokens on their behalf using the standard ERC-20 approve function. If the allowance is not sufficient, the request will still be approved, but the transfer will not be performed.
The process works as follows:
-
The token holder approves the sender, in this case the ConditionalTransfer rule, to spend tokens on their behalf.
-
The token holder creates the transfer request.
-
The rule operator approves the request, and the transfer is immediately performed thanks to the approval.
Time condition
-
Once a request is in the “Approved” status, the token holder has a time limit (e.g. 7 days) to perform the transfer. After that, the token holder must begin the whole process again. This parameter can be changed by the rule operator.
-
Once a request is created with the status “Wait”, the operator also has a time limit (e.g. 7 days) to approve the transfer. After that, the token holder must begin the whole process again. This parameter can also be changed by the rule operator.
Optimizations
Batch functions
Additional features also include batch functions, which allow the creation and approval of requests in batch.
-
createTransferRequest => CreateTransferRequestBatch
-
createTransferRequestWithApproval => createTransferRequestWithApprovalBatch
-
approveTransferRequest => approveTransferRequestBatch
-
approveTransferRequestWithId=>approveTransferRequestBatchWithId
-
resetRequestStatus => resetRequestStatusBatch
Whitelist
It is also possible to configure a whitelist by deploying and setting a RuleWhitelist.
When a transfer is performed, if both the from address and the to address are in the whitelist, the transfer is considered valid even if there is no transfer request.
This can make transfers more convenient for known and trusted entities.
RuleEngine v2.5.0
UML diagram
Here is the UML diagram of the solidity code:

The diagram has been made with Solidity Visual Developer and PlantUML.
Proof of concept
Details
Accounts
Here is the same account involved in this proof of concept:
Role |
Address |
Custody |
---|---|---|
Admin |
0xcc6dfa08b554716b59cb80a0cf1f2adbd27a7f0f |
Taurus-PROTECT |
CT operator |
0x776ef597cd2f87d5c2388c02b602434a3a3c6d53 |
Taurus-PROTECT |
Token holder A |
0xD65Fb7036518F4B34482E0a1905Dc6e3Fc379FF0 |
Metamask |
token holder B |
0x5950537bf855bf8b6246ba75da3540031910e83b |
Taurus-PROTECT |
Configuration
We will have the following configuration for our ConditionalTransfer rule:
Option |
Value |
---|---|
authorized Mint without approval |
True |
authorized burn without approval |
True |
Time limit for an operator to approve |
5 days (432000 seconds) (not really useful since we enable the automatic approval option) |
Time limit to transfer |
3 days = 259200 seconds |
Time limit before automatic approval |
Enabled 3 days = 259200 seconds |
Automatic transfer |
Enabled |
We will test a different scenario:
Scenario |
From |
To |
Value |
Operator |
Token holder |
Result |
---|---|---|---|---|---|---|
1 |
Token Holder A |
Token holder B |
100 with enough allowance |
Approved |
- |
Transfered performed when the operation approved the request |
2 |
Token Holder A |
Token holder B |
101 |
Refused |
Tries to perform the transfer |
Smart Contract Error |
3 |
Token Holder A |
Token holder B |
20 |
No action => accepted after 3 days |
Perform the transfer |
Tokens transfered successfully |
4 |
Token Holder A |
Token holder B |
30 |
Approved |
Perform the transfer but after the limit (3 days) |
Smart Contract Error |
5 |
Token Holder A |
Token holder B |
150 without allowance |
Approved |
Perform the transfer |
Tokens transfered successfully |
6 |
Token Holder A |
Token holder B |
80 |
Approved partially for 40 tokens |
Perform the transfer for 40 tokens |
Tokens transfered successfully |
Functions
This tab summarizes the main functions to call in order to perform the different operations.
Contract |
Description |
Signature with arguments |
---|---|---|
CMTAT |
|
|
|
Mint tokens in batch |
mintBatch(address[] accounts, uint256[] values) |
RuleEngine |
Grant a role to a specific account
|
grantRole(bytes32 role, address account) |
|
Add a new operation rule |
addRuleOperation(address rule_) |
ConditionalTransfer rule |
|
|
|
Create transfer request |
createTransferRequest(address to, uint256 value) |
|
Approve transfer request |
approveTransferRequest((address,address,uint256) keyElement, uint256 partialValue,bool isApproved) |
|
Approve transfer request in batch |
approveTransferRequestBatch((address,address,uint256)[] keyElements, uint256[] partialValues,bool[] isApproved) |
Step
-
Deploy RuleEngine
-
Deploy CMTAT
-
Grant CMTAT inside the RuleEngine
-
Mint CMTAT tokens
-
Deploy the rule ConditionalTransfer
-
Add the rule to the RuleEngine
-
Perform the different scenario
Deployment and configuration
Summary
Deploy the contract summary.
Contract |
Address |
---|---|
CMTAT |
|
RuleEngine |
|
Rule Conditional Transfer |
RuleEngine
Parameter |
Value |
---|---|
admin |
0xcc6dfa08b554716b59cb80a0cf1f2adbd27a7f0f |
forwarderIrrevocable |
0x0000000000000000000000000000000000000000 |
tokenContract |
0x0000000000000000000000000000000000000000 |
Our tokenContract (CMTAT) will be set after deployment.
Taurus-CAPITAL
You can put a gas limit of 2,000,000.
Result
The contract is deployed at the following address: 0x38b7b917c841ef47b13529ef71ae1de440379b65
CMTAT
Here are the constructor arguments to deploy CMTAT.
Group |
Subgroup |
Value |
---|---|---|
forwarder |
|
0x0000000000000000000000000000000000000000 |
admin |
|
0xcc6dfa08b554716b59cb80a0cf1f2adbd27a7f0f |
ERC20Attributes_ |
|
|
|
nameIrrevocable |
CMTAT CT |
|
symbolIrrevocable |
CMTAT CT |
|
decimalsIrrevocable |
0 |
baseModuleAttributes_ |
|
|
|
tokenId |
0 |
|
terms |
|
|
information |
CMTAT conditional transfer |
engines_ |
|
0x0000000000000000000000000000000000000000 |
|
ruleEngine |
0x38b7b917c841ef47b13529ef71ae1de440379b65 |
|
debtEngine |
0x0000000000000000000000000000000000000000 |
|
authorizationEngine |
0x0000000000000000000000000000000000000000 |
|
documentEngine |
0x0000000000000000000000000000000000000000 |
The view inside Taurus-CAPITAL:
Taurus-CAPITAL
You can put a gas limit of 6,000,000.
The contract is deployed at the following address: 0x22fe3168c78dc9e6a2e71bca0caa990439a4e6b7
Add CMTAT to the ruleEngine
Contract called: RuleEngine
Function signature: grantRole(bytes32 role, address account)
To allow our newly deployed CMTAT to use our RuleEngine, we have to grant the required authorization.
Otherwise, when we perform a call through CMTAT (transfer, mint, burn), the transaction will be rejected with the following error AccessControlUnauthorizedAccount(0xe2517d3f).
Taurus-CAPITAL
See transaction on Sepolia Etherscan.
ConditionalTransfer
Parameters for the constructor of our ConditionalTransfer rule:
Parameter |
Value |
---|---|
admin |
0xcc6dfa08b554716b59cb80a0cf1f2adbd27a7f0f |
forwarderIrrevocable |
0x0000000000000000000000000000000000000000 |
RuleEngineContract |
Option |
Options parameters |
Value |
---|---|---|
issuance |
|
|
|
authorizedMintWithoutApproval |
True |
|
authorizedBurnWithoutApproval |
True |
timeLimit |
|
|
|
timeLimitToApprove |
5 days = 432000 seconds |
|
timeLimitToTransfer |
3 days = 259200 seconds |
automaticApproval |
|
|
|
isActivate |
True |
|
timeLimitBeforeAutomaticApproval |
3 days = 259200 seconds |
automaticTransfer |
|
|
|
isActivate |
True |
|
cmtat smart contract |

If you haven't deployed the RuleEngine or CMTAT prior to deploying the rule, you can:
-
Add the RuleEngine by calling the grantRole function, and
-
Configure CMTAT through the corresponding automaticTransfer option.
You can put a gas limit of 4,000,000.
Taurus-CAPITAL
Result
Taurus-CAPITAL
Add an operator
Contract called: ConditionalTransfer Rule
Function signature: grantRole(bytes32 role, address account)
While we could use our admin account to approve requests, in this case, we'll designate a specific address as the operator authorized to approve various requests.
The assigned role is a 32-byte identifier stored in the constant RULE CONDITIONAL TRANSFER OPERATOR ROLE.
Parameter |
Value |
---|---|
Role |
0xb4ff2b395d3b18c7b6759ad944b9ceb191d722e684051da7f6c0811d99f2b011 |
Account |
0x776ef597cd2f87d5c2388c02b602434a3a3c6d53 |
Taurus-CAPITAL
Add the rule to the RuleEngine
Contract called: RuleEngine
Function signature: addRuleOperation(address rule_)
See transaction on Sepolia Etherscan.
Operation
Mint tokens
Contract called: CMTAT
Signature: mintBatch(address[] accounts, uint256[] values)
We will mint tokens and send them to a specific holder (Holder A in our scenario), who will own the tokens outside of PROTECT with Metamask, a self-custodial browser wallet.
The CMTAT code is available on GitHub: CMTAT v2.5.0.
Taurus-CAPITAL
Import tokens on Metamask
After importing the tokens on Metamask, we can see the balance in the wallet.

Metamask-CMTAT import
Metamask view
Since the contract is verified on Sepolia Etherscan, the token holder will use Etherscan to initiate the transfer request.
Scenario
Scenario 1 (approved with ERC20 approval)
Our conditional transfer operator will approve the request inside PROTECT.
The process involves the following steps:
-
Mint tokens to the token holder
-
The token holder
a. approves the Conditional Transfer rule to spend tokens on their behalf
b. submits the transfer request
-
The operator approves the request
Before the transfer
The function validateTransfer returns false.Etherscan View
The function detectedTransferRestriction returns an error code, in this case 51.Etherscan View
The function messageForTransferRestriction returns the following message: The request is not approved.Etherscan View
With our token holder
Approve the rule as spender
Through Etherscan, our token holder can authorize the rule to spend tokens on their behalf in order to perform the transfer.Etherscan View
We confirm the transaction using our MetaMask wallet.
Metamask
MetamaskCheck allowance
Then, we can call the function allowance to check the result.
Etherscan View
See transaction on Sepolia Etherscan.
createTransferRequest
Contract called: ConditionalTransfer Rule
Function signature: function createTransferRequest(address to, uint256 value)
A token holder can create a request by calling the function createTransferRequest.
Argument |
Value |
---|---|
to |
Token Holder B 0x5950537bf855bf8b6246ba75da3540031910e83b |
value |
100 |
Since CMTAT has a decimal of 0, we create a transfer request to transfer 1000 CMTAT tokens to our token holder B.
To perform this, we will use Etherscan and our self-custodial wallet MetaMask.
In the tab Contract, we have all the read and write functions available.
Etherscan View
After connecting our wallet using the 'Connect to Web3' button, we can perform a write call to create a new request.Etherscan View
After that, MetaMask prompts us to confirm the transaction.
Metamask
See transaction on Sepolia Etherscan.
With our rule operator
approveTransferRequest
The operator, in our case, the smart contract admin, approves the request by calling the function approveTransferRequest.
Taurus-CAPITAL
Verify the transfer
Since the token holder has granted allowance to the rule, the transfer is executed immediately once the request is approved by the operator.
Etherscan View - Transfer
Since the transfer has already been performed, the allowance is now 0.
Etherscan View - Allowance
If you check the status of the request, you'll see that it is not marked as approved. But why? This is because the transfer was executed immediately during the approval process. To prevent double-spending, the request is transitioned directly from the APPROVED status to EXECUTED, bypassing the intermediate state.
Etherscan View
Scenario 2 (denied)
In this scenario, Token Holder A submits a transfer request to send 101 tokens to Token Holder B.
However, the request is not approved by the operator.
As a result, if Token Holder A attempts to perform the transfer, the request will be rejected by the CMTAT smart contract.
To save time and align with the other scenarios, we will now proceed to create the transfer requests for all the remaining scenarios.
Create request
Etherscan View
See transaction on Sepolia Etherscan.
Approve request
Once created, it is possible to get the different information, e.g. the id, by calling the function getRequestTrade.
Taurus-CAPITAL
After that, it is possible to approve the request with its id which is 1 in our example.
Taurus-CAPITAL
Or we can also approve the request with the different key elements (from, to, amount).
Taurus-CAPITAL
Perform the transfer
If we attempt to perform the transfer, MetaMask detects that the transaction will be rejected by the smart contract and displays a warning.
Etherscan View - Transfer
Scenario 3 (automatic approval)
In this scenario, we wait for the delay (3 days) before automatic approval.
After 3 days, the holder can perform the transfer even if the request is not approved.
Etherscan View - Transfer
Etherscan View - Transaction details
See transaction on Sepolia Etherscan.
Scenario 4 (time limit to transfer)
We will first approve the requests for Scenario 4 and Scenario 5 in batch.
Taurus-CAPITAL
Three days later, we can see that the transfer is not considered as valid because we have exceeded the time limit to perform the transfer (three days).
Etherscan View
If we check the status, we can see that the request is still in APPROVED (2).
Taurus-CAPITAL
Taurus-CAPITAL
Scenario 5 (approved without ERC20 allowance)
In the case of Scenario 5, there is no allowance set before the approval. As a result, contrary to Scenario 1, the user must perform the transfer themself after the approval is granted.
The transfer can be executed either by calling the ERC-20 transferFrom function directly via Etherscan, as in Scenario 3, or by using the Send feature in MetaMask.
Metamask View
See transaction on Sepolia Etherscan.
Etherscan View
Scenario 6 (partial approval)
Our token holder A creates the request to transfer 80 CMTAT tokens.
Etherscan View
See transaction on Sepolia Etherscan.
Then, the operator approves the request, but only for 40 tokens.
Taurus-CAPITAL
See transaction on Etherscan Sepolia.
We can see that a transfer for 80 tokensis not valid.
Taurus-CAPITAL
But a transfer for 40 tokens is valid.
Taurus-CAPITAL
Our token holder A can now perform the transfer with their Metamask wallet.
Metamask View
Metamask View
See transaction on Sepolia Etherscan.
Etherscan View
FAQ
Other standard implementations
ERC-1400 Polymath implementation
The Polymath implementation of ERC-1400 also includes a conditional transfer feature, enabled through their external module ManualApprovalTransferManager
However, in this case, a token holder cannot initiate a transfer request directly. Instead, they must contact the issuer off-chain, who will then create and approve the request on their behalf.
ERC-3643 (Tokeny)
The ConditionalTransfer feature is available in the compliance module ApproveTransfer
Here are the main differences with our rule:
In less:
-
Only the operator can create and approve a request. A request cannot be initiated on-chain by a token holder.
-
They do not use the concept of “Status”. A request basically has only two statuses: “Approved” or “Not approved”.
In more:
-
They can manage approvals for several tokens, which is not currently available in our version.
-
You can have several approvals for the same transfer request, allowing the same transfer to be performed multiple times without having to wait for each transfer to take place before making the approval.
TransferFrom
With transferFrom, what is the corresponding workflow?
With transferFrom, the sender (who has received approval from the token holder), also called the spender, is not part of the ConditionalTransfer workflow. Thus, a valid request for “from, to, value” must exist.
No checks are performed on the sender of the transferFrom call.
Create several transfer requests with the same value/amount
Can I create several transfer requests with the same value/amount?
It is not possible to create multiple transfer requests with the same value, since each request is uniquely identified by a key in the form of hash(from, to, value), and we do not allow to have several pending approvals/transfer for the same request.