From e02f22f362b00a2d8c985f5013d6527c13323bab Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Wed, 12 Apr 2023 15:40:56 +0000 Subject: [PATCH] Merge pull request #915 from UniqueNetwork/test/additional-xcm-tests Test/additional xcm tests --- --- a/tests/src/util/playgrounds/unique.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -420,6 +420,100 @@ return capture; } + + makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) { + return { + V2: [ + { + WithdrawAsset: [ + { + id, + fun: { + Fungible: amount, + }, + }, + ], + }, + { + BuyExecution: { + fees: { + id, + fun: { + Fungible: amount, + }, + }, + weightLimit: 'Unlimited', + }, + }, + { + DepositAsset: { + assets: { + Wild: 'All', + }, + maxAssets: 1, + beneficiary: { + parents: 0, + interior: { + X1: { + AccountId32: { + network: 'Any', + id: beneficiary, + }, + }, + }, + }, + }, + }, + ], + }; + } + + makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) { + return { + V2: [ + { + ReserveAssetDeposited: [ + { + id, + fun: { + Fungible: amount, + }, + }, + ], + }, + { + BuyExecution: { + fees: { + id, + fun: { + Fungible: amount, + }, + }, + weightLimit: 'Unlimited', + }, + }, + { + DepositAsset: { + assets: { + Wild: 'All', + }, + maxAssets: 1, + beneficiary: { + parents: 0, + interior: { + X1: { + AccountId32: { + network: 'Any', + id: beneficiary, + }, + }, + }, + }, + }, + }, + ], + }; + } } class MoonbeamAccountGroup { @@ -632,6 +726,19 @@ }); return promise; } + + async eventOutcome(maxBlocksToWait: number, eventSection: string, eventMethod: string) { + const eventRecord = await this.event(maxBlocksToWait, eventSection, eventMethod); + + if (eventRecord == null) { + return null; + } + + const event = eventRecord!.event; + const outcome = event.data[1] as EventT; + + return outcome; + } } class SessionGroup { --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -2446,6 +2446,10 @@ return this.ethBalanceGroup.getEthereum(address); } + async setBalanceSubstrate(signer: TSigner, address: TSubstrateAccount, amount: bigint, reservedAmount = 0n) { + await this.helper.executeExtrinsic(signer, 'api.tx.balances.setBalance', [address, amount, reservedAmount], true); + } + /** * Transfer tokens to substrate address * @param signer keyring of signer @@ -3017,6 +3021,18 @@ await this.teleportAssets(signer, destination, beneficiary, assets, feeAssetItem); } + + async send(signer: IKeyringPair, destination: any, message: any) { + await this.helper.executeExtrinsic( + signer, + `api.tx.${this.palletName}.send`, + [ + destination, + message, + ], + true, + ); + } } class XTokensGroup extends HelperGroup { @@ -3280,6 +3296,7 @@ assetRegistry: AcalaAssetRegistryGroup; xTokens: XTokensGroup; tokens: TokensGroup; + xcm: XcmGroup; constructor(logger?: ILogger, options: {[key: string]: any} = {}) { super(logger, options.helperBase ?? AcalaHelper); @@ -3288,6 +3305,7 @@ this.assetRegistry = new AcalaAssetRegistryGroup(this); this.xTokens = new XTokensGroup(this); this.tokens = new TokensGroup(this); + this.xcm = new XcmGroup(this, 'polkadotXcm'); } getSudo() { --- a/tests/src/xcm/xcmQuartz.test.ts +++ b/tests/src/xcm/xcmQuartz.test.ts @@ -19,6 +19,7 @@ import config from '../config'; import {XcmV2TraitsError} from '../interfaces'; import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util'; +import {DevUniqueHelper} from '../util/playgrounds/unique.dev'; const QUARTZ_CHAIN = 2095; const STATEMINE_CHAIN = 1000; @@ -641,63 +642,277 @@ console.log('[Karura -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees)); expect(qtzFees == 0n).to.be.true; }); + + itSub('Karura can send only up to its balance', async ({helper}) => { + // set Karura's sovereign account's balance + const karuraBalance = 10000n * (10n ** QTZ_DECIMALS); + const karuraSovereignAccount = helper.address.paraSiblingSovereignAccount(KARURA_CHAIN); + await helper.getSudo().balance.setBalanceSubstrate(alice, karuraSovereignAccount, karuraBalance); + + const moreThanKaruraHas = karuraBalance * 2n; + + let targetAccountBalance = 0n; + const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice); + + const quartzMultilocation = { + V1: { + parents: 1, + interior: { + X1: {Parachain: QUARTZ_CHAIN}, + }, + }, + }; + + const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + moreThanKaruraHas, + ); + + // Try to trick Quartz + await usingKaruraPlaygrounds(karuraUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram); + }); + + const maxWaitBlocks = 3; + + const xcmpQueueFailEvent = await helper.wait.eventOutcome( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isFailedToTransactAsset, + 'The XCM error should be \'FailedToTransactAsset\'', + ).to.be.true; + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(0n); + + // But Karura still can send the correct amount + const validTransferAmount = karuraBalance / 2n; + const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + validTransferAmount, + ); + + await usingKaruraPlaygrounds(karuraUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram); + }); + + await helper.wait.newBlocks(maxWaitBlocks); + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(validTransferAmount); + }); + + itSub('Should not accept reserve transfer of QTZ from Karura', async ({helper}) => { + const testAmount = 10_000n * (10n ** QTZ_DECIMALS); + const [targetAccount] = await helper.arrange.createAccounts([0n], alice); + + const quartzMultilocation = { + V1: { + parents: 1, + interior: { + X1: { + Parachain: QUARTZ_CHAIN, + }, + }, + }, + }; + + const maliciousXcmProgram = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 1, + interior: { + X1: { + Parachain: QUARTZ_CHAIN, + }, + }, + }, + }, + testAmount, + ); + + await usingKaruraPlaygrounds(karuraUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram); + }); + + const maxWaitBlocks = 3; + + const xcmpQueueFailEvent = await helper.wait.eventOutcome( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; + + const accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); + }); }); -// These tests are relevant only when the foreign asset pallet is disabled +// These tests are relevant only when +// the the corresponding foreign assets are not registered describeXCM('[XCM] Integration test: Quartz rejects non-native tokens', () => { let alice: IKeyringPair; + let alith: IKeyringPair; + + const testAmount = 100_000_000_000n; + let quartzParachainJunction; + let quartzAccountJunction; + let quartzParachainMultilocation: any; + let quartzAccountMultilocation: any; + let quartzCombinedMultilocation: any; + before(async () => { await usingPlaygrounds(async (helper, privateKey) => { alice = await privateKey('//Alice'); - // Set the default version to wrap the first message to other chains. - await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); - }); - }); + quartzParachainJunction = {Parachain: QUARTZ_CHAIN}; + quartzAccountJunction = { + AccountId32: { + network: 'Any', + id: alice.addressRaw, + }, + }; - itSub('Quartz rejects KAR tokens from Karura', async ({helper}) => { - await usingKaruraPlaygrounds(karuraUrl, async (helper) => { - const destination = { + quartzParachainMultilocation = { V1: { parents: 1, interior: { - X2: [ - {Parachain: QUARTZ_CHAIN}, - { - AccountId32: { - network: 'Any', - id: alice.addressRaw, - }, - }, - ], + X1: quartzParachainJunction, + }, + }, + }; + + quartzAccountMultilocation = { + V1: { + parents: 0, + interior: { + X1: quartzAccountJunction, }, }, }; - const id = { - Token: 'KAR', + quartzCombinedMultilocation = { + V1: { + parents: 1, + interior: { + X2: [quartzParachainJunction, quartzAccountJunction], + }, + }, }; - await helper.xTokens.transfer(alice, id, 100_000_000_000n, destination, 'Unlimited'); + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + + // eslint-disable-next-line require-await + await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => { + alith = helper.account.alithAccount(); }); + }); + const expectFailedToTransact = async (network: string, helper: DevUniqueHelper) => { const maxWaitBlocks = 3; - const xcmpQueueFailEvent = await helper.wait.event(maxWaitBlocks, 'xcmpQueue', 'Fail'); + const xcmpQueueFailEvent = await helper.wait.eventOutcome( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + ); expect( xcmpQueueFailEvent != null, - '[Karura] xcmpQueue.FailEvent event is expected', + `[reject ${network} tokens] 'xcmpQueue.FailEvent' event is expected`, ).to.be.true; - const event = xcmpQueueFailEvent!.event; - const outcome = event.data[1] as XcmV2TraitsError; - expect( - outcome.isFailedToTransactAsset, - '[Karura] The XCM error should be `FailedToTransactAsset`', + xcmpQueueFailEvent!.isFailedToTransactAsset, + `[reject ${network} tokens] The XCM error should be 'FailedToTransactAsset'`, ).to.be.true; + }; + + itSub('Quartz rejects KAR tokens from Karura', async ({helper}) => { + await usingKaruraPlaygrounds(karuraUrl, async (helper) => { + const id = { + Token: 'KAR', + }; + const destination = quartzCombinedMultilocation; + await helper.xTokens.transfer(alice, id, testAmount, destination, 'Unlimited'); + }); + + await expectFailedToTransact('KAR', helper); + }); + + itSub('Quartz rejects MOVR tokens from Moonriver', async ({helper}) => { + await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => { + const id = 'SelfReserve'; + const destination = quartzCombinedMultilocation; + await helper.xTokens.transfer(alith, id, testAmount, destination, 'Unlimited'); + }); + + await expectFailedToTransact('MOVR', helper); + }); + + itSub('Quartz rejects SDN tokens from Shiden', async ({helper}) => { + await usingShidenPlaygrounds(shidenUrl, async (helper) => { + const destinationParachain = quartzParachainMultilocation; + const beneficiary = quartzAccountMultilocation; + const assets = { + V1: [{ + id: { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + fun: { + Fungible: testAmount, + }, + }], + }; + const feeAssetItem = 0; + + await helper.executeExtrinsic(alice, 'api.tx.polkadotXcm.reserveWithdrawAssets', [ + destinationParachain, + beneficiary, + assets, + feeAssetItem, + ]); + }); + + await expectFailedToTransact('SDN', helper); }); }); @@ -981,6 +1196,16 @@ console.log('[Moonriver -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees)); expect(qtzFees == 0n).to.be.true; }); + + // eslint-disable-next-line require-await + itSub.skip('Moonriver can send only up to its balance', async ({helper}) => { + throw Error('Not yet implemented'); + }); + + // eslint-disable-next-line require-await + itSub.skip('Should not accept reserve transfer of QTZ from Moonriver', async ({helper}) => { + throw Error('Not yet implemented'); + }); }); describeXCM('[XCM] Integration test: Exchanging tokens with Shiden', () => { @@ -1193,4 +1418,140 @@ console.log(`QTZ Balance on Quartz after XCM is: ${balanceQTZ}`); expect(balanceQTZ).to.eq(balanceAfterQuartzToShidenXCM + qtzFromShidenTransfered); }); + + itSub('Shiden can send only up to its balance', async ({helper}) => { + // set Shiden's sovereign account's balance + const shidenBalance = 10000n * (10n ** QTZ_DECIMALS); + const shidenSovereignAccount = helper.address.paraSiblingSovereignAccount(SHIDEN_CHAIN); + await helper.getSudo().balance.setBalanceSubstrate(alice, shidenSovereignAccount, shidenBalance); + + const moreThanShidenHas = shidenBalance * 2n; + + let targetAccountBalance = 0n; + const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice); + + const quartzMultilocation = { + V1: { + parents: 1, + interior: { + X1: {Parachain: QUARTZ_CHAIN}, + }, + }, + }; + + const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + moreThanShidenHas, + ); + + // Try to trick Quartz + await usingShidenPlaygrounds(shidenUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram); + }); + + const maxWaitBlocks = 3; + + const xcmpQueueFailEvent = await helper.wait.eventOutcome( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isFailedToTransactAsset, + 'The XCM error should be \'FailedToTransactAsset\'', + ).to.be.true; + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(0n); + + // But Shiden still can send the correct amount + const validTransferAmount = shidenBalance / 2n; + const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + validTransferAmount, + ); + + await usingShidenPlaygrounds(shidenUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram); + }); + + await helper.wait.newBlocks(maxWaitBlocks); + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(validTransferAmount); + }); + + itSub('Should not accept reserve transfer of QTZ from Shiden', async ({helper}) => { + const testAmount = 10_000n * (10n ** QTZ_DECIMALS); + const [targetAccount] = await helper.arrange.createAccounts([0n], alice); + + const quartzMultilocation = { + V1: { + parents: 1, + interior: { + X1: { + Parachain: QUARTZ_CHAIN, + }, + }, + }, + }; + + const maliciousXcmProgram = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 1, + interior: { + X1: { + Parachain: QUARTZ_CHAIN, + }, + }, + }, + }, + testAmount, + ); + + await usingShidenPlaygrounds(shidenUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram); + }); + + const maxWaitBlocks = 3; + + const xcmpQueueFailEvent = await helper.wait.eventOutcome( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; + + const accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); + }); }); --- a/tests/src/xcm/xcmUnique.test.ts +++ b/tests/src/xcm/xcmUnique.test.ts @@ -19,6 +19,7 @@ import config from '../config'; import {XcmV2TraitsError} from '../interfaces'; import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingRelayPlaygrounds, usingMoonbeamPlaygrounds, usingStatemintPlaygrounds, usingAstarPlaygrounds} from '../util'; +import {DevUniqueHelper} from '../util/playgrounds/unique.dev'; const UNIQUE_CHAIN = 2037; const STATEMINT_CHAIN = 1000; @@ -643,63 +644,277 @@ console.log('[Acala -> Unique] transaction fees on Unique: %s UNQ', helper.util.bigIntToDecimals(unqFees)); expect(unqFees == 0n).to.be.true; }); + + itSub('Acala can send only up to its balance', async ({helper}) => { + // set Acala's sovereign account's balance + const acalaBalance = 10000n * (10n ** UNQ_DECIMALS); + const acalaSovereignAccount = helper.address.paraSiblingSovereignAccount(ACALA_CHAIN); + await helper.getSudo().balance.setBalanceSubstrate(alice, acalaSovereignAccount, acalaBalance); + + const moreThanAcalaHas = acalaBalance * 2n; + + let targetAccountBalance = 0n; + const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice); + + const uniqueMultilocation = { + V1: { + parents: 1, + interior: { + X1: {Parachain: UNIQUE_CHAIN}, + }, + }, + }; + + const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + moreThanAcalaHas, + ); + + // Try to trick Unique + await usingAcalaPlaygrounds(acalaUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgram); + }); + + const maxWaitBlocks = 3; + + const xcmpQueueFailEvent = await helper.wait.eventOutcome( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isFailedToTransactAsset, + 'The XCM error should be \'FailedToTransactAsset\'', + ).to.be.true; + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(0n); + + // But Acala still can send the correct amount + const validTransferAmount = acalaBalance / 2n; + const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + validTransferAmount, + ); + + await usingAcalaPlaygrounds(acalaUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, uniqueMultilocation, validXcmProgram); + }); + + await helper.wait.newBlocks(maxWaitBlocks); + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(validTransferAmount); + }); + + itSub('Should not accept reserve transfer of UNQ from Acala', async ({helper}) => { + const testAmount = 10_000n * (10n ** UNQ_DECIMALS); + const [targetAccount] = await helper.arrange.createAccounts([0n], alice); + + const uniqueMultilocation = { + V1: { + parents: 1, + interior: { + X1: { + Parachain: UNIQUE_CHAIN, + }, + }, + }, + }; + + const maliciousXcmProgram = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 1, + interior: { + X1: { + Parachain: UNIQUE_CHAIN, + }, + }, + }, + }, + testAmount, + ); + + await usingAcalaPlaygrounds(acalaUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgram); + }); + + const maxWaitBlocks = 3; + + const xcmpQueueFailEvent = await helper.wait.eventOutcome( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; + + const accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); + }); }); -// These tests are relevant only when the foreign asset pallet is disabled +// These tests are relevant only when +// the the corresponding foreign assets are not registered describeXCM('[XCM] Integration test: Unique rejects non-native tokens', () => { let alice: IKeyringPair; + let alith: IKeyringPair; + + const testAmount = 100_000_000_000n; + let uniqueParachainJunction; + let uniqueAccountJunction; + + let uniqueParachainMultilocation: any; + let uniqueAccountMultilocation: any; + let uniqueCombinedMultilocation: any; before(async () => { await usingPlaygrounds(async (helper, privateKey) => { alice = await privateKey('//Alice'); - // Set the default version to wrap the first message to other chains. - await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); - }); - }); + uniqueParachainJunction = {Parachain: UNIQUE_CHAIN}; + uniqueAccountJunction = { + AccountId32: { + network: 'Any', + id: alice.addressRaw, + }, + }; - itSub('Unique rejects ACA tokens from Acala', async ({helper}) => { - await usingAcalaPlaygrounds(acalaUrl, async (helper) => { - const destination = { + uniqueParachainMultilocation = { V1: { parents: 1, interior: { - X2: [ - {Parachain: UNIQUE_CHAIN}, - { - AccountId32: { - network: 'Any', - id: alice.addressRaw, - }, - }, - ], + X1: uniqueParachainJunction, + }, + }, + }; + + uniqueAccountMultilocation = { + V1: { + parents: 0, + interior: { + X1: uniqueAccountJunction, }, }, }; - const id = { - Token: 'ACA', + uniqueCombinedMultilocation = { + V1: { + parents: 1, + interior: { + X2: [uniqueParachainJunction, uniqueAccountJunction], + }, + }, }; - await helper.xTokens.transfer(alice, id, 100_000_000_000n, destination, 'Unlimited'); + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); }); + // eslint-disable-next-line require-await + await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => { + alith = helper.account.alithAccount(); + }); + }); + + const expectFailedToTransact = async (network: string, helper: DevUniqueHelper) => { const maxWaitBlocks = 3; - const xcmpQueueFailEvent = await helper.wait.event(maxWaitBlocks, 'xcmpQueue', 'Fail'); + const xcmpQueueFailEvent = await helper.wait.eventOutcome( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + ); expect( xcmpQueueFailEvent != null, - '[Acala] xcmpQueue.FailEvent event is expected', + `[reject ${network} tokens] 'xcmpQueue.FailEvent' event is expected`, ).to.be.true; - - const event = xcmpQueueFailEvent!.event; - const outcome = event.data[1] as XcmV2TraitsError; expect( - outcome.isFailedToTransactAsset, - '[Acala] The XCM error should be `FailedToTransactAsset`', + xcmpQueueFailEvent!.isFailedToTransactAsset, + `[reject ${network} tokens] The XCM error should be 'FailedToTransactAsset'`, ).to.be.true; + }; + + itSub('Unique rejects ACA tokens from Acala', async ({helper}) => { + await usingAcalaPlaygrounds(acalaUrl, async (helper) => { + const id = { + Token: 'ACA', + }; + const destination = uniqueCombinedMultilocation; + await helper.xTokens.transfer(alice, id, testAmount, destination, 'Unlimited'); + }); + + await expectFailedToTransact('ACA', helper); + }); + + itSub('Unique rejects GLMR tokens from Moonbeam', async ({helper}) => { + await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => { + const id = 'SelfReserve'; + const destination = uniqueCombinedMultilocation; + await helper.xTokens.transfer(alith, id, testAmount, destination, 'Unlimited'); + }); + + await expectFailedToTransact('GLMR', helper); + }); + + itSub('Unique rejects ASTR tokens from Astar', async ({helper}) => { + await usingAstarPlaygrounds(astarUrl, async (helper) => { + const destinationParachain = uniqueParachainMultilocation; + const beneficiary = uniqueAccountMultilocation; + const assets = { + V1: [{ + id: { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + fun: { + Fungible: testAmount, + }, + }], + }; + const feeAssetItem = 0; + + await helper.executeExtrinsic(alice, 'api.tx.polkadotXcm.reserveWithdrawAssets', [ + destinationParachain, + beneficiary, + assets, + feeAssetItem, + ]); + }); + + await expectFailedToTransact('ASTR', helper); }); }); @@ -984,6 +1199,16 @@ console.log('[Moonbeam -> Unique] transaction fees on Unique: %s UNQ', helper.util.bigIntToDecimals(unqFees)); expect(unqFees == 0n).to.be.true; }); + + // eslint-disable-next-line require-await + itSub.skip('Moonbeam can send only up to its balance', async ({helper}) => { + throw Error('Not yet implemented'); + }); + + // eslint-disable-next-line require-await + itSub.skip('Should not accept reserve transfer of UNQ from Moonbeam', async ({helper}) => { + throw Error('Not yet implemented'); + }); }); describeXCM('[XCM] Integration test: Exchanging tokens with Astar', () => { @@ -1196,59 +1421,140 @@ expect(balanceUNQ).to.eq(balanceAfterUniqueToAstarXCM + unqFromAstarTransfered); }); - itSub.skip('Should not accept limitedReserveTransfer of UNQ from ASTAR', async ({helper}) => { + itSub('Astar can send only up to its balance', async ({helper}) => { + // set Astar's sovereign account's balance + const astarBalance = 10000n * (10n ** UNQ_DECIMALS); + const astarSovereignAccount = helper.address.paraSiblingSovereignAccount(ASTAR_CHAIN); + await helper.getSudo().balance.setBalanceSubstrate(alice, astarSovereignAccount, astarBalance); + + const moreThanShidenHas = astarBalance * 2n; + + let targetAccountBalance = 0n; + const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice); + + const uniqueMultilocation = { + V1: { + parents: 1, + interior: { + X1: {Parachain: UNIQUE_CHAIN}, + }, + }, + }; + + const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + moreThanShidenHas, + ); + + // Try to trick Unique + await usingAstarPlaygrounds(astarUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgram); + }); + + const maxWaitBlocks = 3; + + const xcmpQueueFailEvent = await helper.wait.eventOutcome( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isFailedToTransactAsset, + 'The XCM error should be \'FailedToTransactAsset\'', + ).to.be.true; + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(0n); + + // But Astar still can send the correct amount + const validTransferAmount = astarBalance / 2n; + const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + validTransferAmount, + ); + await usingAstarPlaygrounds(astarUrl, async (helper) => { - const destination = { - V1: { - parents: 1, - interior: { - X1: { - Parachain: UNIQUE_CHAIN, - }, + await helper.getSudo().xcm.send(alice, uniqueMultilocation, validXcmProgram); + }); + + await helper.wait.newBlocks(maxWaitBlocks); + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(validTransferAmount); + }); + + itSub('Should not accept reserve transfer of UNQ from Astar', async ({helper}) => { + const testAmount = 10_000n * (10n ** UNQ_DECIMALS); + const [targetAccount] = await helper.arrange.createAccounts([0n], alice); + + const uniqueMultilocation = { + V1: { + parents: 1, + interior: { + X1: { + Parachain: UNIQUE_CHAIN, }, }, - }; + }, + }; - const beneficiary = { - V1: { - parents: 0, + const maliciousXcmProgram = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 1, interior: { X1: { - AccountId32: { - network: 'Any', - id: randomAccount.addressRaw, - }, + Parachain: UNIQUE_CHAIN, }, }, }, - }; + }, + testAmount, + ); - const assets = { - V1: [ - { - id: { - Concrete: { - parents: 1, - interior: { - X1: { - Parachain: UNIQUE_CHAIN, - }, - }, - }, - }, - fun: { - Fungible: unqFromAstarTransfered, - }, - }, - ], - }; + await usingAstarPlaygrounds(astarUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgram); + }); + + const maxWaitBlocks = 3; - // Initial balance is 1 ASTAR - expect(await helper.balance.getSubstrate(randomAccount.address)).to.eq(astarInitialBalance); + const xcmpQueueFailEvent = await helper.wait.eventOutcome( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + ); - const feeAssetItem = 0; - // TODO: expect rejected: - await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited'); - }); + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; + + const accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); }); + }); -- gitstuff