Introduce an interoperable token module

Hello,

In this thread, I want to propose a LIP for the roadmap objective Introduce token standards. The proposal’s contribution is to define a fungible token module to be used in the Lisk ecosystem.

I’m looking forward to your feedback.

Here is a complete LIP draft:

LIP:
Title: Introduce an interoperable token module
Author: Maxime Gagnebin <maxime.gagnebin@lightcurve.io>
Type: Standards Track
Created: <YYYY-MM-DD>
Updated: <YYYY-MM-DD>

Abstract

This LIP introduces a token module to be used in the Lisk ecosystem for minting, burning and transferring tokens. This module allows any chain in the ecosystem to handle and transfer tokens in a coherent, secure and controlled manner. In this LIP, the tokens handled are fungible.

Copyright

This LIP is licensed under the Creative Commons Zero 1.0 Universal.

Motivation

The token module is composed of a state store definition used to store tokens in the state. To modify this store, we propose two transactions: a token transfer transaction and a cross-chain token transfer transaction; as well as multiple reducers to be used by other modules.

Interactions between custom modules and the token module should only happen through the specified reducers. Introducing those reducers allows sidechain developers to create custom modules and custom behavior without needing to ensure and test that all rules of the token module are followed.

With the proposed interoperability solution for the Lisk ecosystem, we anticipate that multiple chains will create and distribute custom tokens. Those tokens can be used for a wide variety of reasons which are the choice of the sidechain developper.

Rationale

Technical Glossary

  • Native chain: With regards to a token, this is the chain where the token was minted.
  • Native tokens: With regards to a chain, all tokens minted on this chain.
  • Foreign chain: With regards to a token, all chains other than the native chain.

Token Identification and Interoperability

To identify tokens in the Lisk ecosystem, we introduce token identifiers in this proposal.
An identifier will be unique among all tokens in the ecosystem. It is built from the chain ID of the chain minting the token and a local identifier, an integer chosen when the token is minted. The local identifier allows chains to define multiple custom tokens,
each identified by their respective local ID. For example, a decentralized exchange could have a governance token (distributed in the genesis block) and a liquidity token (distributed to liquidity providers).

In particular, the LSK token is native to the lisk-mainchain which has chainID = 1, it is also the first (and only) token of this chain and has localID = 0. This entails that the LSK token ID is (1,0).

Supported Tokens

All chains are allowed to select which tokens their protocol supports. Supporting a token only implies that users of the chain can hold those tokens and handle them as specified in this LIP.
It does not mean that the chain implements custom logic associated with those tokens.

The choice of supported tokens must abide by two rules: all chains must support their native tokens and all chains must support the LSK token. The supported tokens can be specified in a config file that is fixed at the chain creation. For example:

  • A decentralized exchange could support all tokens.
  • A chain with a specific use case and no native token could only support the LSK token.
  • A chain with a specific use case and with a native token might only support the LSK token and their native token.
  • A gambling chain might support their native token, the LSK token and tokens from a selected group of oracle chains.

When receiving unsupported tokens from a cross-chain transfer, chains should return those tokens to the sending chain if the message fee was sufficient. The threshold on the message fee to return unsupported tokens is chosen to be the same as the interoperability threshold for returning CCMs for other errors. This threshold is set to be equal to the Lisk mainchain minimum fee.

Lastly, note that modifying the list of supported tokens would result in a fork of the chain. For this reason, the default behavior for Lisk sidechains would be to support all tokens.

Cross-chain Token Transfer

To allow cross-chain transfers of tokens, we define a specific transaction which makes use of the interoperability module and creates a cross-chain message with the relevant information. When sending cross-chain tokens, it is crucial that every chain can correctly maintain escrow amounts of its native tokens across the ecosystem. In this way, the total supply of a token can never be increased by a foreign chain as the native chain only accepts as many tokens from a foreign chain as have been sent to it before.

Transfer To and From the Native Chain

