Digital tokens have been used to tokenize company equity, allowing equity holders to receive dividends, typically on an annual basis. These dividends, portions of a company's earnings, are distributed to shareholders at the company's discretion and depend on its profits. Generally, dividends are distributed in the currency used for the company’s financial reporting.
When equity is tokenized, dividends can still be distributed in the traditional way, that is, off-chain. However, it might be simpler and more convenient to use a digital ledger's programming capabilities to automatically distribute dividends on-chain. This creates at least two challenges:
-
How could such a mechanism work as part of a smart contract?
-
How can dividends be distributed in the same currency the company relies on, especially for equity that may not be tokenized?
This article focuses on the first question and describes our proposal for a dividend distribution mechanism, where:
-
Company equity parts are tokenized with the CMTAT token, an ERC-20-compatible token.
-
Dividends are paid in another ERC-20 token, in our case, the USDC stablecoin.
-
We keep track of holders' balances at the time of the distribution event using CMTAT' snapshot module.
-
Distribution is performed with our contract, called IncomeVault (a prototype developed within CMTA, not yet validated for production from both legal and technical risk perspectives).
We tested our setup using our product Taurus-CAPITAL, a platform to deploy and manage smart contracts, as well as Taurus-PROTECT, for the custody part. We ran the test on an Ethereum testnet (Sepolia), to avoid the potentially prohibitive gas costs of our solution.
To develop IncomeVault, our main tool is Foundry. To create graphs and UML diagrams in this article, we used sol2uml, Solidity Visual Developer, and Surya.
It is worth mentioning that the issuance and distribution on-chain have already been implemented in real production cases by the Swiss company Obligate with their ENote Protocol. According to their documentation, the protocol uses a modified version of the CMTAT. At the scheduled coupon payment date, a payment redemption token (ERC-20) is distributed to every holder of the eNote (token share). This payment redemption token can be used to claim the payment from an escrow contract. In our case, we do not use an intermediary payment redemption token. Instead, token holders can directly claim their dividends in the IncomeVault, which also serves as our escrow contract.
CMTAT has already been used to represent a tokenized note on the blockchain. You will find more details in our articles: Tokenization Use Cases: Streamlining Issuance, Trading, and Settlement for Debt Securities and SCCF and Taurus Announce Successful Tokenization of End-to-End Trade Finance Transaction on TDX Marketplace.
Components
CMTAT token
CMTAT is an open standard suitable to the tokenization of various financial instruments and used by multiple institutional actors, including large Swiss banks. We used the reference Solidity implementation of CMTAT (v2.4.0). You will find more information in another article on our blog: Security Token Standards: A Closer Look at CMTAT.
CMTAT also contains a Debt module to store information related to debt, such as the interest rate, maturity date, issuance date, and other relevant fields.
CMTAT v2.4.0’'s IDebtGlobal interface
CMTAT’s snapshot module
To distribute tokens with the IncomeVault contract, the tokens must be an ERC-20 and must implement ICMTATSnapshot interface, as defined by CMTAT. The snapshot module records the user’s balance and the total supply at a specific point in time. A snapshot for a given date/time must be created before the snapshot date.
For any operation that changes the balance or total supply (such as burning, minting, or transferring tokens), the snapshot module records the user’s balance and total supply from the most recent past snapshot before applying the operation.
Interface ICMTATSnapshot in CMTAT v2.4.0.
IncomeVault contract
The IncomeVault contract is our Solidity contract which manages the distribution of dividends. It’s available at https://github.com/CMTA/IncomeVault/, and we’ll discuss its internal mechanisms later in this article.
USDC stablecoin
USDC is Circle’s popular USD stablecoin. We decided to use it in our proof of concept (PoC) since it is widely used and a testnet version is available.
Dividend distribution mechanism
Before dividends are distributed, the token issuer must deposit the total dividend amount in a vault. Once claims are open, a token holder can then claim their dividend for a given interval period.
Currently, the vault supports ERC-20 tokens as dividends, which covers the following use cases:
-
Dividends are distributed in one of more ERC-20-compatible tokens, such as stablecoins like USDC or USDT, or custom tokens defined by the tokenized asset issuer.
-
Interest is paid out at configurable intervals, as defined by a parameter (e.g., every 6 months or every year).
The specific case of “stock dividend”, where dividends are distributed in the same asset that is generating the dividend, may also be supported by our protocol. However, this would require careful understanding of how and when the dividend amount is defined, particularly concerning the amount of newly issued shares (i.e., minted tokens).
Workflow
This section explains the steps required for a basic dividend distribution scenario:
-
On the CMTAT, the admin registers the dividend time to perform a snapshot and record each holder’s balance at that specified time.
-
An authorized address deposits the dividend asset (such as USDT) in the IncomeVault for a specific time distribution event.
-
An authorized address opens the claim for that specific time to allow token holders to claim their dividend share.
-
Holders claim their dividend by calling the function claimDividend().
Segregated deposit
Each deposit is segregated from other deposits and is associated to its time value. A time is the dividend distribution date (defined as a Unix timestamp) to the token holders, corresponding to a distribution event after a given dividend interval period.
Execution details
Claiming dividend
The distribution of dividends is not automatic. A token holder must claim their dividends by calling the function claimDividend() in order to receive them, a “pull” approach similar to the common practices (for example, in Lido). When one claims their dividends, they must provide the associated time. Therefore, a token holder has to know the different time when a deposit was performed. A batch version of claimDividend() is also available to claim dividends for several different distribution events (thus multiple time values).
The schema below describes the different smart contracts called when a token holder claims their dividend. All the steps are performed in the same transaction:
-
Step 1: The token holder calls the function claimDividend() from IncomeVault.
-
Steps 2-3: The IncomeVault contract calls the function snapshotInfrom() from CMTAT which returns the user's balance at the specific time.
-
Steps 4-5: The IncomeVault contract calls the internal function _operateOnTransfer() from the ValidationModule (not presented here), which then calls the public function operateOnTransfer() from RuleEngine.
-
Step 6: If the ValidationModule returns true, the dividend transfer from IncomeVault to the token holder is initiated.
Dividend computation formula
The dividend amount per holder (here, transaction sender) is computed as follows:
senderDividend = (senderCMTATBalance × dividendTotalSupply) / TokenTotalSupply;
The dividend value will be rounded down to the next lowest integer. Thus, not all the funds in the pool may be distributed even if all token holders claim their dividend. However, what will remain will be strictly less than the number of token holders, in terms of integer units (i.e., the smallest unit of the dividend asset, similar to how satoshis are to bitcoins).
Here’s an example with dividend in USDC (6 decimals) of a CMTAT (0 decimal) token:
-
Assume a CMTAT token with TokenTotalSupply = 12,351 tokens.
-
The sender holds 4221 tokens.
The issuer decides to deposit $21,555.50 in USDC as a dividend. Since USDC has 6 decimals, our computation uses 21,555,500,000 as the USDC value. We thus have:
senderDividend = 4221 × 21,555,500,000 / 12,351 = 7,366,671,969.880981297
Flooring this decimal number to an integer, we obtain a dividend of 7,366,671,969 units of USDC.
Validation module
A claim is considered a transfer from the contract to the sender (token holder). This transfer can be restricted by the ValidationModule, whose role is depicted in the diagram below.
The ValidationModule is imported from the CMTAT and allows us to:
- Freeze/unfreeze an address
- Put the contract in the pause state
- Configure a rule engine with rules to implement transfer restriction/verification. As relevant rules, we can, for example, configure a whitelist, a blacklist, and a specific sanction list.
If the ValidationModule rejects the transfer, the function is reverted.
Restriction
A holder cannot claim their dividend if:
-
The claim time is in the future.
-
The claim time is too far in the past, violating the limit set by the contract variable timeLimitToWithdraw.
-
The claim is not enabled for this specific time.
-
The holder has already claimed their dividend.
-
There is no dividend to claim.
For the batch function, claimDividendBatch(), d and e don't generate an error. Instead, no dividends are distributed for this specific time.
Withdrawing funds
An authorized user can call the following functions to withdraw funds from the vault:
-
withdraw(uint256 time, uint256 amount, address withdrawAddress)
-
withdrawAll(uint256 amount, address withdrawAddress)
which are both defined as public onlyRole(DEBT_VAULT_WITHDRAW_ROLE). With the first, withdraw(), the funds are withdrawn only from the specific dividend distribution time provided.
The second function allows for the withdrawal of funds without specifying a time, which can lead to an "unstable" state with the different pools of dividends. This means that the balance of the smart contract will be lower than the balance recorded in the dividend pools.
This function should be used only in case of emergency or if the vault is closed.
Distributing dividend
An authorized user can decide to distribute the dividend from a given time (i.e., distribution event) to a given list of addresses. In this case, the token holder cannot decide if they want to receive their dividend (they are forced to accept) and cannot choose the address where they want to receive their dividend.
Note that since the function is restricted by access control, it is not possible to use Chainlink Automation to perform an automatic call and distribute the dividends. Moreover, the list of token holders has to be provided by the transaction’s sender.
Smart contract implementation
This section contains schemas and diagrams describing the current implementation of the IncomeVault protocol. The contract must be deployed with a transparent proxy and is compatible with the standard ERC-2771 for meta transactions (gasless).
Several functions are protected by an access control mechanism. The supported roles include:
Role |
Description |
---|---|
DEFAULT_ADMIN_ROLE |
The admin has the privileges of all the roles. The admin manages all the other roles. |
INCOME_VAULT_DEPOSIT_ROLE |
This role can only deposit dividends in the vault. |
INCOME_VAULT_WITHDRAW_ROLE |
This role can withdraw funds from the vault. |
INCOME_VAULT_DISTRIBUTE_ROLE |
This role can distribute the dividends for a specific time to a list of token holders. |
INCOME_VAULT_OPERATOR_ROLE |
This role can open the claims for a specific time. |
The UML diagram below describes the different contract components. The main contract is IncomeVault.
The flowchart below describes the claimDividend() function:
Future possible improvements of the IncomeVault contract include:
-
An automatic distribution of dividends could be performed through Chainlink Automation, but it requires several changes to our contract.
-
Only ERC20 tokens are currently supported. We could extend this to support native tokens too (such as ETH).
Proof-of-concept execution
We tested our proof of concept on the Sepolia Ethereum testnet. The dividend payment is made in USDC, which we acquired from its Sepolia faucet (at 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238). We deployed a CMTAT with a transparent proxy (see 0x80a14113d3092f896bcc1756ff0d7a90513476a8).
Configuration
We configured tokens as follows: a total supply of 2725 CMTAT tokens, distributed to three addresses as described in the table below. The issuer decides to deposit $20 in USDC as dividend. Since USDC has 6 decimals, the integer value used to compute the dividends is 20,000,000.
|
Tokens |
Dividends |
---|---|---|
1000 |
⌊1000 × 20,000,000 / 2725 ⌋ = 7,339,449 |
|
1500 |
⌊1500 × 20,000,000 / 2725 ⌋ = 11,009,174 |
|
225 |
⌊225× 20,000,000 / 2725 ⌋ = 1,651,376 |
CMTAT
Deployment
Since our contract is deployed with a proxy, a function initialize() replaces the traditional constructor to set our contract.
Taurus-CAPITAL
Functions call
Schedule snapshot
Firstly, we configure our first snapshot by calling the function scheduleSnapshot().
This function takes as parameter the timestamp. We take the date of May 28 2024 06:00:00 GMT+0000 which corresponds to 1716876000 in Unix timestamp format.
CMTAT v2.4.0
Taurus-CAPITAL
See the Transaction on Etherscan.
Tokens minting
We use the function mintBatch() to mint tokens to several different addresses, our token holders in our PoC.
CMTAT v2.4.0
Taurus-CAPITAL
See the Transaction on Etherscan.
IncomeVault
Deployment
We deploy our IncomeVault with a proxy. There are the parameters of the function initialize():
|
Description |
Value |
---|---|---|
Admin |
Admin contract |
Our address 0x3f77cf5501540484410c0bce488f223feaebA800 |
ERC-20 token Payment |
ERC-20 used to pay the dividends |
USDC contract, 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 |
CMTAT token |
Token used to represent the company shares |
|
RuleEngine |
Contract to add restrictions on the claims (whitelist, blacklist, etc.) |
Address 0 since we do not use a ruleEngine in our PoC |
AuthorizationEngine |
Contract to add supplementary restrictions on access control |
Address 0 since we do not use an AuthorizationEngine in our PoC |
TimeLimitToWithdraw |
Time limit, after the dividend date, to claim its dividends (in seconds) |
about ~1 month, which corresponds to 2629743 seconds. |
Taurus-CAPITAL
Deposit
ERC20 approval
We approve our IncomeVault to spend tokens on our behalf, a necessary step before calling the function deposit().
Taurus-CAPITAL
Deposit function call
We call our function deposit().
IncomeVault v1.0.0
Taurus-CAPITAL
See the transaction on Etherscan.
Claims opening
Since we have deposited all the dividends, we now open the claim. This is done with the function setStatusClaim() by specifying the distribution date, or 1716876000 in Unix seconds.
IncomeVault v1.0.0
See the transaction on Etherscan.
Nevertheless, token holders cannot claim dividends before the claim date. If they try to do so, the function will be reverted, as checked in our code.
IncomeVault v1.0.0
Dividends distribution
Holders can claim their dividends, or the issuer can decide to distribute the dividends. In our PoC, we decided to distribute the dividends by calling the function distributeDividend()
with the three holders and the distribution date 1716876000 as argument.
Taurus-CAPITAL
See the transaction on Etherscan.
Etherscan
In Taurus-PROTECT, we can verify the balance of each address:
-
Admin
Taurus-PROTECT
-
Token holder 2
Taurus-PROTECT
- Token holder 3
Taurus-PROTECT
Adversarial and failure cases
Below we briefly comment on the controls in place to prevent basic attacks and failure cases.
Dividend claimed several times
- What if a holder tries to claim the same dividend several times?
When a holder claims their dividends for a specific time, a Boolean is set to true to indicate the claimed dividend:
claimedDividend[tokenHolder][time] = true;
This Boolean is set inside the internal function _transferDividend().
Moreover, the functions to claim are protected against reentrancy attacks with the modifier nonReentrant
from OpenZeppelin.
New dividend after claim
- What happens if the authorized address deposits dividends after a token holder has already claimed their dividends?
The function setStatusClaim()
allows opening (true) or closing (false) the claims for a specific time.
A token holder cannot claim their dividend if the claim status is not opened.
If you close the claim (claim status set to false) and deposit new dividends, the previous token holders will be penalized since the dividend total supply for this specific time has increased for all token holders who have not already claimed their dividends.
In summary, once you have opened the claim, you should not deposit new dividend tokens in the vault for a specific time.
Transfer fails
- What happens if the token transfer fails when a token holder tries to claim their dividends?
In this case, the whole transaction is reverted, and the smart contract still considers that dividends have not been claimed by the token holder (sender).