Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update token module: Remove direct lsk transfers, add messageFeeTokenID to CC Transfer #192

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
102 changes: 51 additions & 51 deletions proposals/lip-0051.md
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -456,7 +455,8 @@ crossChainTransferParamsSchema = {
"receivingChainID",
"recipientAddress",
"data",
"messageFee"
"messageFee",
"messageFeeTokenID"
],
"properties": {
"tokenID": {
Expand Down Expand Up @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still add the check about the Interoperability module once we are at it

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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}
)
)
```

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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.
Expand Down Expand Up @@ -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]
)
Expand All @@ -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]
)
Expand Down