These specifications only allow tokens to be transferred to and from their native chain. In particular, this means that a token minted on chain A cannot be transferred directly from chain B to chain C. This is required to allow the native chain to maintain correct escrowed amounts. The alternative would be to allow such transfer and require an additional message to be sent to the native chain to acknowledge the transfer. However the correctness of the escrowed amounts would rely on the processing of this additional information. Network delays could mean that this is only processed much later and that in the meantime users have been tricked into accepting tokens not backed by escrow.

Reducers

Reducers are the exposed methods of the different modules. For the token module the reducers are designed to allow a wide range of use cases while avoiding unexpected behaviors such as unwanted minting or unlocking of tokens.

mint

This reducer allows a chain to mint a specified amount of native tokens. Applying the reducer will increase the balance by the specified amount in the specified user store and at the same time, increase the corresponding total token supply.

burn

This reducer allows a chain to destroy a specified amount of native tokens. When burning tokens, the reducer will remove the specified amount of tokens from the user store and at the same time decrease the total supply corresponding to the token.

transfer

This reducer allows a chain to transfer tokens. When transferring tokens, the reducer will remove the tokens from the sender and add them to the recipient.

transferCrossChain

This reducer is used if a custom module needs to send tokens to another chain. It ensures that all amounts are correctly validated and that tokens are escrowed if necessary.

escrow

This reducer allows to transfer tokens from a user store entry to an escrow store entry. This should be done when native tokens are sent to another chain.

unescrow

This reducer allows to transfer tokens from an escrow store entry to a user store entry. This should be done when native tokens are returned from another chain.

transferEscrow

This reducer allows to transfer tokens from an escrow store entry to another escrow store entry.
This is done when native tokens returning from another chain are directly sent to a third chain.

lock

This reducer is used to lock tokens held by a user. Locking tokens is done “module wise”, i.e., when locking tokens a moduleID has to be specified. This allows locked tokens to be managed more securely. For example, if a token is locked in a DPoS module, then there is no risk that a bug in a custom HTLC module would unlock those tokens.

unlock

This reducer is used to unlock tokens previously locked. As for locking, the corresponding module ID needs to be specified in order to unlock the correct tokens. Notice that there is no protocol rule restricting different modules from unlocking tokens locked with a given moduleID, it is a protection allowing well written code to be more secure.

handleMessageFee

This reducer is needed to handle the message fee when sending cross-chain messages across the ecosystem. It is called by the send reducer of the interoperability module, and should not be called by any other module.

Use of Reducers by Other Modules

As of writing this proposal, other modules exist in the Lisk protocol that make use of tokens. Those uses should be updated to use the reducers defined in this proposal. This guarantees that those modules will not trigger potentially improper state changes. For example:

  • The DPoS module should use the lock and unlock reducers to lock voted tokens and to unlock them when the unlock transaction is included in the blockchain.
  • The framework should use the mint reducer when assigning the block reward.
  • The fee handling should use the transfer reducer to transfer the fee from the transaction sender to the block forger and, on the Lisk mainchain, the burn reducer to burn the minimum part of the fee.

Specification

Constants and Notations

The following constants are used throughout the document

Name Type Value
Interoperability Constants
MODULE_ID_INTEROPERABILITY uint32 64
MAINCHAIN_ID uint32 1
MIN_RETURN_FEE uint64 1000
Token Module Constants
MODULE_ID_TOKEN uint32 TBD
ASSET_ID_TRANSFER uint32 0
ASSET_ID_CROSS_CHAIN_TRANSFER uint32 1
ASSET_ID_CCM_CROSS_CHAIN_TRANSFER uint32 2
CCM_STATUS_TOKEN_NOT_SUPPORTED uint32 5
CCM_STATUS_ESCROW_VIOLATION uint32 6
CCM_STATUS_MIN_BALANCE_NOT_REACHED uint32 7
MIN_BALANCE uint64 50000000
LSK_LOCAL_ID uint32 0
Token Store Constants
STORE_PREFIX_USER bytes 0x00 00
STORE_PREFIX_SUPPLY bytes 0x80 00
STORE_PREFIX_ESCROW bytes 0xc0 00
Escrow Status Constants
ESCROW_STATUS_ACTIVE uint32 0
ESCROW_STATUS_TERMINATED uint32 1

uint32be

uint32be(x) returns the big endian uint32 serialization of an integer x, with 0 <= x < 2^32. This serialization is always 4 bytes long.

