This series of articles aims to aid developers with previous experience of tokenizing real-world assets (RWAs) on Ethereum Virtual Machine (EVM) chains to implement similar tokens on Canton Network. Most Solidity contracts that tokenize RWAs are based on the ERC20 standard, so this series of articles starts with discussing how ERC20 standard is used to tokenize RWAs and how to implement similar (actually better) functionality in Daml.
ERC20 defines six mandatory methods that facilitate querying and updating the account balance, the total supply of the token and the token transfer. Before we can discuss how to implement this functionality in Daml, we need to make an excursion into the difference in the data models used to track ownership and state changes on EVM vs. Canton.
One difference is that on EVM the address may be an identifier for a smart contract or for an external account, also referred to as user or wallet. In Daml an identifier for a smart contract and an identifier for a party that can act on the ledger are two different types (ContractId and Party). Since Daml is a strongly typed language, you can never mix up or confuse a ContractId with a Party.
On EVM chains an ERC20 contract includes a map between account addresses and balances, which is stored as a mutable state variable. This model resembles a traditional banking ledger, where a bank maintains records of customers’ account balances, and transactions modify those balances.
Canton uses a model known as Unspent Transaction Output or UTXO to represent assets on the ledger. This model is used on a number of non-EVM chains including Bitcoin.
A useful mental model for representing assets in UTXO is a physical wallet, where you store paper banknotes. In a physical wallet you may have three one dollar bills, one five dollar bill, two tens and one twenty. The total amount in this wallet is forty eight dollars. Translating this mental model to Daml contracts that represent a tokenized money market fund (MMF), you may own one contract that represents 1.05 shares of the MMF, another one that represents 2.3 shares and another one that represents 0.21 shares. This makes you the owner of 3.56 shares of the fund.
The UTXO model necessitates that a contract representing an asset carries the record of ownership. This is usually implemented by including a field named owner of the type Party in the Daml template for the contract.
template MyAsset
with
owner: Party
amount: Decimal
A Daml template is the source code for a Daml contract on Canton much the same way as the code identified by the contract keyword in Solidity is the source code for a smart contract on EVM blockchain.
Daml contracts are immutable. They can be created and archived, but cannot be mutated once created. State mutation on Canton Network is achieved by creating and archiving contracts. For example, mutation of contract data can be modeled by archiving the active contract and creating a new one in its stead with modified values of some fields. A Daml transaction is a unit of state mutation on Canton Network much like a block is a unit of state mutation on EVM chains. A Daml transaction can take active (previously created and not archived) contracts as input, it can archive (spend) some or all of them. It can also create new contracts. The set of contracts that have been used as input to the transaction and not archived by it, plus the new contracts created by the transaction and not archived by it, represent the unspent transaction output, which is where the UTXO model takes its name from.
It is possible to emulate in Daml the account model used on EVM. For example, we can implement a contract named Bank that stores a map of customer accounts and their balances. However, this contract and all the data stored as part of it will need to be visible to all clients of the bank. In other words, just like with ERC20 tokens on EVM chains, account holders will be able to view each other’s account balances. Instead of a single Bank contract storing all customer account balances, we could implement a separate Account contract for each customer. This can limit the visibility of the account only to the parties that need to interact with it - the owner, the bank and the bank customers that send or receive transfers to or from the account. But even in this case, because in Daml all parties involved in a transaction need to verify it, hence need to see its content, the account balance of the sender will be revealed to the receiver and vice versa when sending a transfer. This is unacceptable for traditional finance (TradFi). Canton Network offers fine-grained programmable privacy by design, where transaction data is distributed to network participants on a strict need-to-know basis. This is what sets Canton Network apart from all other blockchains. To take advantage of this unique feature, tokenization of RWAs is always implemented in Daml using the native UTXO model.
Unlike in the EVM model, where account balance and total supply are on-ledger functions that must be implemented by an ERC20 contract, in UTXO model the account balance is the sum of the amounts of the unspent token contracts owned by a party, and total supply is the sum of the amounts of unspent token contracts. Since contracts in the UTXO model are by design not aware of each other, the account balance and the total supply cannot be implemented as a function of a smart contract. In other words, they cannot be implemented in Daml templates. Instead, they are implemented in ledger clients by querying the ledger for all active contracts satisfying certain criteria, such as having been created from a given Daml template and having a given party ID value of the owner field. The values of the amount fields from all contracts satisfying the criteria can then be added up returning the account balance.
To be able to query the ledger for the right contracts we need to be able to distinguish tokens representing different financial instruments. For example, we need to be able to distinguish between contracts representing USDC stablecoin and tokens representing other stablecoins or MMFs. On EVM chains a financial instrument like a particular stablecoin (e.g. USDC) or a specific fund (e.g. Blackrock BUIDL) is usually represented by a single contract (that implements the ERC20 standard). Therefore the instrument can be identified by its contract address. In UTXO a contract represents a holding or an ownership record of a specific amount of a financial instrument. For example there can be a contract representing 3 USDC owned by Alice, another contract representing 5 USDC owned by Alice, yet another contract representing 2 USDC owned by Bob and so on. The distinction between different instruments is usually facilitated by including information about the issuer party in the contract, often together with some other metadata like the token symbol.
template MyAsset
with
issuer: Party
symbol: Text
owner: Party
amount: Decimal
However, merely including the metadata into the contract would not be sufficient. What makes a ten US dollar banknote genuine is not the words “Federal Reserve Note” or the portrait of Alexander Hamilton printed on it. If I wrote these words and drew a picture of Hamilton on a piece of paper, it wouldn’t make this piece of paper a ten dollar bill. Besides, most of the money in circulation is issued in electronic form and doesn’t bear any signs or pictures. What makes a US dollar genuine is that its issuance, whether in paper form or electronic, is authorized by the US Federal Reserve. As long as it can be proven that the ten dollars that I own have been issued by the Federal Reserve, these ten dollars are genuine and can be used as a legal tender in financial transactions. The issuance authorization is outside of the scope of ERC20. In Solidity it is usually implemented by introducing the contract owner/admin or minter/burner role. For example, the following code snippet restricts the ability to call the mint function to mint new quantities of the token to the address that originally created the contract
constructor() {
minter = msg.sender;
}
function mint(address recipient, uint amount) public {
require(msg.sender == minter);
balances[recipient] += amount;
}
In Daml the concept of issuance authorization is represented by the signatory keyword in a Daml template. The signatory keyword is followed by an expression returning a set of Daml parties. The collective authorization of all signatory parties is required to create or archive a contract on Canton Network. By the same token, no pun intended, the presence of a party ID among the signatories on a contract on Canton Network is an attestation that this party authorized the creation of the contract. To ensure proper authorization of a token mint, all we need to do is to include the issuer party as a signatory on the contract. This way the issuer’s authorization will be required for every creation and every archival of a contract representing the instrument holding, which provides protection against double spend.
template MyAsset
with
issuer: Party
symbol: Text
owner: Party
amount: Decimal
where
signatory issuer
Now a USDC token could be defined as the token created from a Daml template named USDCToken, where the issuer party is Circle and the symbol is “USDC”. To figure out how much USDC Alice holds on the Canton Network, she can query the ledger for all contracts created from a Daml template named USDCToken, where the issuer party is Circle, the symbol is “USDC” and the owner party is Alice.
The ERC20 standard postulates that a contract can optionally implement three methods: name, symbol and decimals. The name and the symbol methods are trivial. To replicate them in Daml we can simply include them as fields in a Daml template implementing an instrument holding token. The decimals method from ERC20 is used as a scaling factor between the amount of the asset and its representation as the account balance on an EVM chain. The latter, according to ERC20, must be an integer, as the account balance must be of the type uint256. To convert the account balance integer to the amount of the asset it represents, the account balance is divided by 10**decimals. For example, if a contract represents a money market fund (MMF), an account balance is 102 and decimals returns 2, this account balance represents 1.02 shares of the MMF. In Daml there’s no need to implement the decimals method, since Daml supports fixed precision numeric types and allows to directly define numeric amounts in terms of fixed precision decimals. The most commonly used numeric type in Daml is Decimal, which represents a fixed precision number with 10 decimal places.
Some ERC20 tokens add guards against overflow of numeric state variables. This is another thing you don’t need to worry about in Daml. Even if an overflow occurs, it will not corrupt the ledger. If an overflow occurs during Daml transaction interpretation, the transaction will be rejected. As a result, no change to the ledger state will occur.
So far, we have looked at the use of ERC20 to tokenize RWAs and discussed how to implement in a Daml template representing an instrument holding
- Mandatory instrument level info
- Include issuer party as a field
- Add issuer party as a contract signatory to ensure proper mint/burn authorization
- Optional instrument level info (name, symbol, decimals)
- Holding level info
- Include owner party as a field
- Include amount as a field
We also discussed that to implement the balanceOf and the totalSupply ERC20 functions we need to use query/read APIs.
In Part 2 of this series we’re going to look at the implementation of token transfers in Daml, completing the replication of functionality mandated by the ERC20 standard.