A New Token Module

In this next edition of the ongoing series covering the Lisk interoperability solution, we discuss one of the key blockchain concepts: fungible tokens and their uses in the Lisk ecosystem. This topic was discussed in the presentation Lisk Interoperability: Token and NFT Standards given at Lisk.js.

By Maxime Gagnebin Ph.D.

31 May 2022

Fungible_Tokens_in_the_Lisk_Ecosystem_MAIN@2x.png

This blog post starts with a short introduction to the concept of fungible tokens. Following this, we describe the Token module proposed for the Lisk ecosystem. Here we cover the main features that are available in the Token module. This section should be your focus if you want an overview of this module without delving into details of developing a sidechain application. In the next section "Using the Exposed Functions", we outline how to use the Token module when creating your own module. If you are a sidechain developer this section will be of particular interest and will show you how easy it will be to interact with the Token module in your blockchain applications. Lastly, we conclude by presenting the topic for next month's blog post.

Fungible Token Definition

Fungible tokens (that will be referred to as  "tokens" in this blog post) are very common in the blockchain space. They are a class of digital assets that encompasses all cryptocurrencies such as Bitcoin, Ethereum, or LSK. The principal feature of fungible tokens is that multiple tokens of the same kind are indistinguishable, this makes them interchangeable, hence the name fungible. For example, any 1 LSK is treated the same as any other 1 LSK on the blockchain. This feature is akin to the fiat behavior where any 1 EUR coin can be replaced with another 1 EUR coin without consequences.

This analogy should make clear that the main usage of tokens is in situations where only the amounts of tokens are relevant. For example in a payment service, every LSK is accepted similarly, or for voting, all LSK have the same weight when counting the votes.

LiskResearch.jpg

The Token Module

In this section, we provide a brief overview of the features of the Token module presented in LIP 0051. The information below is not exhaustive, and we refer readers to the LIP for additional details. Several reasons motivated the introduction of a new Token module. Firstly, the introduction of the state root into the block headers requires every module to specify their state as a key-value store, the root of which is computed as a sparse Merkle tree. Secondly, as the ecosystem supports interoperability, new features are needed to allow cross-chain token transfers, and to guarantee that they remain secure. Lastly, but not least importantly, the new Token module exposes a set of functions to be used by sidechain developers to interact with the Token module in a safe and simple manner.

Token Identification

Once multiple types of tokens are available, an identification method is necessary. On the Ethereum blockchain, various ERC-20 tokens are identified by a 32-byte value. Contrastingly, in the Lisk ecosystem, tokens are identified by two numbers:  the chain ID where the token is minted and a local ID, chosen when the token is minted.

For example, the LSK token is native to the Lisk mainchain which has the chain ID 1, it is also the first (and only) token of this chain, and has the local ID 0. This entails that the LSK token ID is {"chainID": 1, "localID": 0}.

 Choosing the local ID of tokens when they are minted allows the native chain to specify and utilize multiple fungible tokens. For example, a chain could specify two tokens, a governance token with the local ID 0 and a utility token with the local ID 1. Note that in any case, the logic specific to using the tokens is not handled by the Token module, but must be implemented in separate modules.

State Store

All information regarding fungible tokens is stored as key-value pairs in the Token module store. The Token module store is separated into several substores serving different internal purposes. The only substore we will mention here is the user substore which contains the balances of all fungible tokens for every address. The substore uses the user address and the token ID as a store key, and the corresponding available balance and potential locked tokens as the store value.

figure_12x (1).png

Figure 1: The user substore uses the address concatenated with the token ID as the key and stores the corresponding available and locked tokens as the store value.

The main property of locked tokens is that they cannot be sent to another account. They are still associated with the user address, however, they must be unlocked before being used. We will see later that locking tokens can be very useful and allows multiple use cases.

Commands

The Token module only specifies the following two commands:

The token transfer command: This is used to transfer tokens from the available balance of one account to another account on the same chain. It serves the same function as the LSK transfer transaction currently used on the Lisk mainchain. The token cross-chain transfer command: This is used to transfer tokens from the available balance of one account to another account on a different chain. These two commands allow users to send tokens across the ecosystem without having to handle messages or understand the underlying interoperability protocol.

figure_22x (1).png

Figure 2: A small potential ecosystem with some of the allowed token transfers. Token transfers in the same chain can send any tokens in the module store. Cross-chain token transfers must be done with tokens native to either the sending or receiving chain (see next paragraph for more details).

Maintaining the Token Total Supply