ownChainID

In this LIP, ownChainID is the chain ID of the chain under consideration, it is stored in the “own chain account” entry of the interoperability store. This value only exists on chains having an initialized interoperability store.

Reducers from Other Modules

Calling a reducer reducer from the interoperability module is represented by interoperability.reducer(required inputs).

Token Identification

All tokens in the ecosystem are identified by a pair of non-negative integers(chainID, localID), both strictly less than 2^32 . The first element of the pair, chainID, is the chain ID of the chain that minted the token (an integer, as specified in the “Chain Registration” LIP) and the second element, localID, is an integer specified when the token is minted.

Token ID and Native Tokens

Tokens on their native chain are identified by the pair (0,localID). The same tokens in other chains would be identified by the pair (nativeChainID, localID).

The LSK token is identified by the pair (1, 0), i.e., chainID = MAINCHAIN_ID = 1 and localID = 0.

Supported Tokens

The token module contains a function tokenSupported(chainID, localID) function that returns true or false depending on the configuration of the token module. This function is used when receiving cross-chain messages to assert the support for non-native tokens.

  • tokenSupported(MAINCHAIN_ID, LSK_LOCAL_ID) = true. This corresponds to the token ID of the LSK token.
  • tokenSupported(MAINCHAIN_ID, localID) = false, for any localID != LSK_LOCAL_ID.

On the Lisk mainchain, the only supported token is the LSK token.

tokenSupported(chainID, localID) = true if and only if chainID = 1 and localID = 0.

Token Module Store

The token store is separated in three parts, the supply store, the escrow store and the user store.

When a transaction or reducer tries to send tokens to a store entry that does not exist, this entry is created. If this entry is in the escrow store the status of the new entry is initialized to ESCROW_STATUS_ACTIVE.

Supply Store

The token store contains an entry dedicated to storing information about the total supply of native tokens. The store contains a entries with:

  • storagePrefix = STORE_PREFIX_SUPPLY.
  • storageKey = uint32be(localID).
  • storageValue serialized using tokenSupplyStorageSchema.
tokenSupplyStorageSchema = {
    "type": "object",
    "properties": {
        "totalSupply": { 
            "dataType": "uint64",
            "fieldNumber": 1
        },
    },
    "required": ["totalSupply"]
}

Escrow Store

The token store contains an entry dedicated to storing information about native tokens which have been sent to another chain. The state contains an entry with:

  • storagePrefix = STORE_PREFIX_ESCROW.
  • storageKey = uint32be(chainID).
  • storageValue serialized using escrowStorageSchema.
escrowStorageSchema = {
    "type": "object",
    "properties": {
        "status": {
            "dataType": "uint32", 
            "fieldNumber": 1
        },
        "escrowedTokens":{
            "type": "array",
            "fieldNumber": 2,
            "items":{
                "type": "object",
                "properties":{
                    "localID": {
                        "dataType": "bytes", 
                        "fieldNumber": 1
                    },
                    "amount" : {
                        "dataType": "uint64",
                        "fieldNumber": 2
                    },
                },
                "required": ["localID", "amount"]
            }
        }
    }
    "required":[ "status", "escrowedTokens"]
}

In the above object, escrowedTokens is always kept ordered by ascending order of localID. This guarantees that serialization is done consistently across nodes maintaining the chain.

The escrowedTokens array contains only elements with non-zero amounts. If any state transition would reduce the amount property of an element to zero, this element is removed from the array.

When after any state transition, the escrowedTokens array in an escrow store entry is empty, and its status is ESCROW_STATUS_ACTIVE, the entry is removed.

Chain Statuses

The escrow store entries contain a status property. This property can take two values ESCROW_STATUS_ACTIVE and ESCROW_STATUS_TERMINATED. Newly created entries in the escrow store have their status initialized to ESCROW_STATUS_ACTIVE.

User Store

The token store contains entries dedicated to storing the balances of users for a given address and(chainID,localID) pair. The store contains entries with:

  • storagePrefix = STORE_PREFIX_USER
  • storageKey = address || uint32be(chainID) || uint32be(localID)
  • storageValue serialized using userStorageSchema.
