From b29724a61b1bd0e88b17ce10a0b878518207807e Mon Sep 17 00:00:00 2001 From: Incede <33103370+Incede@users.noreply.github.com> Date: Thu, 6 Oct 2022 01:42:01 +0200 Subject: [PATCH 1/5] Update token cross chain methods --- framework/src/modules/token/cc_method.ts | 371 +++++++++++++++-------- framework/src/modules/token/constants.ts | 3 + 2 files changed, 241 insertions(+), 133 deletions(-) diff --git a/framework/src/modules/token/cc_method.ts b/framework/src/modules/token/cc_method.ts index 4f9e7d6d51e..a69d3a7f19e 100644 --- a/framework/src/modules/token/cc_method.ts +++ b/framework/src/modules/token/cc_method.ts @@ -22,182 +22,287 @@ import { } from '../interoperability/types'; import { NamedRegistry } from '../named_registry'; import { TokenMethod } from './method'; -import { ADDRESS_LENGTH, CHAIN_ID_LENGTH } from './constants'; +import { + ADDRESS_LENGTH, + CHAIN_ID_LENGTH, + CROSS_CHAIN_COMMAND_NAME_TRANSFER, + TokenEventResult, + TOKEN_ID_LENGTH, +} from './constants'; -import { UserStoreData, userStoreSchema } from './schemas'; +import { + CCForwardMessageParams, + crossChainForwardMessageParams, + UserStoreData, + userStoreSchema, +} from './schemas'; import { EscrowStore } from './stores/escrow'; import { UserStore } from './stores/user'; import { InteroperabilityMethod } from './types'; +import { BeforeCCCExecutionEvent } from './events/before_ccc_execution'; +import { RecoverEvent } from './events/recover'; +import { EMPTY_BYTES } from '../interoperability/constants'; +import { BeforeCCMForwardingEvent } from './events/before_ccm_forwarding'; +import { MODULE_NAME_TOKEN } from '../interoperability/cc_methods'; + +const CHAIN_ID_ALIAS_NATIVE = Buffer.alloc(0); // To be removed export class TokenInteroperableMethod extends BaseInteroperableMethod { + private readonly _tokenMethod: TokenMethod; + private _interopMethod!: InteroperabilityMethod; - public constructor(stores: NamedRegistry, events: NamedRegistry, _tokenMethod: TokenMethod) { + public constructor(stores: NamedRegistry, events: NamedRegistry, tokenMethod: TokenMethod) { super(stores, events); + this._tokenMethod = tokenMethod; } public addDependencies(interoperabilityMethod: InteroperabilityMethod) { this._interopMethod = interoperabilityMethod; } - public async beforeApplyCCM(_ctx: BeforeApplyCCMsgMethodContext): Promise { - // const { ccm } = ctx; - // const methodContext = ctx.getMethodContext(); - // if (ccm.fee < BigInt(0)) { - // throw new Error('Fee must be greater or equal to zero.'); - // } - // const { id: ownChainID } = await this._interopMethod.getOwnChainAccount(methodContext); - // const { messageFeeTokenID } = await this._interopMethod.getChannel( - // methodContext, - // ccm.sendingChainID, - // ); - // const [feeTokenChainID, feeTokenLocalID] = splitTokenID(messageFeeTokenID); - // const userStore = this.stores.get(UserStore); - // if (!feeTokenChainID.equals(ownChainID)) { - // await userStore.addAvailableBalanceWithCreate( - // methodContext, - // ctx.trsSender, - // messageFeeTokenID, - // ccm.fee, - // ); - // return; - // } - // const escrowStore = this.stores.get(EscrowStore); - // await escrowStore.deductEscrowAmountWithTerminate( - // methodContext, - // this._interopMethod, - // ccm.sendingChainID, - // feeTokenLocalID, - // ccm.fee, - // ); - // const canonicalTokenID = await this._tokenMethod.getCanonicalTokenID( - // methodContext, - // messageFeeTokenID, - // ); - // await userStore.addAvailableBalanceWithCreate( - // methodContext, - // ctx.trsSender, - // canonicalTokenID, - // ccm.fee, - // ); + public async beforeCrossChainCommandExecution(ctx: BeforeApplyCCMsgMethodContext): Promise { + const { trsSender, ccm } = ctx; + const methodContext = ctx.getMethodContext(); + const relayerAddress = trsSender; + const { id: ownChainID } = await this._interopMethod.getOwnChainAccount(methodContext); + const { messageFeeTokenID } = await this._interopMethod.getChannel( + methodContext, + ccm.sendingChainID, + ); + const { chainID: feeTokenChainID, localID: feeTokenLocalID } = messageFeeTokenID; + const userStore = this.stores.get(UserStore); + + if (feeTokenChainID.equals(ownChainID)) { + const escrowedAmount = await this._tokenMethod.getEscrowedAmount( + methodContext, + ccm.sendingChainID, + feeTokenLocalID, + ); + if (escrowedAmount < ccm.fee) { + this.events.get(BeforeCCCExecutionEvent).error( + methodContext, + { + sendingChainID: ccm.sendingChainID, + receivingChainID: ccm.receivingChainID, + messageFeeTokenID: feeTokenLocalID, + messageFee: ccm.fee, + relayerAddress, + }, + TokenEventResult.FAIL_INSUFFICIENT_BALANCE, + ); + + throw new Error('Insufficient balance in the sending chain for the message fee.'); + } + + const escrowStore = this.stores.get(EscrowStore); + await escrowStore.deductEscrowAmountWithTerminate( + methodContext, + this._interopMethod, + ccm.sendingChainID, + feeTokenLocalID, + ccm.fee, + ); + } + + await userStore.addAvailableBalanceWithCreate( + methodContext, + ctx.trsSender, + feeTokenLocalID, + ccm.fee, + ); + + this.events.get(BeforeCCCExecutionEvent).log(methodContext, { + sendingChainID: ccm.sendingChainID, + receivingChainID: ccm.receivingChainID, + messageFeeTokenID: feeTokenLocalID, + messageFee: ccm.fee, + relayerAddress, + }); } - public async beforeRecoverCCM(_ctx: BeforeRecoverCCMsgMethodContext): Promise { - // const { ccm } = ctx; - // const methodContext = ctx.getMethodContext(); - // if (ccm.fee < BigInt(0)) { - // throw new Error('Fee must be greater or equal to zero.'); - // } - // const { id: ownChainID } = await this._interopMethod.getOwnChainAccount(methodContext); - // const { messageFeeTokenID } = await this._interopMethod.getChannel( - // methodContext, - // ccm.sendingChainID, - // ); - // const [feeTokenChainID, feeTokenLocalID] = splitTokenID(messageFeeTokenID); - // const userStore = this.stores.get(UserStore); - // if (!feeTokenChainID.equals(ownChainID)) { - // await userStore.addAvailableBalanceWithCreate( - // methodContext, - // ctx.trsSender, - // messageFeeTokenID, - // ccm.fee, - // ); - // return; - // } - // const escrowStore = this.stores.get(EscrowStore); - // await escrowStore.deductEscrowAmountWithTerminate( - // methodContext, - // this._interopMethod, - // ccm.sendingChainID, - // feeTokenLocalID, - // ccm.fee, - // ); - // const canonicalTokenID = await this._tokenMethod.getCanonicalTokenID( - // methodContext, - // messageFeeTokenID, - // ); - // await userStore.addAvailableBalanceWithCreate( - // methodContext, - // ctx.trsSender, - // canonicalTokenID, - // ccm.fee, - // ); + public async beforeCrossChainMessageForwarding( + ctx: BeforeRecoverCCMsgMethodContext, + ): Promise { + const { ccm } = ctx; + const methodContext = ctx.getMethodContext(); + const { id: ownChainID } = await this._interopMethod.getOwnChainAccount(methodContext); + const { messageFeeTokenID } = await this._interopMethod.getChannel( + methodContext, + ccm.sendingChainID, + ); + const { chainID: feeTokenChainID, localID: feeTokenLocalID } = messageFeeTokenID; + + const escrowedAmount = await this._tokenMethod.getEscrowedAmount( + methodContext, + ccm.sendingChainID, + feeTokenLocalID, + ); + if (escrowedAmount < ccm.fee) { + this.events.get(BeforeCCMForwardingEvent).error( + methodContext, + { + sendingChainID: ccm.sendingChainID, + receivingChainID: ccm.receivingChainID, + messageFeeTokenID: feeTokenLocalID, + messageFee: ccm.fee, + }, + TokenEventResult.FAIL_INSUFFICIENT_BALANCE, + ); + + throw new Error('Insufficient balance in the sending chain for the message fee.'); + } + + const escrowStore = this.stores.get(EscrowStore); + await escrowStore.deductEscrowAmountWithTerminate( + methodContext, + this._interopMethod, + ccm.sendingChainID, + feeTokenLocalID, + ccm.fee, + ); + await escrowStore.addAmount(methodContext, ccm.receivingChainID, feeTokenLocalID, ccm.fee); + + const decodedParams = codec.decode( + crossChainForwardMessageParams, + ccm.params, + ); + + if ( + ccm.module === MODULE_NAME_TOKEN && + ccm.crossChainCommand === CROSS_CHAIN_COMMAND_NAME_TRANSFER && + feeTokenChainID === ownChainID + ) { + if (escrowedAmount < decodedParams.amount) { + this.events.get(BeforeCCMForwardingEvent).error( + methodContext, + { + sendingChainID: ccm.sendingChainID, + receivingChainID: ccm.receivingChainID, + messageFeeTokenID: feeTokenLocalID, + messageFee: ccm.fee, + }, + TokenEventResult.INSUFFICIENT_ESCROW_BALANCE, + ); + + throw new Error('Insufficient balance in the sending chain for the transfer.'); + } + + await escrowStore.deductEscrowAmountWithTerminate( + methodContext, + this._interopMethod, + ccm.sendingChainID, + feeTokenLocalID, + decodedParams.amount, + ); + await escrowStore.addAmount( + methodContext, + ccm.receivingChainID, + feeTokenLocalID, + decodedParams.amount, + ); + + this.events.get(BeforeCCMForwardingEvent).log(methodContext, { + sendingChainID: ccm.sendingChainID, + receivingChainID: ccm.receivingChainID, + messageFeeTokenID: feeTokenLocalID, + messageFee: ccm.fee, + }); + } } - public async beforeSendCCM(_ctx: BeforeSendCCMsgMethodContext): Promise { - // const { ccm } = ctx; - // const methodContext = ctx.getMethodContext(); - // if (ccm.fee < BigInt(0)) { - // throw new Error('Fee must be greater or equal to zero.'); - // } - // const { id: ownChainID } = await this._interopMethod.getOwnChainAccount(methodContext); - // const { messageFeeTokenID } = await this._interopMethod.getChannel( - // methodContext, - // ccm.sendingChainID, - // ); - // const [feeTokenChainID] = splitTokenID(messageFeeTokenID); - // const userStore = this.stores.get(UserStore); - // let tokenID = messageFeeTokenID; - // if (feeTokenChainID.equals(ownChainID)) { - // tokenID = await this._tokenMethod.getCanonicalTokenID(methodContext, messageFeeTokenID); - // const escrowStore = this.stores.get(EscrowStore); - // await escrowStore.addAmount(methodContext, ccm.receivingChainID, messageFeeTokenID, ccm.fee); - // } - // const payer = await userStore.get(ctx, userStore.getKey(ctx.feeAddress, tokenID)); - // if (payer.availableBalance < ccm.fee) { - // throw new Error( - // `Payer ${ctx.feeAddress.toString( - // 'hex', - // )} does not have sufficient balance for fee ${ccm.fee.toString()}`, - // ); - // } - // payer.availableBalance -= ccm.fee; - // await userStore.save(ctx, ctx.feeAddress, tokenID, payer); + public async verifyCrossChainMessage(ctx: BeforeSendCCMsgMethodContext): Promise { + const { ccm } = ctx; + const methodContext = ctx.getMethodContext(); + if (ccm.fee < BigInt(0)) { + throw new Error('Fee must be greater or equal to zero.'); + } + const { id: ownChainID } = await this._interopMethod.getOwnChainAccount(methodContext); + const { messageFeeTokenID } = await this._interopMethod.getChannel( + methodContext, + ccm.sendingChainID, + ); + const { chainID: feeTokenChainID, localID: feeTokenLocalID } = messageFeeTokenID; + if (feeTokenChainID.equals(ownChainID)) { + const escrowedAmount = await this._tokenMethod.getEscrowedAmount( + methodContext, + ccm.sendingChainID, + feeTokenLocalID, + ); + if (escrowedAmount < ccm.fee) { + throw new Error('Insufficient escrow amount.'); + } + } } public async recover(ctx: RecoverCCMsgMethodContext): Promise { const methodContext = ctx.getMethodContext(); const userStore = this.stores.get(UserStore); - if (!ctx.storePrefix.equals(userStore.subStorePrefix)) { - throw new Error(`Invalid store prefix ${ctx.storePrefix.toString('hex')} to recover.`); + const address = ctx.storeKey.slice(0, ADDRESS_LENGTH); + let account: UserStoreData; + let decodingFailed = false; + try { + account = codec.decode(userStoreSchema, ctx.storeValue); + } catch (error) { + decodingFailed = true; } - if (ctx.storeKey.length !== 28) { - throw new Error(`Invalid store key ${ctx.storeKey.toString('hex')} to recover.`); + if ( + !ctx.storePrefix.equals(userStore.subStorePrefix) || + ctx.storeKey.length !== ADDRESS_LENGTH + TOKEN_ID_LENGTH || + decodingFailed + ) { + this.events + .get(RecoverEvent) + .error( + methodContext, + address, + { terminatedChainID: ctx.terminatedChainID, tokenID: EMPTY_BYTES, amount: BigInt(0) }, + TokenEventResult.RECOVER_FAIL_INVALID_INPUTS, + ); + + throw new Error('Invalid arguments.'); } - const account = codec.decode(userStoreSchema, ctx.storeValue); - const address = ctx.storeKey.slice(0, ADDRESS_LENGTH); + const chainID = ctx.storeKey.slice(ADDRESS_LENGTH, ADDRESS_LENGTH + CHAIN_ID_LENGTH); + const tokenID = ctx.storeKey.slice(ADDRESS_LENGTH, ADDRESS_LENGTH + TOKEN_ID_LENGTH); const localID = ctx.storeKey.slice(ADDRESS_LENGTH + CHAIN_ID_LENGTH); const totalAmount = - account.availableBalance + - account.lockedBalances.reduce((prev, curr) => prev + curr.amount, BigInt(0)); + account!.availableBalance + + account!.lockedBalances.reduce((prev, curr) => prev + curr.amount, BigInt(0)); const { id: ownChainID } = await this._interopMethod.getOwnChainAccount(methodContext); - - if (!ownChainID.equals(chainID)) { - throw new Error( - `ChainID ${chainID.toString('hex')} does not match with own chain ID ${ownChainID.toString( - 'hex', - )}`, - ); - } const escrowStore = this.stores.get(EscrowStore); const escrowKey = Buffer.concat([ctx.terminatedChainID, localID]); const escrowData = await escrowStore.get(ctx, escrowKey); - if (escrowData.amount < totalAmount) { - throw new Error( - `Escrow amount ${escrowData.amount.toString()} is not sufficient for ${totalAmount.toString()}`, - ); + + if (!ownChainID.equals(chainID) || escrowData.amount < totalAmount) { + this.events + .get(RecoverEvent) + .error( + methodContext, + address, + { terminatedChainID: ctx.terminatedChainID, tokenID, amount: totalAmount }, + TokenEventResult.RECOVER_FAIL_INSUFFICIENT_ESCROW, + ); + + throw new Error('Insufficient escrow amount.'); } + escrowData.amount -= totalAmount; await escrowStore.set(ctx, escrowKey, escrowData); - const localTokenID = Buffer.concat([Buffer.alloc(0), localID]); + const localTokenID = Buffer.concat([CHAIN_ID_ALIAS_NATIVE, localID]); await userStore.addAvailableBalanceWithCreate( methodContext, address, localTokenID, totalAmount, ); + + this.events.get(RecoverEvent).log(methodContext, address, { + terminatedChainID: ctx.terminatedChainID, + tokenID, + amount: totalAmount, + }); } } diff --git a/framework/src/modules/token/constants.ts b/framework/src/modules/token/constants.ts index 05489d8cffa..56c861f3ae6 100644 --- a/framework/src/modules/token/constants.ts +++ b/framework/src/modules/token/constants.ts @@ -57,10 +57,13 @@ export const enum TokenEventResult { DATA_TOO_LONG = 4, ESCROW_NOT_INITIALIZED = 5, INVALID_TOKEN_ID = 6, + RECOVER_FAIL_INVALID_INPUTS = 9, + RECOVER_FAIL_INSUFFICIENT_ESCROW = 10, MINT_FAIL_NON_NATIVE_TOKEN = 11, MINT_FAIL_TOTAL_SUPPLY_TOO_BIG = 12, MINT_FAIL_TOKEN_NOT_INITIALIZED = 13, MAX_AVAILABLE_ID_REACHED = 14, + INSUFFICIENT_ESCROW_BALANCE = 15, } export type TokenErrorEventResult = Exclude; From 34d61dd8c366091687def994c1cdfa5a91c3856d Mon Sep 17 00:00:00 2001 From: Incede <33103370+Incede@users.noreply.github.com> Date: Thu, 6 Oct 2022 18:58:27 +0200 Subject: [PATCH 2/5] Update per feedback --- framework/src/modules/token/cc_method.ts | 164 ++++++++++------------- framework/src/modules/token/module.ts | 3 +- 2 files changed, 71 insertions(+), 96 deletions(-) diff --git a/framework/src/modules/token/cc_method.ts b/framework/src/modules/token/cc_method.ts index a69d3a7f19e..96151b0926e 100644 --- a/framework/src/modules/token/cc_method.ts +++ b/framework/src/modules/token/cc_method.ts @@ -13,6 +13,7 @@ */ import { codec } from '@liskhq/lisk-codec'; +import * as crypto from '@liskhq/lisk-cryptography'; import { BaseInteroperableMethod } from '../interoperability/base_interoperable_method'; import { BeforeApplyCCMsgMethodContext, @@ -20,8 +21,6 @@ import { BeforeSendCCMsgMethodContext, RecoverCCMsgMethodContext, } from '../interoperability/types'; -import { NamedRegistry } from '../named_registry'; -import { TokenMethod } from './method'; import { ADDRESS_LENGTH, CHAIN_ID_LENGTH, @@ -44,17 +43,15 @@ import { RecoverEvent } from './events/recover'; import { EMPTY_BYTES } from '../interoperability/constants'; import { BeforeCCMForwardingEvent } from './events/before_ccm_forwarding'; import { MODULE_NAME_TOKEN } from '../interoperability/cc_methods'; - -const CHAIN_ID_ALIAS_NATIVE = Buffer.alloc(0); // To be removed +import { splitTokenID } from './utils'; export class TokenInteroperableMethod extends BaseInteroperableMethod { - private readonly _tokenMethod: TokenMethod; + private _ownChainID!: Buffer; private _interopMethod!: InteroperabilityMethod; - public constructor(stores: NamedRegistry, events: NamedRegistry, tokenMethod: TokenMethod) { - super(stores, events); - this._tokenMethod = tokenMethod; + public init(ownChainID: Buffer): void { + this._ownChainID = ownChainID; } public addDependencies(interoperabilityMethod: InteroperabilityMethod) { @@ -64,28 +61,26 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { public async beforeCrossChainCommandExecution(ctx: BeforeApplyCCMsgMethodContext): Promise { const { trsSender, ccm } = ctx; const methodContext = ctx.getMethodContext(); - const relayerAddress = trsSender; - const { id: ownChainID } = await this._interopMethod.getOwnChainAccount(methodContext); - const { messageFeeTokenID } = await this._interopMethod.getChannel( + const relayerAddress = crypto.address.getAddressFromPublicKey(trsSender); + const tokenID = await this._interopMethod.getMessageFeeTokenID( methodContext, ccm.sendingChainID, ); - const { chainID: feeTokenChainID, localID: feeTokenLocalID } = messageFeeTokenID; + const [chainID] = splitTokenID(tokenID); const userStore = this.stores.get(UserStore); - if (feeTokenChainID.equals(ownChainID)) { - const escrowedAmount = await this._tokenMethod.getEscrowedAmount( - methodContext, - ccm.sendingChainID, - feeTokenLocalID, - ); - if (escrowedAmount < ccm.fee) { + if (chainID.equals(this._ownChainID)) { + const escrowStore = this.stores.get(EscrowStore); + const escrowKey = escrowStore.getKey(ccm.sendingChainID, tokenID); + const escrowAccount = await escrowStore.get(methodContext, escrowKey); + + if (escrowAccount.amount < ccm.fee) { this.events.get(BeforeCCCExecutionEvent).error( methodContext, { sendingChainID: ccm.sendingChainID, receivingChainID: ccm.receivingChainID, - messageFeeTokenID: feeTokenLocalID, + messageFeeTokenID: tokenID, messageFee: ccm.fee, relayerAddress, }, @@ -95,27 +90,16 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { throw new Error('Insufficient balance in the sending chain for the message fee.'); } - const escrowStore = this.stores.get(EscrowStore); - await escrowStore.deductEscrowAmountWithTerminate( - methodContext, - this._interopMethod, - ccm.sendingChainID, - feeTokenLocalID, - ccm.fee, - ); + escrowAccount.amount -= ccm.fee; + await escrowStore.set(methodContext, escrowKey, escrowAccount); } - await userStore.addAvailableBalanceWithCreate( - methodContext, - ctx.trsSender, - feeTokenLocalID, - ccm.fee, - ); + await userStore.addAvailableBalanceWithCreate(methodContext, relayerAddress, tokenID, ccm.fee); this.events.get(BeforeCCCExecutionEvent).log(methodContext, { sendingChainID: ccm.sendingChainID, receivingChainID: ccm.receivingChainID, - messageFeeTokenID: feeTokenLocalID, + messageFeeTokenID: tokenID, messageFee: ccm.fee, relayerAddress, }); @@ -126,25 +110,22 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { ): Promise { const { ccm } = ctx; const methodContext = ctx.getMethodContext(); - const { id: ownChainID } = await this._interopMethod.getOwnChainAccount(methodContext); - const { messageFeeTokenID } = await this._interopMethod.getChannel( + const tokenID = await this._interopMethod.getMessageFeeTokenID( methodContext, ccm.sendingChainID, ); - const { chainID: feeTokenChainID, localID: feeTokenLocalID } = messageFeeTokenID; + const [chainID] = splitTokenID(tokenID); - const escrowedAmount = await this._tokenMethod.getEscrowedAmount( - methodContext, - ccm.sendingChainID, - feeTokenLocalID, - ); - if (escrowedAmount < ccm.fee) { + const escrowStore = this.stores.get(EscrowStore); + const escrowKey = escrowStore.getKey(ccm.sendingChainID, tokenID); + const escrowAccount = await escrowStore.get(methodContext, escrowKey); + if (escrowAccount.amount < ccm.fee) { this.events.get(BeforeCCMForwardingEvent).error( methodContext, { sendingChainID: ccm.sendingChainID, receivingChainID: ccm.receivingChainID, - messageFeeTokenID: feeTokenLocalID, + messageFeeTokenID: tokenID, messageFee: ccm.fee, }, TokenEventResult.FAIL_INSUFFICIENT_BALANCE, @@ -153,15 +134,10 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { throw new Error('Insufficient balance in the sending chain for the message fee.'); } - const escrowStore = this.stores.get(EscrowStore); - await escrowStore.deductEscrowAmountWithTerminate( - methodContext, - this._interopMethod, - ccm.sendingChainID, - feeTokenLocalID, - ccm.fee, - ); - await escrowStore.addAmount(methodContext, ccm.receivingChainID, feeTokenLocalID, ccm.fee); + escrowAccount.amount -= ccm.fee; + await escrowStore.set(methodContext, escrowKey, escrowAccount); + + await escrowStore.addAmount(methodContext, ccm.receivingChainID, tokenID, ccm.fee); const decodedParams = codec.decode( crossChainForwardMessageParams, @@ -171,15 +147,15 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { if ( ccm.module === MODULE_NAME_TOKEN && ccm.crossChainCommand === CROSS_CHAIN_COMMAND_NAME_TRANSFER && - feeTokenChainID === ownChainID + chainID === this._ownChainID ) { - if (escrowedAmount < decodedParams.amount) { + if (escrowAccount.amount < decodedParams.amount) { this.events.get(BeforeCCMForwardingEvent).error( methodContext, { sendingChainID: ccm.sendingChainID, receivingChainID: ccm.receivingChainID, - messageFeeTokenID: feeTokenLocalID, + messageFeeTokenID: tokenID, messageFee: ccm.fee, }, TokenEventResult.INSUFFICIENT_ESCROW_BALANCE, @@ -188,24 +164,21 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { throw new Error('Insufficient balance in the sending chain for the transfer.'); } - await escrowStore.deductEscrowAmountWithTerminate( - methodContext, - this._interopMethod, - ccm.sendingChainID, - feeTokenLocalID, - decodedParams.amount, - ); + const updatedEscrowAccount = await escrowStore.get(methodContext, escrowKey); + updatedEscrowAccount.amount -= decodedParams.amount; + await escrowStore.set(methodContext, escrowKey, updatedEscrowAccount); + await escrowStore.addAmount( methodContext, ccm.receivingChainID, - feeTokenLocalID, + tokenID, decodedParams.amount, ); this.events.get(BeforeCCMForwardingEvent).log(methodContext, { sendingChainID: ccm.sendingChainID, receivingChainID: ccm.receivingChainID, - messageFeeTokenID: feeTokenLocalID, + messageFeeTokenID: tokenID, messageFee: ccm.fee, }); } @@ -217,19 +190,19 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { if (ccm.fee < BigInt(0)) { throw new Error('Fee must be greater or equal to zero.'); } - const { id: ownChainID } = await this._interopMethod.getOwnChainAccount(methodContext); - const { messageFeeTokenID } = await this._interopMethod.getChannel( + const tokenID = await this._interopMethod.getMessageFeeTokenID( methodContext, ccm.sendingChainID, ); - const { chainID: feeTokenChainID, localID: feeTokenLocalID } = messageFeeTokenID; - if (feeTokenChainID.equals(ownChainID)) { - const escrowedAmount = await this._tokenMethod.getEscrowedAmount( + const [chainID] = splitTokenID(tokenID); + if (chainID.equals(this._ownChainID)) { + const escrowStore = this.stores.get(EscrowStore); + const escrowAccount = await escrowStore.get( methodContext, - ccm.sendingChainID, - feeTokenLocalID, + escrowStore.getKey(ccm.sendingChainID, tokenID), ); - if (escrowedAmount < ccm.fee) { + + if (escrowAccount.amount < ccm.fee) { throw new Error('Insufficient escrow amount.'); } } @@ -240,16 +213,10 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { const userStore = this.stores.get(UserStore); const address = ctx.storeKey.slice(0, ADDRESS_LENGTH); let account: UserStoreData; - let decodingFailed = false; - try { - account = codec.decode(userStoreSchema, ctx.storeValue); - } catch (error) { - decodingFailed = true; - } + if ( !ctx.storePrefix.equals(userStore.subStorePrefix) || - ctx.storeKey.length !== ADDRESS_LENGTH + TOKEN_ID_LENGTH || - decodingFailed + ctx.storeKey.length !== ADDRESS_LENGTH + TOKEN_ID_LENGTH ) { this.events .get(RecoverEvent) @@ -263,19 +230,32 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { throw new Error('Invalid arguments.'); } + try { + account = codec.decode(userStoreSchema, ctx.storeValue); + } catch (error) { + this.events + .get(RecoverEvent) + .error( + methodContext, + address, + { terminatedChainID: ctx.terminatedChainID, tokenID: EMPTY_BYTES, amount: BigInt(0) }, + TokenEventResult.RECOVER_FAIL_INVALID_INPUTS, + ); + + throw new Error('Invalid arguments.'); + } + const chainID = ctx.storeKey.slice(ADDRESS_LENGTH, ADDRESS_LENGTH + CHAIN_ID_LENGTH); const tokenID = ctx.storeKey.slice(ADDRESS_LENGTH, ADDRESS_LENGTH + TOKEN_ID_LENGTH); - const localID = ctx.storeKey.slice(ADDRESS_LENGTH + CHAIN_ID_LENGTH); const totalAmount = - account!.availableBalance + - account!.lockedBalances.reduce((prev, curr) => prev + curr.amount, BigInt(0)); + account.availableBalance + + account.lockedBalances.reduce((prev, curr) => prev + curr.amount, BigInt(0)); - const { id: ownChainID } = await this._interopMethod.getOwnChainAccount(methodContext); const escrowStore = this.stores.get(EscrowStore); - const escrowKey = Buffer.concat([ctx.terminatedChainID, localID]); + const escrowKey = escrowStore.getKey(ctx.terminatedChainID, tokenID); const escrowData = await escrowStore.get(ctx, escrowKey); - if (!ownChainID.equals(chainID) || escrowData.amount < totalAmount) { + if (!this._ownChainID.equals(chainID) || escrowData.amount < totalAmount) { this.events .get(RecoverEvent) .error( @@ -291,13 +271,7 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { escrowData.amount -= totalAmount; await escrowStore.set(ctx, escrowKey, escrowData); - const localTokenID = Buffer.concat([CHAIN_ID_ALIAS_NATIVE, localID]); - await userStore.addAvailableBalanceWithCreate( - methodContext, - address, - localTokenID, - totalAmount, - ); + await userStore.addAvailableBalanceWithCreate(methodContext, address, tokenID, totalAmount); this.events.get(RecoverEvent).log(methodContext, address, { terminatedChainID: ctx.terminatedChainID, diff --git a/framework/src/modules/token/module.ts b/framework/src/modules/token/module.ts index 829461f6cd9..36ae1ddbff8 100644 --- a/framework/src/modules/token/module.ts +++ b/framework/src/modules/token/module.ts @@ -63,7 +63,7 @@ import { TokenIDSupportRemovedEvent } from './events/token_id_supported_removed' export class TokenModule extends BaseInteroperableModule { public method = new TokenMethod(this.stores, this.events, this.name); public endpoint = new TokenEndpoint(this.stores, this.offchainStores); - public crossChainMethod = new TokenInteroperableMethod(this.stores, this.events, this.method); + public crossChainMethod = new TokenInteroperableMethod(this.stores, this.events); private _minBalances!: MinBalance[]; private _ownChainID!: Buffer; @@ -178,6 +178,7 @@ export class TokenModule extends BaseInteroperableModule { feeTokenID: Buffer.from(config.feeTokenID, 'hex'), userAccountInitializationFee: BigInt('50000000'), }); + this.crossChainMethod.init(this._ownChainID); this.endpoint.init(this.method); this._transferCommand.init({ method: this.method, From 790712c00b59028d7831d9d64b4396df49f65c83 Mon Sep 17 00:00:00 2001 From: Incede <33103370+Incede@users.noreply.github.com> Date: Fri, 7 Oct 2022 01:54:52 +0200 Subject: [PATCH 3/5] Further feedback --- framework/src/modules/token/cc_method.ts | 30 +++++++++----------- framework/src/modules/token/stores/escrow.ts | 23 +++++++++------ 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/framework/src/modules/token/cc_method.ts b/framework/src/modules/token/cc_method.ts index 96151b0926e..1de07b77125 100644 --- a/framework/src/modules/token/cc_method.ts +++ b/framework/src/modules/token/cc_method.ts @@ -72,7 +72,7 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { if (chainID.equals(this._ownChainID)) { const escrowStore = this.stores.get(EscrowStore); const escrowKey = escrowStore.getKey(ccm.sendingChainID, tokenID); - const escrowAccount = await escrowStore.get(methodContext, escrowKey); + const escrowAccount = await escrowStore.getOrDefault(methodContext, escrowKey); if (escrowAccount.amount < ccm.fee) { this.events.get(BeforeCCCExecutionEvent).error( @@ -94,7 +94,7 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { await escrowStore.set(methodContext, escrowKey, escrowAccount); } - await userStore.addAvailableBalanceWithCreate(methodContext, relayerAddress, tokenID, ccm.fee); + await userStore.addAvailableBalance(methodContext, relayerAddress, tokenID, ccm.fee); this.events.get(BeforeCCCExecutionEvent).log(methodContext, { sendingChainID: ccm.sendingChainID, @@ -118,7 +118,7 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { const escrowStore = this.stores.get(EscrowStore); const escrowKey = escrowStore.getKey(ccm.sendingChainID, tokenID); - const escrowAccount = await escrowStore.get(methodContext, escrowKey); + const escrowAccount = await escrowStore.getOrDefault(methodContext, escrowKey); if (escrowAccount.amount < ccm.fee) { this.events.get(BeforeCCMForwardingEvent).error( methodContext, @@ -139,16 +139,15 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { await escrowStore.addAmount(methodContext, ccm.receivingChainID, tokenID, ccm.fee); - const decodedParams = codec.decode( - crossChainForwardMessageParams, - ccm.params, - ); - if ( ccm.module === MODULE_NAME_TOKEN && ccm.crossChainCommand === CROSS_CHAIN_COMMAND_NAME_TRANSFER && - chainID === this._ownChainID + chainID.equals(this._ownChainID) ) { + const decodedParams = codec.decode( + crossChainForwardMessageParams, + ccm.params, + ); if (escrowAccount.amount < decodedParams.amount) { this.events.get(BeforeCCMForwardingEvent).error( methodContext, @@ -164,7 +163,7 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { throw new Error('Insufficient balance in the sending chain for the transfer.'); } - const updatedEscrowAccount = await escrowStore.get(methodContext, escrowKey); + const updatedEscrowAccount = await escrowStore.getOrDefault(methodContext, escrowKey); updatedEscrowAccount.amount -= decodedParams.amount; await escrowStore.set(methodContext, escrowKey, updatedEscrowAccount); @@ -187,9 +186,6 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { public async verifyCrossChainMessage(ctx: BeforeSendCCMsgMethodContext): Promise { const { ccm } = ctx; const methodContext = ctx.getMethodContext(); - if (ccm.fee < BigInt(0)) { - throw new Error('Fee must be greater or equal to zero.'); - } const tokenID = await this._interopMethod.getMessageFeeTokenID( methodContext, ccm.sendingChainID, @@ -197,7 +193,7 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { const [chainID] = splitTokenID(tokenID); if (chainID.equals(this._ownChainID)) { const escrowStore = this.stores.get(EscrowStore); - const escrowAccount = await escrowStore.get( + const escrowAccount = await escrowStore.getOrDefault( methodContext, escrowStore.getKey(ccm.sendingChainID, tokenID), ); @@ -253,7 +249,7 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { const escrowStore = this.stores.get(EscrowStore); const escrowKey = escrowStore.getKey(ctx.terminatedChainID, tokenID); - const escrowData = await escrowStore.get(ctx, escrowKey); + const escrowData = await escrowStore.getOrDefault(methodContext, escrowKey); if (!this._ownChainID.equals(chainID) || escrowData.amount < totalAmount) { this.events @@ -269,9 +265,9 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { } escrowData.amount -= totalAmount; - await escrowStore.set(ctx, escrowKey, escrowData); + await escrowStore.set(methodContext, escrowKey, escrowData); - await userStore.addAvailableBalanceWithCreate(methodContext, address, tokenID, totalAmount); + await userStore.addAvailableBalance(methodContext, address, tokenID, totalAmount); this.events.get(RecoverEvent).log(methodContext, address, { terminatedChainID: ctx.terminatedChainID, diff --git a/framework/src/modules/token/stores/escrow.ts b/framework/src/modules/token/stores/escrow.ts index 62c84a83517..02e0124c5c6 100644 --- a/framework/src/modules/token/stores/escrow.ts +++ b/framework/src/modules/token/stores/escrow.ts @@ -39,6 +39,19 @@ export class EscrowStore extends BaseStore { return Buffer.concat([escrowChainID, tokenID]); } + public async getOrDefault(ctx: StoreGetter, key: Buffer): Promise { + let escrowData: EscrowStoreData; + try { + escrowData = await this.get(ctx, key); + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } + escrowData = { amount: BigInt(0) }; + } + return escrowData; + } + public async createDefaultAccount( context: StoreGetter, chainID: Buffer, @@ -53,16 +66,8 @@ export class EscrowStore extends BaseStore { tokenID: Buffer, amount: bigint, ): Promise { - let escrowData: EscrowStoreData; const escrowKey = Buffer.concat([chainID, tokenID]); - try { - escrowData = await this.get(context, escrowKey); - } catch (error) { - if (!(error instanceof NotFoundError)) { - throw error; - } - escrowData = { amount: BigInt(0) }; - } + const escrowData = await this.getOrDefault(context, escrowKey); escrowData.amount += amount; await this.set(context, escrowKey, escrowData); } From fd7980b7e9bcf4f86463c73c983402fab9b932a9 Mon Sep 17 00:00:00 2001 From: Incede <33103370+Incede@users.noreply.github.com> Date: Fri, 7 Oct 2022 01:55:35 +0200 Subject: [PATCH 4/5] Add/update unit tests --- ...operable_api.spec.ts => cc_method.spec.ts} | 446 +++++++++++------- 1 file changed, 268 insertions(+), 178 deletions(-) rename framework/test/unit/modules/token/{interoperable_api.spec.ts => cc_method.spec.ts} (61%) diff --git a/framework/test/unit/modules/token/interoperable_api.spec.ts b/framework/test/unit/modules/token/cc_method.spec.ts similarity index 61% rename from framework/test/unit/modules/token/interoperable_api.spec.ts rename to framework/test/unit/modules/token/cc_method.spec.ts index dbb8d4b2560..a475a1881b6 100644 --- a/framework/test/unit/modules/token/interoperable_api.spec.ts +++ b/framework/test/unit/modules/token/cc_method.spec.ts @@ -13,16 +13,20 @@ */ import { codec } from '@liskhq/lisk-codec'; -import { utils } from '@liskhq/lisk-cryptography'; -import { TokenMethod, TokenModule } from '../../../../src/modules/token'; +import { address, utils } from '@liskhq/lisk-cryptography'; +import { TokenModule } from '../../../../src/modules/token'; import { CCM_STATUS_OK, CHAIN_ID_LENGTH, CROSS_CHAIN_COMMAND_NAME_FORWARD, - TOKEN_ID_LENGTH, + CROSS_CHAIN_COMMAND_NAME_TRANSFER, + TokenEventResult, } from '../../../../src/modules/token/constants'; import { TokenInteroperableMethod } from '../../../../src/modules/token/cc_method'; -import { userStoreSchema } from '../../../../src/modules/token/schemas'; +import { + crossChainForwardMessageParams, + userStoreSchema, +} from '../../../../src/modules/token/schemas'; import { MethodContext, createMethodContext, EventQueue } from '../../../../src/state_machine'; import { PrefixedStateReadWriter } from '../../../../src/state_machine/prefixed_state_read_writer'; import { InMemoryPrefixedStateDB } from '../../../../src/testing/in_memory_prefixed_state'; @@ -30,13 +34,20 @@ import { fakeLogger } from '../../../utils/mocks'; import { UserStore } from '../../../../src/modules/token/stores/user'; import { SupplyStore } from '../../../../src/modules/token/stores/supply'; import { EscrowStore } from '../../../../src/modules/token/stores/escrow'; +import { BeforeCCCExecutionEvent } from '../../../../src/modules/token/events/before_ccc_execution'; +import { BeforeCCMForwardingEvent } from '../../../../src/modules/token/events/before_ccm_forwarding'; +import { RecoverEvent } from '../../../../src/modules/token/events/recover'; -describe('CrossChain Forward command', () => { +describe('TokenInteroperableMethod', () => { const tokenModule = new TokenModule(); - const defaultAddress = utils.getRandomBytes(20); - const defaultTokenIDAlias = Buffer.alloc(TOKEN_ID_LENGTH, 0); - const defaultTokenID = Buffer.from([0, 0, 0, 1, 0, 0, 0, 0]); - const defaultForeignTokenID = Buffer.from([1, 0, 0, 0, 0, 0, 0, 0]); + const defaultPublicKey = Buffer.from( + '5d036a858ce89f844491762eb89e2bfbd50a4a0a0da658e4b2628b25b117ae09', + 'hex', + ); + const defaultAddress = address.getAddressFromPublicKey(defaultPublicKey); + const ownChainID = Buffer.from([0, 0, 0, 1]); + const defaultTokenID = Buffer.concat([ownChainID, Buffer.alloc(4)]); + const defaultForeignTokenID = Buffer.from([0, 0, 0, 2, 0, 0, 0, 0]); const defaultAccount = { availableBalance: BigInt(10000000000), lockedBalances: [ @@ -69,104 +80,69 @@ describe('CrossChain Forward command', () => { }; let tokenInteropMethod: TokenInteroperableMethod; - let tokenMethod: TokenMethod; - let interopMethod: { - getOwnChainAccount: jest.Mock; - send: jest.Mock; - error: jest.Mock; - terminateChain: jest.Mock; - getChannel: jest.Mock; - }; let stateStore: PrefixedStateReadWriter; let methodContext: MethodContext; let userStore: UserStore; + let escrowStore: EscrowStore; + + const checkEventResult = ( + eventQueue: EventQueue, + BaseEvent: any, + expectedResult: TokenEventResult, + length = 1, + index = 0, + ) => { + expect(eventQueue.getEvents()).toHaveLength(length); + expect(eventQueue.getEvents()[index].toObject().name).toEqual(new BaseEvent('token').name); + expect( + codec.decode>( + new BaseEvent('token').schema, + eventQueue.getEvents()[index].toObject().data, + ).result, + ).toEqual(expectedResult); + }; beforeEach(async () => { - tokenMethod = new TokenMethod(tokenModule.stores, tokenModule.events, tokenModule.name); - tokenInteropMethod = new TokenInteroperableMethod( - tokenModule.stores, - tokenModule.events, - tokenMethod, - ); - interopMethod = { - getOwnChainAccount: jest.fn().mockResolvedValue({ id: Buffer.from([0, 0, 0, 1]) }), - send: jest.fn(), - error: jest.fn(), - terminateChain: jest.fn(), - getChannel: jest.fn().mockResolvedValue({ messageFeeTokenID: defaultTokenID }), - }; - // const minBalances = [ - // { tokenID: defaultTokenIDAlias, amount: BigInt(MIN_BALANCE) }, - // { tokenID: defaultForeignTokenID, amount: BigInt(MIN_BALANCE) }, - // ]; - tokenMethod.addDependencies(interopMethod as never); - // tokenInteropMethod.addDependencies(interopMethod); - // tokenMethod.init({ - // ownchainID: Buffer.from([0, 0, 0, 1]), - // minBalances, - // }); + tokenInteropMethod = new TokenInteroperableMethod(tokenModule.stores, tokenModule.events); + tokenInteropMethod.init(ownChainID); + tokenInteropMethod.addDependencies({ + send: jest.fn().mockResolvedValue(true), + getMessageFeeTokenID: jest.fn().mockResolvedValue(defaultTokenID), + } as never); - stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); methodContext = createMethodContext({ - stateStore, - eventQueue: new EventQueue(0), + stateStore: new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()), + eventQueue: new EventQueue(0).getChildQueue(Buffer.from([0])), }); userStore = tokenModule.stores.get(UserStore); - await userStore.save(methodContext, defaultAddress, defaultTokenIDAlias, defaultAccount); + await userStore.save(methodContext, defaultAddress, defaultTokenID, defaultAccount); await userStore.save(methodContext, defaultAddress, defaultForeignTokenID, defaultAccount); const supplyStore = tokenModule.stores.get(SupplyStore); - await supplyStore.set(methodContext, defaultTokenIDAlias.slice(CHAIN_ID_LENGTH), { + await supplyStore.set(methodContext, defaultTokenID, { totalSupply: defaultTotalSupply, }); - const escrowStore = tokenModule.stores.get(EscrowStore); + escrowStore = tokenModule.stores.get(EscrowStore); await escrowStore.set( methodContext, - Buffer.concat([ - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), - defaultTokenIDAlias.slice(CHAIN_ID_LENGTH), - ]), + Buffer.concat([defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), defaultTokenID]), { amount: defaultEscrowAmount }, ); await escrowStore.set( methodContext, - Buffer.concat([Buffer.from([3, 0, 0, 0]), defaultTokenIDAlias.slice(CHAIN_ID_LENGTH)]), + Buffer.concat([Buffer.from([3, 0, 0, 0]), defaultTokenID]), { amount: defaultEscrowAmount }, ); }); - // TODO: Update with https://github.com/LiskHQ/lisk-sdk/issues/7577 - describe.skip('beforeApplyCCM', () => { - it('should reject if fee is negative', async () => { + describe('beforeCrossChainCommandExecution', () => { + it('should credit fee to transaction sender if token id is not native', async () => { + jest + .spyOn(tokenInteropMethod['_interopMethod'], 'getMessageFeeTokenID') + .mockResolvedValue(defaultForeignTokenID); await expect( - tokenInteropMethod.beforeApplyCCM({ - ccm: { - crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, - module: tokenModule.name, - nonce: BigInt(1), - sendingChainID, - receivingChainID: Buffer.from([0, 0, 0, 1]), - fee: BigInt(-3), - status: CCM_STATUS_OK, - params: utils.getRandomBytes(30), - }, - feeAddress: defaultAddress, - getMethodContext: () => methodContext, - eventQueue: new EventQueue(0), - getStore: (moduleID: Buffer, prefix: Buffer) => stateStore.getStore(moduleID, prefix), - logger: fakeLogger, - chainID: utils.getRandomBytes(32), - ccu, - trsSender: defaultAddress, - }), - ).rejects.toThrow('Fee must be greater or equal to zero'); - }); - - it('should credit fee to transaction sender if fee token id is not native', async () => { - interopMethod.getChannel.mockResolvedValue({ messageFeeTokenID: defaultForeignTokenID }); - await expect( - tokenInteropMethod.beforeApplyCCM({ + tokenInteropMethod.beforeCrossChainCommandExecution({ ccm: { crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, module: tokenModule.name, @@ -184,17 +160,24 @@ describe('CrossChain Forward command', () => { logger: fakeLogger, chainID: utils.getRandomBytes(32), ccu, - trsSender: defaultAddress, + trsSender: defaultPublicKey, }), ).resolves.toBeUndefined(); - await expect( - tokenMethod.getAvailableBalance(methodContext, defaultAddress, defaultForeignTokenID), - ).resolves.toEqual(defaultAccount.availableBalance + fee); + const { availableBalance } = await userStore.get( + methodContext, + userStore.getKey(defaultAddress, defaultForeignTokenID), + ); + expect(availableBalance).toEqual(defaultAccount.availableBalance + fee); + checkEventResult( + methodContext.eventQueue, + BeforeCCCExecutionEvent, + TokenEventResult.SUCCESSFUL, + ); }); - it('should terminate sending chain if escrow balance is not sufficient', async () => { + it('should throw if escrow balance is not sufficient', async () => { await expect( - tokenInteropMethod.beforeApplyCCM({ + tokenInteropMethod.beforeCrossChainCommandExecution({ ccm: { crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, module: tokenModule.name, @@ -212,15 +195,19 @@ describe('CrossChain Forward command', () => { logger: fakeLogger, chainID: utils.getRandomBytes(32), ccu, - trsSender: defaultAddress, + trsSender: defaultPublicKey, }), - ).resolves.toBeUndefined(); - expect(interopMethod.terminateChain).toHaveBeenCalled(); + ).rejects.toThrow('Insufficient balance in the sending chain for the message fee.'); + checkEventResult( + methodContext.eventQueue, + BeforeCCCExecutionEvent, + TokenEventResult.FAIL_INSUFFICIENT_BALANCE, + ); }); it('should deduct escrow account for fee and credit to sender if token id is native', async () => { await expect( - tokenInteropMethod.beforeApplyCCM({ + tokenInteropMethod.beforeCrossChainCommandExecution({ ccm: { crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, module: tokenModule.name, @@ -238,30 +225,38 @@ describe('CrossChain Forward command', () => { logger: fakeLogger, chainID: utils.getRandomBytes(32), ccu, - trsSender: defaultAddress, + trsSender: defaultPublicKey, }), ).resolves.toBeUndefined(); - await expect( - tokenMethod.getAvailableBalance(methodContext, defaultAddress, defaultTokenID), - ).resolves.toEqual(defaultAccount.availableBalance + fee); - await expect( - tokenMethod.getEscrowedAmount(methodContext, sendingChainID, defaultTokenID), - ).resolves.toEqual(defaultEscrowAmount - fee); + const { availableBalance } = await userStore.get( + methodContext, + userStore.getKey(defaultAddress, defaultTokenID), + ); + expect(availableBalance).toEqual(defaultAccount.availableBalance + fee); + const { amount } = await escrowStore.get( + methodContext, + userStore.getKey(sendingChainID, defaultTokenID), + ); + expect(amount).toEqual(defaultEscrowAmount - fee); + checkEventResult( + methodContext.eventQueue, + BeforeCCCExecutionEvent, + TokenEventResult.SUCCESSFUL, + ); }); }); - // TODO: Update with https://github.com/LiskHQ/lisk-sdk/issues/7577 - describe.skip('beforeRecoverCCM', () => { - it('should reject if fee is negative', async () => { + describe('beforeCrossChainMessageForwarding', () => { + it('should throw if escrow balance is not sufficient', async () => { await expect( - tokenInteropMethod.beforeRecoverCCM({ + tokenInteropMethod.beforeCrossChainMessageForwarding({ ccm: { crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, module: tokenModule.name, nonce: BigInt(1), sendingChainID, receivingChainID: Buffer.from([0, 0, 0, 1]), - fee: BigInt(-3), + fee: fee + defaultEscrowAmount, status: CCM_STATUS_OK, params: utils.getRandomBytes(30), }, @@ -271,15 +266,19 @@ describe('CrossChain Forward command', () => { getStore: (moduleID: Buffer, prefix: Buffer) => stateStore.getStore(moduleID, prefix), logger: fakeLogger, chainID: utils.getRandomBytes(32), - trsSender: defaultAddress, + trsSender: defaultPublicKey, }), - ).rejects.toThrow('Fee must be greater or equal to zero'); + ).rejects.toThrow('Insufficient balance in the sending chain for the message fee.'); + checkEventResult( + methodContext.eventQueue, + BeforeCCMForwardingEvent, + TokenEventResult.FAIL_INSUFFICIENT_BALANCE, + ); }); - it('should credit fee to transaction sender if message fee token id is not native', async () => { - interopMethod.getChannel.mockResolvedValue({ messageFeeTokenID: defaultForeignTokenID }); + it('should deduct escrow account for fee and credit to sender if ccm command is forward', async () => { await expect( - tokenInteropMethod.beforeRecoverCCM({ + tokenInteropMethod.beforeCrossChainMessageForwarding({ ccm: { crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, module: tokenModule.name, @@ -288,7 +287,15 @@ describe('CrossChain Forward command', () => { receivingChainID: Buffer.from([0, 0, 0, 1]), fee, status: CCM_STATUS_OK, - params: utils.getRandomBytes(30), + params: codec.encode(crossChainForwardMessageParams, { + tokenID: utils.getRandomBytes(9), + amount: BigInt(1000), + senderAddress: defaultAddress, + forwardToChainID: Buffer.from([4, 0, 0, 0]), + recipientAddress: defaultAddress, + data: 'ddd', + forwardedMessageFee: BigInt(2000), + }), }, feeAddress: defaultAddress, getMethodContext: () => methodContext, @@ -296,26 +303,42 @@ describe('CrossChain Forward command', () => { getStore: (moduleID: Buffer, prefix: Buffer) => stateStore.getStore(moduleID, prefix), logger: fakeLogger, chainID: utils.getRandomBytes(32), - trsSender: defaultAddress, + trsSender: defaultPublicKey, }), ).resolves.toBeUndefined(); - await expect( - tokenMethod.getAvailableBalance(methodContext, defaultAddress, defaultForeignTokenID), - ).resolves.toEqual(defaultAccount.availableBalance + fee); + + const { amount } = await escrowStore.get( + methodContext, + userStore.getKey(sendingChainID, defaultTokenID), + ); + expect(amount).toEqual(defaultEscrowAmount - fee); + const { amount: receiver } = await escrowStore.get( + methodContext, + userStore.getKey(Buffer.from([0, 0, 0, 1]), defaultTokenID), + ); + expect(receiver).toEqual(fee); }); - it('should terminate sending chain if escrow balance is not sufficient', async () => { + it('should throw if ccm command is transfer but escrow amount is less than the ccm params amount', async () => { await expect( - tokenInteropMethod.beforeRecoverCCM({ + tokenInteropMethod.beforeCrossChainMessageForwarding({ ccm: { - crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, module: tokenModule.name, nonce: BigInt(1), sendingChainID, receivingChainID: Buffer.from([0, 0, 0, 1]), - fee: fee + defaultEscrowAmount, + fee, status: CCM_STATUS_OK, - params: utils.getRandomBytes(30), + params: codec.encode(crossChainForwardMessageParams, { + tokenID: utils.getRandomBytes(9), + amount: defaultEscrowAmount + BigInt(1000), + senderAddress: defaultAddress, + forwardToChainID: Buffer.from([4, 0, 0, 0]), + recipientAddress: defaultAddress, + data: 'ddd', + forwardedMessageFee: BigInt(2000), + }), }, feeAddress: defaultAddress, getMethodContext: () => methodContext, @@ -323,24 +346,36 @@ describe('CrossChain Forward command', () => { getStore: (moduleID: Buffer, prefix: Buffer) => stateStore.getStore(moduleID, prefix), logger: fakeLogger, chainID: utils.getRandomBytes(32), - trsSender: defaultAddress, + trsSender: defaultPublicKey, }), - ).resolves.toBeUndefined(); - expect(interopMethod.terminateChain).toHaveBeenCalled(); + ).rejects.toThrow('Insufficient balance in the sending chain for the transfer.'); + checkEventResult( + methodContext.eventQueue, + BeforeCCMForwardingEvent, + TokenEventResult.INSUFFICIENT_ESCROW_BALANCE, + ); }); - it('should deduct escrow account for fee and credit to sender if token id is native', async () => { + it('should deduct escrow account for fee+ccm.params.amount and credit to sender if ccm command is forward', async () => { await expect( - tokenInteropMethod.beforeRecoverCCM({ + tokenInteropMethod.beforeCrossChainMessageForwarding({ ccm: { - crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, module: tokenModule.name, nonce: BigInt(1), sendingChainID, receivingChainID: Buffer.from([0, 0, 0, 1]), fee, status: CCM_STATUS_OK, - params: utils.getRandomBytes(30), + params: codec.encode(crossChainForwardMessageParams, { + tokenID: utils.getRandomBytes(9), + amount: BigInt(1000), + senderAddress: defaultAddress, + forwardToChainID: Buffer.from([4, 0, 0, 0]), + recipientAddress: defaultAddress, + data: 'ddd', + forwardedMessageFee: BigInt(2000), + }), }, feeAddress: defaultAddress, getMethodContext: () => methodContext, @@ -348,30 +383,38 @@ describe('CrossChain Forward command', () => { getStore: (moduleID: Buffer, prefix: Buffer) => stateStore.getStore(moduleID, prefix), logger: fakeLogger, chainID: utils.getRandomBytes(32), - trsSender: defaultAddress, + trsSender: defaultPublicKey, }), ).resolves.toBeUndefined(); - await expect( - tokenMethod.getAvailableBalance(methodContext, defaultAddress, defaultTokenID), - ).resolves.toEqual(defaultAccount.availableBalance + fee); - await expect( - tokenMethod.getEscrowedAmount(methodContext, sendingChainID, defaultTokenID), - ).resolves.toEqual(defaultEscrowAmount - fee); + const { amount } = await escrowStore.get( + methodContext, + userStore.getKey(sendingChainID, defaultTokenID), + ); + expect(amount).toEqual(defaultEscrowAmount - fee - BigInt(1000)); + const { amount: receiver } = await escrowStore.get( + methodContext, + userStore.getKey(Buffer.from([0, 0, 0, 1]), defaultTokenID), + ); + expect(receiver).toEqual(fee + BigInt(1000)); + checkEventResult( + methodContext.eventQueue, + BeforeCCMForwardingEvent, + TokenEventResult.SUCCESSFUL, + ); }); }); - // TODO: Update with https://github.com/LiskHQ/lisk-sdk/issues/7577 - describe.skip('beforeSendCCM', () => { - it('should reject if fee is negative', async () => { + describe('verifyCrossChainMessage', () => { + it('should resolve if token id is native and escrow amount is sufficient', async () => { await expect( - tokenInteropMethod.beforeSendCCM({ + tokenInteropMethod.verifyCrossChainMessage({ ccm: { crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, module: tokenModule.name, nonce: BigInt(1), sendingChainID, receivingChainID: Buffer.from([0, 0, 0, 1]), - fee: BigInt(-3), + fee, status: CCM_STATUS_OK, params: utils.getRandomBytes(30), }, @@ -382,19 +425,19 @@ describe('CrossChain Forward command', () => { logger: fakeLogger, chainID: utils.getRandomBytes(32), }), - ).rejects.toThrow('Fee must be greater or equal to zero'); + ).resolves.toBeUndefined(); }); - it('should credit receiving chain escrow account for fee if message token id is native', async () => { + it('should reject if token id is native and fee payer does not have sufficient balance', async () => { await expect( - tokenInteropMethod.beforeSendCCM({ + tokenInteropMethod.verifyCrossChainMessage({ ccm: { crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, module: tokenModule.name, nonce: BigInt(1), sendingChainID, receivingChainID: Buffer.from([0, 0, 0, 1]), - fee, + fee: fee + defaultEscrowAmount, status: CCM_STATUS_OK, params: utils.getRandomBytes(30), }, @@ -405,22 +448,22 @@ describe('CrossChain Forward command', () => { logger: fakeLogger, chainID: utils.getRandomBytes(32), }), - ).resolves.toBeUndefined(); - await expect( - tokenMethod.getEscrowedAmount(methodContext, Buffer.from([0, 0, 0, 1]), defaultTokenID), - ).resolves.toEqual(fee); + ).rejects.toThrow('Insufficient escrow amount.'); }); - it('should reject if fee payer does not have sufficient balance', async () => { + it('should resolve if token id is not native', async () => { + jest + .spyOn(tokenInteropMethod['_interopMethod'], 'getMessageFeeTokenID') + .mockResolvedValue(defaultForeignTokenID); await expect( - tokenInteropMethod.beforeSendCCM({ + tokenInteropMethod.verifyCrossChainMessage({ ccm: { crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, module: tokenModule.name, nonce: BigInt(1), sendingChainID, receivingChainID: Buffer.from([0, 0, 0, 1]), - fee: fee + defaultAccount.availableBalance + BigInt(1), + fee, status: CCM_STATUS_OK, params: utils.getRandomBytes(30), }, @@ -431,19 +474,21 @@ describe('CrossChain Forward command', () => { logger: fakeLogger, chainID: utils.getRandomBytes(32), }), - ).rejects.toThrow('does not have sufficient balance for fee'); + ).resolves.toBeUndefined(); }); + }); - it('should deduct fee from fee payer', async () => { + describe('recover', () => { + it('should reject if store prefix is not store prefix user', async () => { await expect( - tokenInteropMethod.beforeSendCCM({ + tokenInteropMethod.recover({ ccm: { crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, module: tokenModule.name, nonce: BigInt(1), sendingChainID, receivingChainID: Buffer.from([0, 0, 0, 1]), - fee, + fee: BigInt(-3), status: CCM_STATUS_OK, params: utils.getRandomBytes(30), }, @@ -453,17 +498,24 @@ describe('CrossChain Forward command', () => { getStore: (moduleID: Buffer, prefix: Buffer) => stateStore.getStore(moduleID, prefix), logger: fakeLogger, chainID: utils.getRandomBytes(32), + module: tokenModule.name, + storeKey: Buffer.concat([defaultAddress, defaultTokenID]), + storePrefix: Buffer.from([0, 0]), + storeValue: codec.encode(userStoreSchema, { + availableBalance: defaultAccount.availableBalance * BigInt(2), + lockedBalances: [{ module: 'dpos', amount: BigInt(20) }], + }), + terminatedChainID: sendingChainID, }), - ).resolves.toBeUndefined(); - await expect( - tokenMethod.getAvailableBalance(methodContext, defaultAddress, defaultTokenID), - ).resolves.toEqual(defaultAccount.availableBalance - fee); + ).rejects.toThrow('Invalid arguments.'); + checkEventResult( + methodContext.eventQueue, + RecoverEvent, + TokenEventResult.RECOVER_FAIL_INVALID_INPUTS, + ); }); - }); - // TODO: Update with https://github.com/LiskHQ/lisk-sdk/issues/7577 - describe.skip('recover', () => { - it('should reject if store fix is not store prefix user', async () => { + it('should reject if store key is not 28 bytes', async () => { await expect( tokenInteropMethod.recover({ ccm: { @@ -483,18 +535,23 @@ describe('CrossChain Forward command', () => { logger: fakeLogger, chainID: utils.getRandomBytes(32), module: tokenModule.name, - storeKey: Buffer.concat([defaultAddress, defaultTokenID]), - storePrefix: Buffer.from([0, 0]), + storeKey: Buffer.concat([defaultAddress, defaultTokenID, Buffer.alloc(20)]), + storePrefix: userStore.subStorePrefix, storeValue: codec.encode(userStoreSchema, { availableBalance: defaultAccount.availableBalance * BigInt(2), lockedBalances: [{ module: 'dpos', amount: BigInt(20) }], }), terminatedChainID: sendingChainID, }), - ).rejects.toThrow('Invalid store prefix'); + ).rejects.toThrow('Invalid arguments.'); + checkEventResult( + methodContext.eventQueue, + RecoverEvent, + TokenEventResult.RECOVER_FAIL_INVALID_INPUTS, + ); }); - it('should reject if store key is not 28 bytes', async () => { + it('should reject if store value cannot be decoded', async () => { await expect( tokenInteropMethod.recover({ ccm: { @@ -514,18 +571,23 @@ describe('CrossChain Forward command', () => { logger: fakeLogger, chainID: utils.getRandomBytes(32), module: tokenModule.name, - storeKey: Buffer.concat([defaultAddress, defaultTokenID, Buffer.alloc(20)]), + storeKey: Buffer.concat([defaultAddress, defaultTokenID]), storePrefix: userStore.subStorePrefix, - storeValue: codec.encode(userStoreSchema, { - availableBalance: defaultAccount.availableBalance * BigInt(2), - lockedBalances: [{ module: 'dpos', amount: BigInt(20) }], - }), + storeValue: utils.getRandomBytes(32), terminatedChainID: sendingChainID, }), - ).rejects.toThrow('Invalid store key'); + ).rejects.toThrow('Invalid arguments.'); + checkEventResult( + methodContext.eventQueue, + RecoverEvent, + TokenEventResult.RECOVER_FAIL_INVALID_INPUTS, + ); }); it('should reject if token is not native', async () => { + jest + .spyOn(tokenInteropMethod['_interopMethod'], 'getMessageFeeTokenID') + .mockResolvedValue(defaultForeignTokenID); await expect( tokenInteropMethod.recover({ ccm: { @@ -553,7 +615,12 @@ describe('CrossChain Forward command', () => { }), terminatedChainID: sendingChainID, }), - ).rejects.toThrow('does not match with own chain ID'); + ).rejects.toThrow('Insufficient escrow amount.'); + checkEventResult( + methodContext.eventQueue, + RecoverEvent, + TokenEventResult.RECOVER_FAIL_INSUFFICIENT_ESCROW, + ); }); it('should reject if not enough balance is escrowed', async () => { @@ -585,11 +652,20 @@ describe('CrossChain Forward command', () => { }), terminatedChainID: sendingChainID, }), - ).rejects.toThrow('is not sufficient for'); + ).rejects.toThrow('Insufficient escrow amount.'); + checkEventResult( + methodContext.eventQueue, + RecoverEvent, + TokenEventResult.RECOVER_FAIL_INSUFFICIENT_ESCROW, + ); }); it('should deduct escrowed amount for the total recovered amount', async () => { const recipient = utils.getRandomBytes(20); + await userStore.set(methodContext, Buffer.concat([recipient, defaultTokenID]), { + availableBalance: BigInt(0), + lockedBalances: [], + }); await expect( tokenInteropMethod.recover({ ccm: { @@ -615,17 +691,25 @@ describe('CrossChain Forward command', () => { terminatedChainID: sendingChainID, }), ).resolves.toBeUndefined(); - await expect( - tokenMethod.getEscrowedAmount(methodContext, sendingChainID, defaultTokenID), - ).resolves.toEqual( + + const { amount } = await escrowStore.get( + methodContext, + userStore.getKey(sendingChainID, defaultTokenID), + ); + expect(amount).toEqual( defaultEscrowAmount - defaultAccount.availableBalance - defaultAccount.lockedBalances[0].amount, ); + checkEventResult(methodContext.eventQueue, RecoverEvent, TokenEventResult.SUCCESSFUL); }); it('should credit the address for the total recovered amount', async () => { const recipient = utils.getRandomBytes(20); + await userStore.set(methodContext, Buffer.concat([recipient, defaultTokenID]), { + availableBalance: BigInt(0), + lockedBalances: [], + }); await expect( tokenInteropMethod.recover({ ccm: { @@ -651,9 +735,15 @@ describe('CrossChain Forward command', () => { terminatedChainID: sendingChainID, }), ).resolves.toBeUndefined(); - await expect( - tokenMethod.getAvailableBalance(methodContext, recipient, defaultTokenID), - ).resolves.toEqual(defaultAccount.availableBalance + defaultAccount.lockedBalances[0].amount); + + const { availableBalance } = await userStore.get( + methodContext, + userStore.getKey(recipient, defaultTokenID), + ); + expect(availableBalance).toEqual( + defaultAccount.availableBalance + defaultAccount.lockedBalances[0].amount, + ); + checkEventResult(methodContext.eventQueue, RecoverEvent, TokenEventResult.SUCCESSFUL); }); }); }); From 348102c7a82eb568c5865ed9b45e883d632f5a48 Mon Sep 17 00:00:00 2001 From: Incede <33103370+Incede@users.noreply.github.com> Date: Tue, 11 Oct 2022 09:59:39 +0200 Subject: [PATCH 5/5] Use updated account to compare and fix test descriptions --- framework/src/modules/token/cc_method.ts | 4 ++-- framework/test/unit/modules/token/cc_method.spec.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/framework/src/modules/token/cc_method.ts b/framework/src/modules/token/cc_method.ts index 1de07b77125..b179e61b1cd 100644 --- a/framework/src/modules/token/cc_method.ts +++ b/framework/src/modules/token/cc_method.ts @@ -148,7 +148,8 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { crossChainForwardMessageParams, ccm.params, ); - if (escrowAccount.amount < decodedParams.amount) { + const updatedEscrowAccount = await escrowStore.get(methodContext, escrowKey); + if (updatedEscrowAccount.amount < decodedParams.amount) { this.events.get(BeforeCCMForwardingEvent).error( methodContext, { @@ -163,7 +164,6 @@ export class TokenInteroperableMethod extends BaseInteroperableMethod { throw new Error('Insufficient balance in the sending chain for the transfer.'); } - const updatedEscrowAccount = await escrowStore.getOrDefault(methodContext, escrowKey); updatedEscrowAccount.amount -= decodedParams.amount; await escrowStore.set(methodContext, escrowKey, updatedEscrowAccount); diff --git a/framework/test/unit/modules/token/cc_method.spec.ts b/framework/test/unit/modules/token/cc_method.spec.ts index a475a1881b6..4c2cbf8bdfb 100644 --- a/framework/test/unit/modules/token/cc_method.spec.ts +++ b/framework/test/unit/modules/token/cc_method.spec.ts @@ -276,7 +276,7 @@ describe('TokenInteroperableMethod', () => { ); }); - it('should deduct escrow account for fee and credit to sender if ccm command is forward', async () => { + it('should deduct escrow account for fee and credit to receving chain escrow account if ccm command is forward', async () => { await expect( tokenInteropMethod.beforeCrossChainMessageForwarding({ ccm: { @@ -356,7 +356,7 @@ describe('TokenInteroperableMethod', () => { ); }); - it('should deduct escrow account for fee+ccm.params.amount and credit to sender if ccm command is forward', async () => { + it('should deduct escrow account for fee+ccm.params.amount and credit to sender if ccm command is transfer', async () => { await expect( tokenInteropMethod.beforeCrossChainMessageForwarding({ ccm: { @@ -428,7 +428,7 @@ describe('TokenInteroperableMethod', () => { ).resolves.toBeUndefined(); }); - it('should reject if token id is native and fee payer does not have sufficient balance', async () => { + it('should reject if token id is native and sending chain escrow account does not have sufficient balance', async () => { await expect( tokenInteropMethod.verifyCrossChainMessage({ ccm: {