blockchain technology

Tokenization of RWAs on Canton Network vs EVM chains - Part 2

author by Alex Putkov May 8, 2025

Share this article

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.


 

The token transfer functionality in ERC20 is facilitated by transfer and transferFrom functions. The implementation of the transfer function can be trivial.

function transfer (address to, uint256 amount) public returns (bool) {
      balances[msg.sender] -= amount;
      balances[to] += amount;
      emit Transfer(msg.sender, to amount);
      return true;

}

There’s no need to check transfer authorization, as the function definition in ERC20 specifies that the transfer is always made from the caller’s account. Except for the purpose of providing a better error message, there’s no need to explicitly check whether the source account has sufficient funds for the transfer. If it doesn’t, the function will throw an exception because it will try to set the uint256 value in the balances mapping variable out of range. However, this trivial implementation is not satisfactory for tokens representing RWAs because it doesn’t impose any restrictions on who can own the token. Any party that owns the token can transfer it to any address without performing any know-your-customer (KYC) verification on the transfer recipient. For TradFi players that have an obligation to comply with anti-money laundering (AML) and counter terrorism financing (CTF) regulations, such implementation is insufficient.

Some tokens, for example Ondo OUSG representing Ondo Short Term US Government Bond Fund, rely on separate implementation of KYC capabilities and add a call in the body of the transfer function to check whether the sender and the recipient addresses have been KYC’d.

Others, like Franklin OnChain U.S. Government Money Fund (BENJI), only allow the admin to execute the transfer function, effectively disabling the capability to use a wallet to perform token transfers and making the admin an exclusive transfer agent.

Daml is a full fledged programming language, which means there are numerous ways to implement a token. For tokenization of RWAs we recommend the approach that involves an asset registry, which also functions as the transfer agent. This approach is in line with the TradFi workflows. It allows satisfying the KYC compliance requirements by easily integrating with existing off-ledger KYC systems and processes. It also allows composing simple transfers into more complex Delivery vs. Payment (DvP) workflows.

The simplest implementation of an asset registry in Daml starts with a trivial template for a token that we discussed in Part 1 of this series.

template Token
  with
   issuer: Party
   symbol: Text
   owner: Decimal
    amount: Decimal
  where
   signatory issuer
   observer owner
   ensure amount>0

Compared to the MyAsset template from Part 1 of this series, this Token template adds the observer keyword to the template and the owner party as an observer on the Token contract. Daml ledger model limits the visibility of the contracts on the ledger to the stakeholders on the contract. The stakeholders are the union of signatories and observers. The collective authorization from all the signatories is required to create a contract and to archive it. The signatories are informed of everything that happens with the contract, for example they’re informed when someone retrieves contract data from the ledger. The observers don’t need to authorize contract creation or archival. They’re only informed of ledger actions that create and archive the contract. But they do have visibility of the contract on the ledger, in other words the observers can fetch the contract from the ledger. Parties other than the stakeholders don’t have contract visibility. This is the core feature of Daml and Canton that provides programmable privacy for on-ledger data and distributes transaction data on a strict need-to-know basis. In case of the Token template we add the owner as an observer to allow the token owner to view their holdings.
We also used the ensure keyword, which allows us to specify restrictions on the contract. The keyword is followed by a Boolean expression, which must evaluate to True or the action that attempts to create the contract will fail. The effect is the same as calling the require function in the constructor of a Solidity contract. In our example any transaction that attempts to create a Token contract with the amount <= 0 will be rejected. 

With the Token template the issuer party can mint and burn tokens by creating and archiving Token contracts. A token transfer means a change of ownership of a token from one party to another. Since Daml contracts are immutable, a transfer always involves archiving an existing contract and creating a new one with the new owner in its stead. At this point in our implementation only the issuer party can perform token transfers, because the issuer's authority is required to create and archive Token contracts. How can we allow a token holder to perform a transfer? This is where the Daml Delegation Pattern comes in handy. The Delegation Pattern involves a role contract that allows a party to delegate its authority to another party for a specific set of actions. The delegating party is a signatory on the role contract. The delegate party is the controller on a choice on the role contract. A choice in Daml is like an external function in a Solidity contract, except that a Daml choice takes as one of the parameters a list of parties known as controllers, and the Daml authorization model requires collective authorization from all controllers to execute (in Daml terminology “to exercise”) the choice. All actions within the body of a choice are collectively authorized by all the choice controllers and all the signatories on the contract. Therefore, a role contract delegates the authority of the contract signatories to perform a set of actions specified in the body of a choice to the choice controllers. Only the authority of choice controllers is required to exercise the choice, while any action within the choice carries authorization by contract signatories as well as choice controllers. This delegation of authority from contract signatories to choice controllers is limited. It only allows choice controllers to execute the specific list of actions encoded in the body of the choice, nothing else.

Following the Delegation Pattern we can create a role contract with the token issuer as signatory, which would allow token owner to exercise a choice that will archive a token contract owned by the choice controller and create a new token contract in its stead with the new owner. In other words, this role contract will allow a token owner to transfer the token to another party.

template AssetRegistry
  with
   admin: Party
  where
   signatory admin

   nonconsuming choice Execute_Transfer: ContractId Token
     with      
        sender: Party
       tokenToTransfer: ContractId Token
       recipient: Party
   controller sender
   do
       tokenData <- fetch tokenToTransfer
       assertMsg “Transfer sender is not the owner of the token” $    sender == tokenData.owner
       archive tokenToTransfer
       create tokenData with owner = recipient