userStorageSchema = {
    "type": "object",
    "properties": {
        "availableBalance": {
            "dataType": "uint64",
            "fieldNumber": 1
        },
        "lockedBalances":{ 
            "type": "array",
            "fieldNumber": 2, 
            "items": {
                "type": "object",
                "properties":{
                    "moduleID":{"dataType":"uint32", "fieldNumber": 1},
                    "amount"  :{"dataType": "uint64","fieldNumber": 2},
                },
                "required":[ "moduleID", "amount" ]
            }
        }
    }
    "required":["availableBalance", "lockedBalances"]
}

In the above object, lockedBalances is always kept ordered by ascending order of moduleID. This guarantees that serialization is done consistently across nodes maintaining the chain.

The lockedBalances array contains only elements with non-zero amounts. If any state transition would reduce the amount property of an element to zero, this element is removed from the array.

When, after any state transition, all amounts in a user store entry (available and locked) are zero the state entry is removed.

Store Notation

For the rest of this proposal:

  • Let userStore(address, chainID, localID) be the token store entry with storagePrefix = STORE_PREFIX_USER and storageKey = address || uint32be(chainID) || uint32be(localID).
    • Let availableBalance(address, chainID, localID) be the availableBalance property of userStore(address, chainID, localID).
    • Let lockedAmount(address, moduleID, chainID, localID) be the amount corresponding to the given moduleID in the lockedBalances array of userStore(address, chainID, localID).
  • Let escrowStore(chainID) be the token store entry with storagePrefix = STORE_PREFIX_ESCROW and storageKey = uint32be(chainID).
    • Let escrowAmount(chainID, localID) be the amount corresponding to the given localID in the escrowedTokens array of escrowStore(chainID).
    • Let escrowStatus(chainID) be the status property of escrowStore(chainID).
  • Let supplyStore(localID) be the token store entry with storagePrefix = STORE_PREFIX_SUPPLY and storageKey = uint32be(localID).
    • Let totalSupply(localID) be the totalSupply property stored in supplyStore(localID).

Transactions

The module provides the following transactions to modify token entries.

Token Transfer

This transaction has:

  • moduleID = MODULE_ID_TOKEN
  • assetID = ASSET_ID_TRANSFER
Asset Schema

The asset property of token transfer transactions follows the schema tokenTransferAsset.

tokenTransferAsset = {
    "type": "object",
    "properties":{
        "tokenChainID": {
            "dataType": "uint32",
            "fieldNumber": 1
        },
        "tokenLocalID": {
            "dataType": "uint32",
            "fieldNumber": 2
        },
        "amount": {
            "dataType": "uint64",
            "fieldNumber": 3 
        },
        "recipientAddress": {
            "dataType": "bytes",
            "fieldNumber": 4 
        },
        "data": {
            "dataType": "string",
            "fieldNumber": 5 
        },
    },
    "required": [ 
        "chainID", 
        "localID", 
        "amount" , 
        "recipientAddress", 
        "data" 
    ]
}
Asset Validity

The asset is valid if

  • recipientAddress is a byte array of length 20.
  • data has length less than or equal to 64.
  • its application does not fail.
Transaction Application

When applying a token transfer transaction trs, the logic below is followed:

derive senderAddress from trs.senderPublicKey
let tokenChainID, tokenLocalID, recipientAddress, amount given by trs.asset

if availableBalance(senderAddress, tokenChainID, tokenLocalID) < amount:
    transaction application fails 

availableBalance(senderAddress, tokenChainID, tokenLocalID) -= amount 
availableBalance(recipientAddress, tokenChainID, tokenLocalID) += amount

Cross-chain Token Transfer

This transaction has:

  • moduleID = MODULE_ID_TOKEN
  • assetID = ASSET_ID_CROSS_CHAIN_TRANSFER
Asset Schema

The asset property of cross-chain token transfer transactions follows the schema crossChainTransferAsset.