As previously mentioned, the first part of the token ID is the chain ID of the native chain of the token (where the token is minted). A chain is then free to mint as many tokens as their protocol allows with the correct chain ID. However, a chain should not be able to mint tokens with the chain ID of other chains and flood the network with them. For example, only the Lisk mainchain will mint LSK tokens as part of the block generation, but a sidechain cannot mint LSK tokens and send them across the network.

One way of achieving this feature is to have each chain keep track of how many of its native tokens are in which chain in the Lisk ecosystem. This is easily done by escrowing any tokens sent to another chain. In essence, the Token store has an account for each other chain where tokens have been sent and those accounts store the amount of tokens that have been transferred to that chain. To be able to maintain the correctness of those escrow accounts, the native chain of a token must be aware of the token moving from chain to chain. To allow this, the Token module only allows tokens to be sent from their native chain to another chain, or from another chain back to the native chain.

figure_32x (1).png

Figure 3: Illustrating the rule above with the cross-chain transfer of tokens from the A-chain. The tokens can be sent back and forth from the A-chain to the B-chain (and also back and forth to the C-chain), but tokens from the A-chain cannot be sent from the B-chain to the C-chain.

Exposed Functions

The exposed functions are the entry gates to the Token module store and the way that other modules interact with the Token module

figure_42x (1).png

Figure 4: The exposed functions in the Token module are used by other modules to interact with the Token module store.

Let's give a few examples of the exposed functions, more can be found in the LIP-0051.

initializeToken is used to create a new type of token. mint is used to issue new tokens of a given type. burn is used to destroy tokens of a given type. lock is used to lock tokens, in essence moving them from the user's available balance to the locked tokens array. unlock is used to unlock previously locked tokens. transfer is used to move tokens from one store entry to another.

The exposed functions are designed to always maintain a valid Token module store, as they define allowed state transitions that can be induced by other modules. They further simplify the usage of the Token module by other modules, through providing simple interfaces that do not require knowledge of the Token module's inner workings. The next section describes more elaborate uses and shows the exposed functions in action.

Using the Exposed Functions

This section will explore some of the possibilities that the Token module offers. We will outline three potential uses and describe how they could be created utilizing the exposed functions. You will see that the exposed functions allow for a wide range of applications to be developed. As a general rule, it is best to not modify the Token module, as modifying the Token module could have unintended consequences such as minting tokens native to another chain.

In the following examples, we will use the notation token.functionName(arguments) to denote a call to the function with the name functionName in the Token module.

Example 1: Voting and Unlocking

The first use case is actually present on the Lisk mainchain and is being used on a daily basis. Voting is an integral part of a DPoS blockchain and handling the voted tokens must be done with the Token module. When a vote is cast, the command locks the voted tokens and increases the total number of votes that the delegate received. The command must also unlock the tokens and decrease the total number of votes for the delegate when the vote is removed (for simplicity here there is no waiting period for unlocking, in contrast to the Lisk mainchain).

File name
1vote(voteAmount, delegate):
2    transactionSender = address sending this transaction
3    delegate.totalVotesReceived += voteAmount
4
5    if voteAmount > 0:
6        token.lock(address = transactionSender, 
7                   lockingModule = MODULE_ID_DPOS,
8                   tokenID = TOKEN_ID_DPOS,
9                   amount = voteAmount)
10    else:
11        token.unlock(address = transactionSender, 
12                     lockingModule = MODULE_ID_DPOS,
13                     tokenID = TOKEN_ID_DPOS,
14                     amount = - voteAmount)

Example 2: HTLC Module

The previous example was a module that was used locally on a chain. However, there is no reason that this must be the case. An HTLC module is a classic example of a module that benefits from being supported on different chains in order to permit safe token swaps. The basic paradigm of an HTLC is to allow users to lock their tokens and only allow their unlocking under one of the following conditions:

The unlock action happens before the set exit time is done by the targeted user, and by providing the correct secret code. The unlock action happens after the set exit time and is done by the user who created the HTLC. Hence, an HTLC module only requires two commands: enterHTLC and claimHTLC. Both of them make use of the token module to lock and unlock the tokens.

The first command enterHTLC will lock the tokens in the sender account and will create a lock0bject in the HTLC store.

File name
1enterHTLC(partnerAddress, amount, tokenID, hashedSecret, exitTime):
2
3    token.lock(address = address sending the transaction,
4               amount = amount, 
5               tokenID = tokenID, 
6               moduleID = MODULE_ID_HTLC)
7
8    lockObject = {"creator": address of transaction sender,
9                  "partner": partnerAddress,
10                  "amount": amount,
11                  "tokenID": tokenID,
12                  "hashedSecret": hashedSecret,
13                  "exitTime": exitTime}
14
15    add lockObject to HTLC store