By default Daml choices are consuming, which means the contract the choice is exercised on is automatically archived (consumed) once the choice is exercised. The keyword nonconsuming in front of the choice declaration defines the choice named Execute_Transfer as nonconsuming, meaning the AssetRegistry contract remains active after the choice is exercised. This is necessary to allow the same AssetRegistry contract to be used for numerous transfers. The return type of the choice is ContractId Token.
The choice takes three arguments: the sender and the recipient of the type Party and tokenToTransfer of the type ContractId Token. A Party type represents the on-ledger identity of an entity acting on the ledger. It is similar to the address of an external account on EVM chains. A ContractId is the type that represents a unique identifier for a Daml contract on the ledger. It is similar to the address of a contract account on EVM chains with one caveat. Since contracts on EVM chains are mutable and Daml contracts are not, the address of a contract account on EVM chains tends to be more durable than a Daml ContractId. The latter changes every time contract mutation is simulated by archiving the existing contract and creating a new one in its stead. In Daml, ContractIds are subtyped by the template the contract is created from. ContractId Token is the type for ContractIds for contracts created from the template named Token.

The sender party is declared as the sole controller on the Execute_Transfer choice, meaning only the authority of the sender party is required to exercise the choice. In other words, in this model the sender can unilaterally execute the transfer.

The body of the choice in the do block performs four actions on the ledger. First it fetches the contract data for the Token contract provided as argument to the choice using the fetch function and the token ContractId. This action verifies that the contract provided as input to the choice is active. In other words, it verifies that the asset the sender intends to transfer exists. Then the assertMsg function is called to ensure that the value of the owner field in the Token contract is the same party as the sender. In other words, this action verifies that the sender owns the asset being transferred. The assertMsg function is similar to the require function in Solidity. Then the archive function is called to burn the token provided to the choice as an argument. And finally, the create function mints a new Token contract, that is identical to the burned contract, except that the new Token contract will have a new ContractId and the value of the owner field will be set to the recipient party.

Since the authority of the issuer is required to create or archive a Token contract, a token transfer by exercising a choice on the AssetRegistry contract will only succeed if the issuer party on the Token contract and the admin party on the AssetRegistry contract are the same.

So far our implementation doesn’t perform any KYC checks. How can we satisfy the KYC compliance requirements? The recommended way is to issue on-ledger credentials that attest that the party holding the credential has been KYC’d.

template KYCCredential
  with
   issuer: Party
   holder: Party
  where
   signatory issuer
   observer holder

Now the Execute_Transfer choice on the AssetRegistry template can be enhanced to include the verification that the recipient party holds the credential that permits the recipient to hold the token being transferred.



   nonconsuming choice Execute_Transfer: ContractId Token
     with      

        sender: Party
       tokenToTransfer: ContractId Token
       recipient:ContractId KYCCredential
   controller sender
   do
       recipientData <- fetch recipient
       tokenData <- fetch tokenToTransfer
       assertMsg “Transfer sender is not the owner of the token” $    sender == tokenData.owner
       archive tokenToTransfer
       create tokenData with owner = recipientData.holder

The recipient argument of the Execute_Transfer choice now takes the ContractId of a KYCCredential contract instead of the recipient party. To exercise the choice one must provide the ContractId of a KYCCredential contract for the recipient. The validity of the KYCCredential contract is verified in the body of the choice by fetching the contract from the ledger. The value of the recipient party, which is required to create the Token contract with the recipient party as the owner, is taken from the holder field of the KYCCredential contract data.

Looking at the AssetRegistry template you may notice that the only stakeholder on the contract is the admin party. This means the visibility of the contract is limited to the AssetRegistry admin. How is it possible then for the transfer sender to exercise a choice on the AssetRegistry contract, if the sender party can’t view the contract? Shouldn’t the sender party be an observer  on the AssetRegistry contract? Well, adding every asset holder party as an observer on a contract would be unwieldy. Too hard to maintain, not to mention the potential for unbounded growth in the size of the contract, as more and more asset holders are added. Instead, we can use a feature known as explicit contract disclosure. It allows contract stakeholders to share visibility of the contract with non-stakeholders using off-ledger distribution. A contract stakeholder can create a DisclosedContract message and share it with non-stakeholders off-ledger, for example through a web service. A non-stakeholder can include this message in the command submitted to the ledger, which allows the command to access the disclosed contract on-ledger.
In our model the AssetRegistry is not the only contract that needs to be disclosed. Daml authorization model requires the party submitting a transaction to have visibility of all contracts utilized in the transaction. Since the Execute_Transfer choice fetches the KYCCredential held by the transfer recipient, the transfer sender party, which executes the transfer by submitting the command to exercise Execute_Transfer choice, must have visibility of the KYCCredential contract. In this model the recipient of the transfer needs to create the DisclosedContract for the KYCCredential contract and share it with the transfer sender. And the sender needs to include the DisclosedContract with the command submission to the ledger. Note that the model does not require disclosing any sensitive information such as asset ownership records. Neither AssetRegistry nor KYCCredential contracts contain any confidential information. Conceptually this is akin to how in a traditional bank transfer the sender needs to include the recipient’s account number with the transfer request, and the recipient needs to share their bank account number with the sender.

You can find complete source code for this model in a Github repo The source code in the repo includes test scripts to demonstrate the model’s working. It also includes the capability to use multiple UTXO contracts as the source funds for the transfer as well as to send multiple transfers at once.

The implementation of a token we introduced in this article is purposely simplistic. It doesn’t provide enough flexibility to be the standard for real-life use cases. In the next article we’re going to look at the Canton Token Standard, which is the actual standard for token implementation on Canton Network.