crossChainTransferAsset = {
    "type": "object",
    "properties": {
        "tokenChainID": {
            "dataType": "uint32",
            "fieldNumber": 1
        },
        "tokenLocalID": {
            "dataType": "uint32",
            "fieldNumber": 2
        },
        "amount": {
            "dataType": "uint64",
            "fieldNumber": 3 
        },
        "receivingChainID": {
            "dataType": "uint32",
            "fieldNumber": 4 
        },
        "recipientAddress": {
            "dataType": "bytes",
            "fieldNumber": 5 
        },
        "data": {
            "dataType": "string",
            "fieldNumber": 6 
        },
        "messageFee": {
            "dataType": "uint64",
            "fieldNumber": 7 
        }
    },
    "required": [ 
        "tokenChainID", 
        "tokenLocalID",
        "amount", 
        "receivingChainID", 
        "recipientAddress", 
        "data", 
        "messageFee" 
    ]
}
Asset Validity

The asset is valid if

  • recipientAddress is a byte array of length 20.
  • data has length less than or equal to 64.
  • tokenChainID is either 0, MAINCHAIN_ID or receivingChainID.
  • its application does not fail.
Transaction Application

When applying a cross-chain token transfer transaction trs, the following is done:

  • Derive senderAddress from trs.senderPublicKey.

  • Apply the same logic as the reducer

    transferCrossChain(senderAddress, 
                       trs.asset.receivingChainID, 
                       trs.asset.recipientAddress, 
                       trs.asset.tokenChainID, 
                       trs.asset.tokenLocalID, 
                       trs.asset.amount, 
                       trs.asset.messageFee,
                       trs.asset.data).
    
    

Applying Cross-chain Messages

Applying Cross-chain Token Transfer Messages

CCM Asset

The asset property of cross-chain token transfer messages follows the schema crossChainTransferMessageAsset.

crossChainTransferMessageAsset = {
    "type": "object",
    "properties":{
        "tokenChainID": {
            "dataType": "uint32",
            "fieldNumber": 1
        },
        "tokenLocalID": {
            "dataType": "uint32",
            "fieldNumber": 2
        },
        "amount": {
            "dataType": "uint64",
            "fieldNumber": 3
        },
        "senderAddress": {
            "dataType": "bytes",
            "fieldNumber": 5 
        },
        "recipientAddress": {
            "dataType": "bytes",
            "fieldNumber": 5 
        },
        "data": {
            "dataType": "string",
            "fieldNumber": 6 
        }
    },
    "required": [
        "tokenChainID", 
        "tokenLocalID", 
        "amount" ,   
        "senderAddress", 
        "recipientAddress", 
        "data" 
    ]
}

When applying a cross-chain message CCM with

  • moduleID = MODULE_ID_TOKEN
  • assetID = ASSET_ID_CCM_CROSS_CHAIN_TRANSFER

the logic below is followed.

chainID          = CCM.asset.tokenChainID
localID          = CCM.asset.tokenLocalID 
amount           = CCM.asset.amount
recipientAddress = CCM.asset.recipientAddress
senderAddress    = CCM.asset.senderAddress
sendingChainID   = CCM.sendingChainID

// token should only be sent to and from their native chains 
if chainID not in [ownChainID, CCM.sendingChainID, MAINCHAIN_ID]:
    set escrowStatus(CCM.sendingChainID) = ESCROW_STATUS_TERMINATED
    if CCM.status == CCM_STATUS_OK 
    and CCM.fee >= MIN_RETURN_FEE*size(CCM):
        interoperability.error(CCM, CCM_STATUS_ESCROW_VIOLATION)
        exit CCM application

if chainID == ownChainID:
    if escrowAmount(sendingChainID, localID) < asset.amount :
        set escrowStatus(CCM.sendingChainID) = ESCROW_STATUS_TERMINATED
        if CCM.status == CCM_STATUS_OK 
        and CCM.fee >= MIN_RETURN_FEE*size(CCM):
            CCM.asset.amount = 0
            interoperability.error(CCM, CCM_STATUS_ESCROW_VIOLATION)
            exit CCM application

    escrowAmount(sendingChainID, localID) -= amount

if CCM.status == 0:
    availableBalance(recipientAddress, 0, localID) += amount
else:
    availableBalance(senderAddress, 0, localID) += amount

