diff --git a/proposals/lip-0051.md b/proposals/lip-0051.md index 20ff8e51f..21ffc21e4 100644 --- a/proposals/lip-0051.md +++ b/proposals/lip-0051.md @@ -7,7 +7,7 @@ Discussions-To: https://research.lisk.com/t/define-state-and-state-transitions-o Status: Draft Type: Standards Track Created: 2021-05-21 -Updated: 2022-11-11 +Updated: 2022-12-19 ``` ## Abstract @@ -63,8 +63,6 @@ To allow cross-chain transfers of tokens, we define a specific command which mak 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. -The only exception is the case of LSK token, which can be transferred from sidechain A to sidechain B through the Lisk mainchain: in this case, a [cross-chain message][lip-0049] is sent from sidechain A to the Lisk mainchain; the mainchain then updates the escrowed amounts and forwards the cross-chain message to sidechain B. - ### Protocol Logic for Other Modules The Token module provides several exposed methods, which are designed to allow a wide range of use cases while avoiding unexpected behaviors (such as unwanted minting or unlocking of tokens). The functions below are the main exposed methods of the Token module. @@ -195,7 +193,8 @@ The following constants are used throughout the document: | `MINT_FAIL_TOKEN_NOT_INITIALIZED` | uint32 | 10 | Used when the mint function fails because the token to be minted is not initialized. | | `TOKEN_ID_NOT_AVAILABLE` | uint32 | 11 | Used when the specified token ID is not available. | | `TOKEN_ID_NOT_NATIVE` | uint32 | 12 | Used when the specified token ID is not native to the chain. | -| `INSUFFICIENT_ESCROW_BALANCE` | uint32 | 12 | Used when the escrowed account does not have sufficient balance. | +| `INSUFFICIENT_ESCROW_BALANCE` | uint32 | 13 | Used when the escrowed account does not have sufficient balance. | +| `INTEROPERABILITY_NOT_REGISTERED` | uint32 | 14 | Used when the a cross-chain operation is called without having registered an interoperability module. | ### Type Definition @@ -441,7 +440,7 @@ Here, the [initializeUserAccountInternal](#initializeuseraccountinternal) functi Transactions executing this command have: * `module = MODULE_NAME_TOKEN` -* `command = COMMAND_NAME_CROSS_CHAIN_TRANSFER` +* `command = COMMAND_NAME_CROSS_CHAIN_TRANSFER` ##### Parameters @@ -456,7 +455,8 @@ crossChainTransferParamsSchema = { "receivingChainID", "recipientAddress", "data", - "messageFee" + "messageFee", + "messageFeeTokenID" ], "properties": { "tokenID": { @@ -486,36 +486,40 @@ crossChainTransferParamsSchema = { "messageFee": { "dataType": "uint64", "fieldNumber": 6 + }, + "messageFeeTokenID": { + "dataType": "bytes", + "length": TOKEN_ID_LENGTH, + "fieldNumber": 7 } } } ``` + ##### Verification ```python def verify(trs: Transaction) -> None: senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] + amount = trs.params.amount receivingChainID = trs.params.receivingChainID tokenID = trs.params.tokenID - amount = trs.params.amount messageFee = trs.params.messageFee - data = trs.params.data + messageFeeTokenID = trs.params.messageFeeTokenID - # Transfer is only possible for tokens native to either sending or receiving chain - # and if there is a direct channel, in which case a forward message will be sent. + # Transfer is only possible for tokens native to either sending or receiving chain. tokenChainID = getChainID(tokenID) # The native chain of the token used for the transaction. - if tokenChainID not in [OWN_CHAIN_ID, receivingChainID, MAINCHAIN_ID]: - raise Exception("Token must be native to either the sending or the receiving chain or the mainchain.") + if tokenChainID not in [OWN_CHAIN_ID, receivingChainID]: + raise Exception("Token must be native to either the sending or the receiving chain.") + + if messageFeeTokenID != Interoperability.getMessageFeeTokenID(receivingChainID): + raise Exception("Invalid message fee Token ID.") balanceChecks: dict[TokenID, uint64] = {} - if tokenID in balanceChecks: - balanceChecks[tokenID] += amount - else: - balanceChecks[tokenID] = amount + balanceChecks[tokenID] = amount - messageFeeTokenID = Interoperability.getMessageFeeTokenID(receivingChainID) if messageFeeTokenID in balanceChecks: balanceChecks[messageFeeTokenID] += messageFee else: @@ -609,9 +613,12 @@ def verify(ccu: Transaction, ccm: CCM) -> None: tokenChainID = getChainID(tokenID) sendingChainID = ccm.sendingChainID amount = ccm.params.amount + + if ccm.status > MAX_RESERVED_ERROR_STATUS: + raise Exception("Invalid CCM status code") - if tokenChainID not in [OWN_CHAIN_ID, sendingChainID, MAINCHAIN_ID]: - raise Exception("Token must be native to either the sending or the receiving chain or the mainchain.") + if tokenChainID not in [sendingChainID, OWN_CHAIN_ID]: + raise Exception("Token must be native to either the sending or the receiving chain.") if tokenChainID == OWN_CHAIN_ID: if escrowStore(sendingChainID, tokenID) does not exist or escrowAmount(sendingChainID, tokenID) < amount: @@ -1205,7 +1212,7 @@ beforeCCCExecutionEventDataSchema = { #### beforeCCMForwarding -This event has `name = EVENT_NAME_BEFORE_CCM_FORWARDING`. This event is emitted during calls of [beforeCrossChainMessageForwarding](#beforecrosschainmessageforwarding) function. +This event has `name = EVENT_NAME_BEFORE_CCM_FORWARDING`. This event is emitted during calls of [beforeCrossChainMessageForwarding](#beforecrosschainmessageforwarding) function; since this function is defined only in the mainchain, this event is also defined only in mainchain. ##### Topics @@ -1468,13 +1475,16 @@ def transferCrossChainInternal( crossChainCommand=CROSS_CHAIN_COMMAND_NAME_TRANSFER, receivingChainID=receivingChainID, fee=messageFee, - params={ + params= encode( + schema = crossChainTransferMessageParamsSchema, + object = { "tokenID": tokenID, "amount": amount, "senderAddress": senderAddress, "recipientAddress": recipientAddress, "data": data } + ) ) ``` @@ -1842,6 +1852,10 @@ def transferCrossChain( emitFailedCrossChainTransferEvent(senderAddress, tokenID, amount, receivingChainID, recipientAddress, DATA_TOO_LONG) raise Exception("Data field too long.") + if Interoperability module is not registered: + emitFailedCrossChainTransferEvent(senderAddress, tokenID, amount, receivingChainID, recipientAddress, INTEROPERABILITY_NOT_REGISTERED) + raise Exception("Interoperability module is not registered.") + # Balance checks. balanceChecks: dict[TokenID, uint64] = {} balanceChecks[tokenID] = amount @@ -2196,7 +2210,7 @@ def supportTokenID(tokenID: TokenID) -> None: ) ``` -#### removeSupportTokenID +#### removeSupport This function is called to remove support from a specified token. In case all tokens are supported or all tokens of the token's native chain are supported, this function raises an exception. @@ -2206,8 +2220,8 @@ def removeSupport(tokenID: TokenID) -> None: if supportedTokens(ALL_SUPPORTED_TOKENS_KEY) exists: raise Exception('All tokens are supported.') - if tokenID = TOKEN_ID_LSK: - raise Exception('Can not remove support for LSK token.') + if tokenID = TOKEN_ID_LSK or chainID == OWN_CHAIN_ID: + raise Exception('Can not remove support for the specified token.') if supportedTokens(chainID) exists: if supportedTokens(chainID).supportedTokenIDs == []: # All tokens from this chain are supported. @@ -2490,7 +2504,7 @@ def beforeCrossChainCommandExecution(ccu: Transaction, ccm: CCM) -> None: "ccmID": Interoperability.encodeCCM(ccm), "messageFeeTokenID": messageFeeTokenID, "relayerAddress": relayerAddress, - "result": RESULT_INSUFFICIENT_BALANCE + "result": INSUFFICIENT_ESCROW_BALANCE }, topics=[relayerAddress, messageFeeTokenID] ) @@ -2505,50 +2519,36 @@ def beforeCrossChainCommandExecution(ccu: Transaction, ccm: CCM) -> None: #### beforeCrossChainMessageForwarding +This function is defined only in the Lisk mainchain. + ```python def beforeCrossChainMessageForwarding(ccu: Transaction, ccm: CCM) -> None: # This should never fail since it is checked in verifyCrossChainMessage. messageFeeTokenID = Interoperability.getMessageFeeTokenID(ccm.receivingChainID) # Always TOKEN_ID_LSK. if escrowAmount(ccm.sendingChainID, messageFeeTokenID) < ccm.fee: - emitFailedCCMForwardEvent(ccm, RESULT_INSUFFICIENT_BALANCE) - raise Exception("Insufficient balance in the sending chain for the message fee.") - - # Transfer the fee from escrow to escrow. messageFeeTokenID is always TOKEN_ID_LSK. - escrowAmount(ccm.sendingChainID, messageFeeTokenID) -= ccm.fee - escrowAmount(ccm.receivingChainID, messageFeeTokenID) += ccm.fee - - if ccm.module == MODULE_NAME_TOKEN and ccm.crossChainCommand == CROSS_CHAIN_COMMAND_NAME_TRANSFER and getChainID(ccm.params.tokenID) == OWN_CHAIN_ID: - if escrowAmount(ccm.sendingChainID, ccm.params.tokenID) < ccm.params.amount: - emitFailedCCMForwardEvent(ccm, INSUFFICIENT_ESCROW_BALANCE) - raise Exception("Insufficient balance in the sending chain for the transfer.") - - # Transfer the amount from escrow to escrow. - escrowAmount(ccm.sendingChainID, ccm.params.tokenID) -= ccm.params.amount - escrowAmount(ccm.receivingChainID, ccm.params.tokenID) += ccm.params.amount - - emitEvent( + emitPersistentEvent( module=MODULE_NAME_TOKEN, name=EVENT_NAME_BEFORE_CCM_FORWARDING, data={ "ccmID": Interoperability.encodeCCM(ccm), "messageFeeTokenID": messageFeeTokenID, - "result": RESULT_SUCCESSFUL + "result": INSUFFICIENT_ESCROW_BALANCE }, topics=[ccm.sendingChainID, ccm.receivingChainID] - ) + ) + raise Exception("Insufficient balance in the sending chain for the message fee.") -def emitFailedCCMForwardEvent( - ccm: CCM, - result: uint32 -) -> None: - messageFeeTokenID = Interoperability.getMessageFeeTokenID(ccm.receivingChainID) - emitPersistentEvent( + # Transfer the fee from escrow to escrow. messageFeeTokenID is always TOKEN_ID_LSK. + escrowAmount(ccm.sendingChainID, messageFeeTokenID) -= ccm.fee + escrowAmount(ccm.receivingChainID, messageFeeTokenID) += ccm.fee + + emitEvent( module=MODULE_NAME_TOKEN, name=EVENT_NAME_BEFORE_CCM_FORWARDING, data={ "ccmID": Interoperability.encodeCCM(ccm), "messageFeeTokenID": messageFeeTokenID, - "result": result + "result": RESULT_SUCCESSFUL }, topics=[ccm.sendingChainID, ccm.receivingChainID] )