The second command, claimHTLC, implements the necessary logic to unlock the tokens and transfer them to the correct account.

File name
1
2claimHTLC(secret, lockObject):
3    if no HTLC store entry correspond to lockObject:
4        fail
5
6    transactionSender = address sending the transaction
7
8    if hash(secret) == lockObject.hashedSecret
9        and timestamp < lockObject.exitTime
10        and transactionSender == lockObject.partner:
11
12        token.unlock(address = lockObject.creator,
13                     moduleID = MODULE_ID_HTLC,
14                     tokenID = lockObject.tokenID
15                     amount = lockObject.amount)
16
17        token.transfer(senderAddress = lockObject.creator, 
18                       recipientAddress = transactionSender,
19                       tokenID = lockObject.tokenID
20                       amount = lockObject.amount)
21
22        erase lockObject from the store
23
24    if timestamp > lockObject.exitTime
25        and transactionSender == lockObject.creator:
26
27        token.unlock(address = lockObject.creator,
28                     moduleID = MODULE_ID_HTLC,
29                     tokenID = lockObject.tokenID
30                     amount = lockObject.amount)
31
32        erase lockObject from the store

One of the main uses of this module would be to facilitate safe token swaps between users on different chains. The following process illustrates how the HTLC module with these two commands can be used for this purpose:

Two users, Alice and Bob, agree on a swap, say 10 A-token for 5 B-token. Alice chooses a secret S and sends enterHTLC(user2, 10, A-token, hash(S), 100222) to the A-chain. This creates lock0bject1 in the HTLC state store in the A-chain. Bob sends enterHTLC(user1, 5, B-token, hash(S), 100111) to the B-chain. This creates lock0bject2 in the HTLC state store in the B-chain. Alice reveals S by sending claimHTLC(S, lockObject2) on the B-chain. Alice gets the 5 B-token. Bob sends claimHTLC(S, lockObject1) on the A-chain. Bob gets the 10 A-token.

Notice that in order for the swap to be secure (to guarantee that Alice does not end up with both 10 A-tokens and 5 B-tokens), Bob must choose an exit time sufficiently smaller than the one chosen by Alice.

Example 3: Loyalty Points

For this last example, we want to describe a chain on which each user can create their own token. They then have the freedom to give these tokens away as they please to reward other users. Such a chain could, for instance, be used by businesses to give loyalty points to customers. For this example, we will suppose that we have a loyalty module that stores all registered users (the ones allowed to give out points), as well as their associated token ID.

The first command that we need to define is a registration command that will check if the user is already registered, then assign the next available token ID to this user, and initialize this token ID so that new tokens can now be minted.

File name
1registerParticipant():
2    transactionSender = address sending the transaction
3    if transactionSender is already registered:
4        fail
5
6    localID = token.nextAvailableLocalID()
7    token.initializeToken(localID)
8
9    participant = {"address": transactionSender,
10                   "tokenID": {"chainID": 0, 
11                               "localID": localID}}
12
13    add participant to the loyalty module store

Secondly, we define a command that is used to give away points. Any address that has registered as a participant, can give out their respective loyalty tokens.

File name
1giveLoyaltyPoint(recipientAddress, amountLoyalty):
2    transactionSender = address sending this transaction
3
4    let participant be the object in the loyalty store 
5        with address == transactionSender
6
7    if there is no such participant:
8        fail
9
10    token.mint(address = recipientAddress, 
11               tokenID = shopLoyalty.tokenID,
12               amount = amountLoyalty)

Thirdly, we define a command used to redeem the loyalty tokens. Here the used tokens are burned and a corresponding action is taken. The way the points are used could be defined in another command from the same module, or they could be used in another module.

File name
1useLoyaltyPoint(amountLoyalty, tokenIDLoyalty):
2    transactionSender = address sending this transaction
3
4    token.burn(address = transactionSender, 
5               tokenID = tokenIDLoyalty,
6               amount = amountLoyalty)
7
8    do something corresponding to using the loyalty points

The variations on this theme are numerous and cover a wide range of use cases, from friendship points to store vouchers or even DEX like applications.

Conclusion and Next Topic

This blog post has presented the Token module. The Lisk ecosystem will contain two modules responsible for token management, the Token module responsible for fungible tokens, and the NFT module which will be responsible for non-fungible tokens. The NFT module and its potential uses will be presented in the next blog post of this series.

If you wish to dive further into the technical specifications or ask questions about the Token module, we invite you to visit our Research forum, particularly the "Define state and state transitions of Token module" thread.