if chainID != ownChainID and CCM.status == 0:
    // return any non-supported tokens with enough fee
    if tokenSupported(chainID, localID) == false:
        if CCM.fee >= MIN_RETURN_FEE*size(CCM) :
            interoperability.error(CCM, CCM_STATUS_TOKEN_NOT_SUPPORTED)
        else:
            exit CCM application

    availableBalance(recipientAddress, chainID, localID) += amount
else:
    exit CCM application

Tokens and Genesis Blocks

The genesis block of a chain can have a non-empty token store. The distribution of tokens at genesis is left to sidechain developers and must follow the conditions below:

  • No entries with prefix key STORE_PREFIX_ESCROW should exist in the genesis block.
  • Only tokens with chainID == 0 must exist in the genesis block. They can be part of the available balance or part of the locked balances.
  • For all localID, the sum of all corresponding amounts (available or locked) over all existing user store entries must equal totalSupply(localID).

Mainchain Minimum Balance Specifications

As specified in LIP 0025, mainchain user store entries cannot hold less than MIN_BALANCE of LSK token. To follow this rule:

  • Applying transactions that would result in an address address with availableBalance(address, 0, LSK_LOCAL_ID) < MIN_BALANCE is invalid.
  • Cross-chain messages that would result in an address address with availableBalance(address, 0, LSK_LOCAL_ID) < MIN_BALANCE after their application must be rejected. This is done by calling interoperability.error(CCM, CCM_STATUS_MIN_BALANCE_NOT_REACHED) on the rejected CCM.

Reducers

The token module provides the following reducers to modify the token state. Any other modules should use those reducers to modify the token state. The token state should never be modified from outside the module without using a reducer as this could result in unexpected behavior and could cause an improper state transition.

getAvailableBalance

getAvailableBalance(address, chainID, localID):
    return availableBalance(address, chainID, localID)

getLockedAmount

getLockedAmount(address, moduleID, chainID, localID):
    return lockedAmount(address, moduleID, chainID, localID)

getEscrowAmount

getEscrowAmount(chainID, localID):
    return escrowAmount(chainID, localID)

getEscrowStatus

getEscrowStatus(chainID):
    return escrowStatus(chainID)

initializeLocalID

initializeLocalID(localID):
    if supplyStore(localID) exists:
        reducer fails
    else:
        create a token store entry with
        storagePrefix = STORE_PREFIX_SUPPLY
        storageKey = uint32be(localID)
        storageValue = {totalsupply: 0} serialized using escrowStorageSchema 

mint

mint(address, localID, amount): 
    if amount < 0:
        reducer fails
    if supplyStore(localID) does not exist:
        reducer fails

    // this reducer is only used to mint native tokens
    // the identifier of the minted token is (0,localID)
    availableBalance(address, 0, localID) += amount
    totalSupply(localID) += amount

burn

burn(address, localID, amount): 
    if amount < 0:
        reducer fails
    if availableBalance(address, 0, localID) < amount:
        reducer fails

    availableBalance(address, 0, localID) -= amount
    totalSupply(localID) -= amount

transfer

transfer(senderAddress, recipientAddress, chainID, localID, amount): 
    if amount < 0:
        reducer fails
    if availableBalance(senderAddress, chainID, localID)< amount:
        reducer fails

    availableBalance(senderAddress, chainID, localID) -= amount 
    availableBalance(recipientAddress, chainID, localID) += amount 

transferCrossChain

