--- a/tests/src/util/playgrounds/unique.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -420,6 +420,63 @@ return capture; } + + makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, amount: bigint | string) { + return { + V2: [ + { + WithdrawAsset: [ + { + id: { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + fun: { + Fungible: amount, + }, + }, + ], + }, + { + BuyExecution: { + fees: { + id: { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + fun: { + Fungible: amount, + }, + }, + weightLimit: 'Unlimited' + }, + }, + { + DepositAsset: { + assets: { + Wild: 'All' + }, + maxAssets: 1, + beneficiary: { + parents: 0, + interior: { + X1: { + AccountId32: { + network: 'Any', + id: beneficiary + }, + }, + }, + }, + } + }, + ], + }; + } } class MoonbeamAccountGroup { --- 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 | string, reservedAmount: bigint | string = 0n) { + await this.helper.executeExtrinsic(signer, 'api.tx.balances.setBalance', [address, amount, reservedAmount], true); + } + /** * Transfer tokens to substrate address * @param signer keyring of signer --- a/tests/src/xcm/xcmQuartz.test.ts +++ b/tests/src/xcm/xcmQuartz.test.ts @@ -642,6 +642,84 @@ 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, moreThanKaruraHas); + + // Try to trick Quartz + await usingKaruraPlaygrounds(karuraUrl, async (helper) => { + await helper.getSudo().executeExtrinsic( + alice, + 'api.tx.polkadotXcm.send', + [ + quartzMultilocation, + maliciousXcmProgram, + ], + true, + ); + }); + + 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, validTransferAmount); + + await usingKaruraPlaygrounds(karuraUrl, async (helper) => { + await helper.getSudo().executeExtrinsic( + alice, + 'api.tx.polkadotXcm.send', + [ + quartzMultilocation, + validXcmProgram, + ], + true, + ); + }); + + await helper.wait.newBlocks(maxWaitBlocks); + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(validTransferAmount); + }); }); // These tests are relevant only when @@ -1060,6 +1138,10 @@ console.log('[Moonriver -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees)); expect(qtzFees == 0n).to.be.true; }); + + itSub.skip('Moonriver can send only up to its balance', async ({helper}) => { + throw Error("Not yet implemented"); + }); }); describeXCM('[XCM] Integration test: Exchanging tokens with Shiden', () => { @@ -1272,4 +1354,82 @@ 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, moreThanShidenHas); + + // Try to trick Quartz + await usingShidenPlaygrounds(shidenUrl, async (helper) => { + await helper.getSudo().executeExtrinsic( + alice, + 'api.tx.polkadotXcm.send', + [ + quartzMultilocation, + maliciousXcmProgram, + ], + true, + ); + }); + + 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, validTransferAmount); + + await usingShidenPlaygrounds(shidenUrl, async (helper) => { + await helper.getSudo().executeExtrinsic( + alice, + 'api.tx.polkadotXcm.send', + [ + quartzMultilocation, + validXcmProgram, + ], + true, + ); + }); + + await helper.wait.newBlocks(maxWaitBlocks); + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(validTransferAmount); + }); }); --- a/tests/src/xcm/xcmUnique.test.ts +++ b/tests/src/xcm/xcmUnique.test.ts @@ -644,6 +644,84 @@ 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, moreThanAcalaHas); + + // Try to trick Unique + await usingAcalaPlaygrounds(acalaUrl, async (helper) => { + await helper.getSudo().executeExtrinsic( + alice, + 'api.tx.polkadotXcm.send', + [ + uniqueMultilocation, + maliciousXcmProgram, + ], + true, + ); + }); + + 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, validTransferAmount); + + await usingAcalaPlaygrounds(acalaUrl, async (helper) => { + await helper.getSudo().executeExtrinsic( + alice, + 'api.tx.polkadotXcm.send', + [ + uniqueMultilocation, + validXcmProgram, + ], + true, + ); + }); + + await helper.wait.newBlocks(maxWaitBlocks); + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(validTransferAmount); + }); }); // These tests are relevant only when @@ -1063,6 +1141,10 @@ console.log('[Moonbeam -> Unique] transaction fees on Unique: %s UNQ', helper.util.bigIntToDecimals(unqFees)); expect(unqFees == 0n).to.be.true; }); + + itSub.skip('Moonbeam can send only up to its balance', async ({helper}) => { + throw Error("Not yet implemented"); + }); }); describeXCM('[XCM] Integration test: Exchanging tokens with Astar', () => { @@ -1330,4 +1412,82 @@ await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited'); }); }); + + 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, moreThanShidenHas); + + // Try to trick Unique + await usingAstarPlaygrounds(astarUrl, async (helper) => { + await helper.getSudo().executeExtrinsic( + alice, + 'api.tx.polkadotXcm.send', + [ + uniqueMultilocation, + maliciousXcmProgram, + ], + true, + ); + }); + + 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, validTransferAmount); + + await usingAstarPlaygrounds(astarUrl, async (helper) => { + await helper.getSudo().executeExtrinsic( + alice, + 'api.tx.polkadotXcm.send', + [ + uniqueMultilocation, + validXcmProgram, + ], + true, + ); + }); + + await helper.wait.newBlocks(maxWaitBlocks); + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(validTransferAmount); + }); });