transferCrossChain(senderAddress, 
                   receivingChainID, 
                   recipientAddress, 
                   chainID, 
                   localID, 
                   amount, 
                   messageFee, 
                   data): 

    if amount < 0:
        reducer fails
    if chainID not in [0, MAINCHAIN_ID, receivingChainID]:
        reducer fails
    if escrowStatus(receivingChainID) == ESCROW_STATUS_TERMINATED
    and (chainID == 0 or chainID == MAINCHAIN_ID):
        reducer fails
    if length(data) > 64:
        reducer fails

    if ownChainID == MAINCHAIN_ID:
        if availableBalance(senderAddress, 0, LSK_LOCAL_ID) < messageFee:
            reducer fails
        availableBalance(senderAddress, 0, LSK_LOCAL_ID) -= messageFee 
        escrowAmount(receivingChainID, LSK_LOCAL_ID)    += messageFee
    else:
        if availableBalance(senderAddress, MAINCHAIN_ID, LSK_LOCAL_ID) < messageFee:
            reducer fails
        availableBalance(senderAddress, MAINCHAIN_ID, LSK_LOCAL_ID) -= messageFee

    if availableBalance(senderAddress, chainID, localID) < amount:
        reducer fails
    availableBalance(senderAddress, chainID, localID) -= amount

    if chainID == 0:  
        escrowAmount(receivingChainID, localID) += amount
        newChainID = ownChainID
    else:
        newChainID = chainID

    timestamp = timestamp of the block including the execution of this logic

    serializedAsset = serialization of messageAsset below 
                      following crossChainTransferMessageAsset

    messageAsset: {  
        "tokenChainID": newChainID,
        "tokenLocalID": localID,
        "amount": amount,
        "senderAddress": senderAddress,
        "recipientAddress": recipientAddress,
        "data": data
    }

    call interoperability.send(timestamp,
                               MODULE_ID_TOKEN,
                               ASSET_ID_CCM_CROSS_CHAIN_TRANSFER,
                               receivingChainID,
                               messageFee,
                               senderAddress,
                               serializedAsset)

escrow

escrow(escrowChainID, address, localID, amount): 
    if amount < 0:
        reducer fails
    if availableBalance(address, 0, localID)< amount:
        reducer fails
    if escrowStatus(toChainID) == ESCROW_STATUS_TERMINATED:
        reducer fails

    availableBalance(address, 0, localID) -= amount 
    escrowAmount(escrowChainID, localID) += amount 

unescrow

unescrow(escrowChainID, address, localID, amount): 
    if amount < 0:
        reducer fails
    if escrowAmount(escrowChainID, localID)< amount:
        reducer fails

    availableBalance(address, 0, localID) += amount
    escrowAmount(escrowChainID, localID) -= amount 

transferEscrow

transferEscrow(fromChainID, toChainID, localID, amount): 
    if amount < 0:
        reducer fails
    if escrowAmount(fromChainID, localID)< amount:
        reducer fails
    if escrowStatus(toChainID) == ESCROW_STATUS_TERMINATED:
        reducer fails

    escrowAmount(fromChainID, localID) -= amount
    escrowAmount(toChainID, localID) += amount

lock

lock(address, moduleID, chainID, localID, amount): 
    if amount < 0:
        reducer fails
    if availableBalance(address, chainID, localID) < amount:
    reducer fails
    availableBalance(address, chainID, localID) -= amount
    lockedAmount(address,moduleID, chainID, localID) += amount 

unlock

unlock(address, moduleID, chainID, localID, amount):
    if amount < 0:
        reducer fails
    if lockedAmount(address, moduleID, chainID, localID) < amount:
        reducer fails
    availableBalance(address, chainID, localID) += amount
    lockedAmount(address, moduleID, chainID, localID) -= amount

payMessageFee

payMessageFee(address, amount): 
    if amount < 0:
        reducer fails
    availableBalance(address, MAINCHAIN_ID, LSK_LOCAL_ID) += amount

handleMessageFee

handleMessageFee(address, receivingChainID, amount):
    if amount < 0:
        reducer fails
        
    // if this chain is the mainchain, escrow LSK
    if ownChainID == MAINCHAIN_ID:
        if availableBalance(senderAddress, 0, LSK_LOCAL_ID) < amount:
            reducer fails
        availableBalance(senderAddress, 0, LSK_LOCAL_ID) -= amount 
        escrowAmount(receivingChainID, LSK_LOCAL_ID)    += amount

    // else burn LSK
    else:
        if availableBalance(senderAddress, MAINCHAIN_ID, LSK_LOCAL_ID) < amount:
            reducer fails
        availableBalance(senderAddress, MAINCHAIN_ID, LSK_LOCAL_ID) -= amount)
    

Backwards Compatibility

This introduces a different token handling mechanism for the whole Lisk ecosystem which requires a hard fork.

1 Like