From bcc1c229c277d05edfff91e1d4225ca5621a9dab Mon Sep 17 00:00:00 2001 From: Daniel Shiposha Date: Thu, 13 Apr 2023 01:41:21 +0000 Subject: [PATCH] fix: add/extend xcm tests for moonbeam, extend untrusted reserve location --- --- a/tests/src/util/playgrounds/unique.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import {stringToU8a} from '@polkadot/util'; -import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto'; +import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto'; import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper, AstarHelper} from './unique'; import {ApiPromise, Keyring, WsProvider} from '@polkadot/api'; import * as defs from '../../interfaces/definitions'; @@ -155,6 +155,7 @@ export class DevMoonbeamHelper extends MoonbeamHelper { account: MoonbeamAccountGroup; wait: WaitGroup; + fastDemocracy: MoonbeamFastDemocracyGroup; constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) { options.helperBase = options.helperBase ?? DevMoonbeamHelper; @@ -163,6 +164,7 @@ super(logger, options); this.account = new MoonbeamAccountGroup(this); this.wait = new WaitGroup(this); + this.fastDemocracy = new MoonbeamFastDemocracyGroup(this); } } @@ -554,6 +556,103 @@ } } +class MoonbeamFastDemocracyGroup { + helper: DevMoonbeamHelper; + + constructor(helper: DevMoonbeamHelper) { + this.helper = helper; + } + + async executeProposal(proposalDesciption: string, encodedProposal: string) { + const proposalHash = blake2AsHex(encodedProposal); + + const alithAccount = this.helper.account.alithAccount(); + const baltatharAccount = this.helper.account.baltatharAccount(); + const dorothyAccount = this.helper.account.dorothyAccount(); + + const councilVotingThreshold = 2; + const technicalCommitteeThreshold = 2; + const fastTrackVotingPeriod = 3; + const fastTrackDelayPeriod = 0; + + console.log(`[democracy] executing '${proposalDesciption}' proposal`); + + // >>> Propose external motion through council >>> + console.log('\t* Propose external motion through council.......'); + const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal}); + const encodedMotion = externalMotion?.method.toHex() || ''; + const motionHash = blake2AsHex(encodedMotion); + console.log('\t* Motion hash is %s', motionHash); + + await this.helper.collective.council.propose( + baltatharAccount, + councilVotingThreshold, + externalMotion, + externalMotion.encodedLength, + ); + + const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1; + await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true); + await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true); + + await this.helper.collective.council.close( + dorothyAccount, + motionHash, + councilProposalIdx, + { + refTime: 1_000_000_000, + proofSize: 1_000_000, + }, + externalMotion.encodedLength, + ); + console.log('\t* Propose external motion through council.......DONE'); + // <<< Propose external motion through council <<< + + // >>> Fast track proposal through technical committee >>> + console.log('\t* Fast track proposal through technical committee.......'); + const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod); + const encodedFastTrack = fastTrack?.method.toHex() || ''; + const fastTrackHash = blake2AsHex(encodedFastTrack); + console.log('\t* FastTrack hash is %s', fastTrackHash); + + await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength); + + const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1; + await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true); + await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true); + + await this.helper.collective.techCommittee.close( + baltatharAccount, + fastTrackHash, + techProposalIdx, + { + refTime: 1_000_000_000, + proofSize: 1_000_000, + }, + fastTrack.encodedLength, + ); + console.log('\t* Fast track proposal through technical committee.......DONE'); + // <<< Fast track proposal through technical committee <<< + + const refIndexField = 0; + const referendumIndex = await this.helper.wait.eventData(3, 'democracy', 'Started', refIndexField); + + // >>> Referendum voting >>> + console.log(`\t* Referendum #${referendumIndex} voting.......`); + await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, { + balance: 10_000_000_000_000_000_000n, + vote: {aye: true, conviction: 1}, + }); + console.log(`\t* Referendum #${referendumIndex} voting.......DONE`); + // <<< Referendum voting <<< + + // Wait for the democracy execute + await this.helper.wait.newBlocks(5); + + console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`); + } +} + class WaitGroup { helper: ChainHelperBase; @@ -727,7 +826,7 @@ return promise; } - async eventOutcome(maxBlocksToWait: number, eventSection: string, eventMethod: string) { + async eventData(maxBlocksToWait: number, eventSection: string, eventMethod: string, fieldIndex: number) { const eventRecord = await this.event(maxBlocksToWait, eventSection, eventMethod); if (eventRecord == null) { @@ -735,9 +834,9 @@ } const event = eventRecord!.event; - const outcome = event.data[1] as EventT; + const data = event.data[fieldIndex] as EventT; - return outcome; + return data; } } --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -642,6 +642,10 @@ return call(...params); } + encodeApiCall(apiCall: string, params: any[]) { + return this.constructApiCall(apiCall, params).method.toHex(); + } + async executeExtrinsic(sender: TSigner, extrinsic: string, params: any[], expectSuccess=true, options: Partial|null = null/*, failureMessage='expected success'*/) { if(this.api === null) throw Error('API not initialized'); if(!extrinsic.startsWith('api.tx.')) throw Error(`${extrinsic} is not transaction`); --- a/tests/src/xcm/xcmQuartz.test.ts +++ b/tests/src/xcm/xcmQuartz.test.ts @@ -15,7 +15,6 @@ // along with Unique Network. If not, see . import {IKeyringPair} from '@polkadot/types/types'; -import {blake2AsHex} from '@polkadot/util-crypto'; import config from '../config'; import {XcmV2TraitsError} from '../interfaces'; import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util'; @@ -680,11 +679,13 @@ }); const maxWaitBlocks = 3; + const outcomeField = 1; - const xcmpQueueFailEvent = await helper.wait.eventOutcome( + const xcmpQueueFailEvent = await helper.wait.eventData( maxWaitBlocks, 'xcmpQueue', 'Fail', + outcomeField, ); expect( @@ -738,7 +739,7 @@ }, }; - const maliciousXcmProgram = helper.arrange.makeXcmProgramReserveAssetDeposited( + const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited( targetAccount.addressRaw, { Concrete: { @@ -753,16 +754,55 @@ testAmount, ); + const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + testAmount, + ); + + // Try to trick Quartz using full QTZ identification await usingKaruraPlaygrounds(karuraUrl, async (helper) => { - await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram); + await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId); }); const maxWaitBlocks = 3; + const outcomeField = 1; - const xcmpQueueFailEvent = await helper.wait.eventOutcome( + let xcmpQueueFailEvent = await helper.wait.eventData( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + outcomeField, + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; + + let accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); + + // Try to trick Quartz using shortened QTZ identification + await usingKaruraPlaygrounds(karuraUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId); + }); + + xcmpQueueFailEvent = await helper.wait.eventData( maxWaitBlocks, 'xcmpQueue', 'Fail', + outcomeField, ); expect( @@ -775,7 +815,7 @@ 'The XCM error should be \'isUntrustedReserveLocation\'', ).to.be.true; - const accountBalance = await helper.balance.getSubstrate(targetAccount.address); + accountBalance = await helper.balance.getSubstrate(targetAccount.address); expect(accountBalance).to.be.equal(0n); }); }); @@ -845,11 +885,13 @@ const expectFailedToTransact = async (network: string, helper: DevUniqueHelper) => { const maxWaitBlocks = 3; + const outcomeField = 1; - const xcmpQueueFailEvent = await helper.wait.eventOutcome( + const xcmpQueueFailEvent = await helper.wait.eventData( maxWaitBlocks, 'xcmpQueue', 'Fail', + outcomeField, ); expect( @@ -918,7 +960,7 @@ describeXCM('[XCM] Integration test: Exchanging QTZ with Moonriver', () => { // Quartz constants - let quartzDonor: IKeyringPair; + let alice: IKeyringPair; let quartzAssetLocation; let randomAccountQuartz: IKeyringPair; @@ -927,11 +969,6 @@ // Moonriver constants let assetId: string; - const councilVotingThreshold = 2; - const technicalCommitteeThreshold = 2; - const votingPeriod = 3; - const delayPeriod = 0; - const quartzAssetMetadata = { name: 'xcQuartz', symbol: 'xcQTZ', @@ -952,13 +989,12 @@ before(async () => { await usingPlaygrounds(async (helper, privateKey) => { - quartzDonor = await privateKey('//Alice'); - [randomAccountQuartz] = await helper.arrange.createAccounts([0n], quartzDonor); + alice = await privateKey('//Alice'); + [randomAccountQuartz] = await helper.arrange.createAccounts([0n], alice); balanceForeignQtzTokenInit = 0n; // Set the default version to wrap the first message to other chains. - const alice = quartzDonor; await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); }); @@ -994,84 +1030,13 @@ unitsPerSecond, numAssetsWeightHint, }); - const proposalHash = blake2AsHex(encodedProposal); console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal); - console.log('Encoded length %d', encodedProposal.length); - console.log('Encoded proposal hash for batch utility after schedule is %s', proposalHash); - // >>> Note motion preimage >>> - console.log('Note motion preimage.......'); - await helper.democracy.notePreimage(baltatharAccount, encodedProposal); - console.log('Note motion preimage.......DONE'); - // <<< Note motion preimage <<< - - // >>> Propose external motion through council >>> - console.log('Propose external motion through council.......'); - const externalMotion = helper.democracy.externalProposeMajority({Legacy: proposalHash}); - const encodedMotion = externalMotion?.method.toHex() || ''; - const motionHash = blake2AsHex(encodedMotion); - console.log('Motion hash is %s', motionHash); - - await helper.collective.council.propose(baltatharAccount, councilVotingThreshold, externalMotion, externalMotion.encodedLength); - - const councilProposalIdx = await helper.collective.council.proposalCount() - 1; - await helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true); - await helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true); - - await helper.collective.council.close( - dorothyAccount, - motionHash, - councilProposalIdx, - { - refTime: 1_000_000_000, - proofSize: 1_000_000, - }, - externalMotion.encodedLength, - ); - console.log('Propose external motion through council.......DONE'); - // <<< Propose external motion through council <<< - - // >>> Fast track proposal through technical committee >>> - console.log('Fast track proposal through technical committee.......'); - const fastTrack = helper.democracy.fastTrack(proposalHash, votingPeriod, delayPeriod); - const encodedFastTrack = fastTrack?.method.toHex() || ''; - const fastTrackHash = blake2AsHex(encodedFastTrack); - console.log('FastTrack hash is %s', fastTrackHash); - - await helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength); - - const techProposalIdx = await helper.collective.techCommittee.proposalCount() - 1; - await helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true); - await helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true); - - await helper.collective.techCommittee.close( - baltatharAccount, - fastTrackHash, - techProposalIdx, - { - refTime: 1_000_000_000, - proofSize: 1_000_000, - }, - fastTrack.encodedLength, - ); - console.log('Fast track proposal through technical committee.......DONE'); - // <<< Fast track proposal through technical committee <<< - - // >>> Referendum voting >>> - console.log('Referendum voting.......'); - await helper.democracy.referendumVote(dorothyAccount, 0, { - balance: 10_000_000_000_000_000_000n, - vote: {aye: true, conviction: 1}, - }); - console.log('Referendum voting.......DONE'); - // <<< Referendum voting <<< + await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal); // >>> Acquire Quartz AssetId Info on Moonriver >>> console.log('Acquire Quartz AssetId Info on Moonriver.......'); - - // Wait for the democracy execute - await helper.wait.newBlocks(5); assetId = (await helper.assetManager.assetTypeId(quartzAssetLocation)).toString(); @@ -1089,7 +1054,7 @@ }); await usingPlaygrounds(async (helper) => { - await helper.balance.transferToSubstrate(quartzDonor, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT); + await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT); balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccountQuartz.address); }); }); @@ -1197,14 +1162,197 @@ 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'); + itSub('Moonriver can send only up to its balance', async ({helper}) => { + // set Moonriver's sovereign account's balance + const moonriverBalance = 10000n * (10n ** QTZ_DECIMALS); + const moonriverSovereignAccount = helper.address.paraSiblingSovereignAccount(MOONRIVER_CHAIN); + await helper.getSudo().balance.setBalanceSubstrate(alice, moonriverSovereignAccount, moonriverBalance); + + const moreThanMoonriverHas = moonriverBalance * 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', + }, + }, + moreThanMoonriverHas, + ); + + // Try to trick Quartz + await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => { + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgram]); + + // Needed to bypass the call filter. + const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); + await helper.fastDemocracy.executeProposal('try to spend more QTZ than Moonriver has', batchCall); + }); + + const maxWaitBlocks = 3; + const outcomeField = 1; + + const xcmpQueueFailEvent = await helper.wait.eventData( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + outcomeField, + ); + + 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 Moonriver still can send the correct amount + const validTransferAmount = moonriverBalance / 2n; + const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + validTransferAmount, + ); + + await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => { + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, validXcmProgram]); + + // Needed to bypass the call filter. + const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); + await helper.fastDemocracy.executeProposal('Spend the correct amount of QTZ', batchCall); + }); + + await helper.wait.newBlocks(maxWaitBlocks); + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(validTransferAmount); }); - // eslint-disable-next-line require-await - itSub.skip('Should not accept reserve transfer of QTZ from Moonriver', async ({helper}) => { - throw Error('Not yet implemented'); + itSub('Should not accept reserve transfer of QTZ from Moonriver', 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 maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: { + X1: { + Parachain: QUARTZ_CHAIN, + }, + }, + }, + }, + testAmount, + ); + + const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + testAmount, + ); + + // Try to trick Quartz using full QTZ identification + await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => { + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramFullId]); + + // Needed to bypass the call filter. + const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); + await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using path asset identification', batchCall); + }); + + const maxWaitBlocks = 3; + const outcomeField = 1; + + let xcmpQueueFailEvent = await helper.wait.eventData( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + outcomeField, + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; + + let accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); + + // Try to trick Quartz using shortened QTZ identification + await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => { + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramHereId]); + + // Needed to bypass the call filter. + const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); + await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using "here" asset identification', batchCall); + }); + + xcmpQueueFailEvent = await helper.wait.eventData( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + outcomeField, + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; + + accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); }); }); @@ -1456,11 +1604,13 @@ }); const maxWaitBlocks = 3; + const outcomeField = 1; - const xcmpQueueFailEvent = await helper.wait.eventOutcome( + const xcmpQueueFailEvent = await helper.wait.eventData( maxWaitBlocks, 'xcmpQueue', 'Fail', + outcomeField, ); expect( @@ -1514,7 +1664,7 @@ }, }; - const maliciousXcmProgram = helper.arrange.makeXcmProgramReserveAssetDeposited( + const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited( targetAccount.addressRaw, { Concrete: { @@ -1529,16 +1679,55 @@ testAmount, ); + const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + testAmount, + ); + + // Try to trick Quartz using full QTZ identification await usingShidenPlaygrounds(shidenUrl, async (helper) => { - await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram); + await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId); }); const maxWaitBlocks = 3; + const outcomeField = 1; + + let xcmpQueueFailEvent = await helper.wait.eventData( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + outcomeField, + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; + + let accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); + + // Try to trick Quartz using shortened QTZ identification + await usingShidenPlaygrounds(shidenUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId); + }); - const xcmpQueueFailEvent = await helper.wait.eventOutcome( + xcmpQueueFailEvent = await helper.wait.eventData( maxWaitBlocks, 'xcmpQueue', 'Fail', + outcomeField, ); expect( @@ -1551,7 +1740,7 @@ 'The XCM error should be \'isUntrustedReserveLocation\'', ).to.be.true; - const accountBalance = await helper.balance.getSubstrate(targetAccount.address); + 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 @@ -15,7 +15,6 @@ // along with Unique Network. If not, see . import {IKeyringPair} from '@polkadot/types/types'; -import {blake2AsHex} from '@polkadot/util-crypto'; import config from '../config'; import {XcmV2TraitsError} from '../interfaces'; import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingRelayPlaygrounds, usingMoonbeamPlaygrounds, usingStatemintPlaygrounds, usingAstarPlaygrounds} from '../util'; @@ -682,11 +681,13 @@ }); const maxWaitBlocks = 3; + const outcomeField = 1; - const xcmpQueueFailEvent = await helper.wait.eventOutcome( + const xcmpQueueFailEvent = await helper.wait.eventData( maxWaitBlocks, 'xcmpQueue', 'Fail', + outcomeField, ); expect( @@ -725,7 +726,7 @@ expect(targetAccountBalance).to.be.equal(validTransferAmount); }); - itSub('Should not accept reserve transfer of UNQ from Acala', async ({helper}) => { + itSub.only('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); @@ -740,7 +741,7 @@ }, }; - const maliciousXcmProgram = helper.arrange.makeXcmProgramReserveAssetDeposited( + const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited( targetAccount.addressRaw, { Concrete: { @@ -755,16 +756,30 @@ testAmount, ); + const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + testAmount, + ); + + // Try to trick Unique using full UNQ identification await usingAcalaPlaygrounds(acalaUrl, async (helper) => { - await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgram); + await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgramFullId); }); const maxWaitBlocks = 3; + const outcomeField = 1; - const xcmpQueueFailEvent = await helper.wait.eventOutcome( + let xcmpQueueFailEvent = await helper.wait.eventData( maxWaitBlocks, 'xcmpQueue', 'Fail', + outcomeField, ); expect( @@ -777,8 +792,33 @@ 'The XCM error should be \'isUntrustedReserveLocation\'', ).to.be.true; - const accountBalance = await helper.balance.getSubstrate(targetAccount.address); + let accountBalance = await helper.balance.getSubstrate(targetAccount.address); expect(accountBalance).to.be.equal(0n); + + // Try to trick Unique using shortened UNQ identification + await usingAcalaPlaygrounds(acalaUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgramHereId); + }); + + xcmpQueueFailEvent = await helper.wait.eventData( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + outcomeField, + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; + + accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); }); }); @@ -847,11 +887,13 @@ const expectFailedToTransact = async (network: string, helper: DevUniqueHelper) => { const maxWaitBlocks = 3; + const outcomeField = 1; - const xcmpQueueFailEvent = await helper.wait.eventOutcome( + const xcmpQueueFailEvent = await helper.wait.eventData( maxWaitBlocks, 'xcmpQueue', 'Fail', + outcomeField, ); expect( @@ -920,7 +962,7 @@ describeXCM('[XCM] Integration test: Exchanging UNQ with Moonbeam', () => { // Unique constants - let uniqueDonor: IKeyringPair; + let alice: IKeyringPair; let uniqueAssetLocation; let randomAccountUnique: IKeyringPair; @@ -929,11 +971,6 @@ // Moonbeam constants let assetId: string; - const councilVotingThreshold = 2; - const technicalCommitteeThreshold = 2; - const votingPeriod = 3; - const delayPeriod = 0; - const uniqueAssetMetadata = { name: 'xcUnique', symbol: 'xcUNQ', @@ -954,13 +991,12 @@ before(async () => { await usingPlaygrounds(async (helper, privateKey) => { - uniqueDonor = await privateKey('//Alice'); - [randomAccountUnique] = await helper.arrange.createAccounts([0n], uniqueDonor); + alice = await privateKey('//Alice'); + [randomAccountUnique] = await helper.arrange.createAccounts([0n], alice); balanceForeignUnqTokenInit = 0n; // Set the default version to wrap the first message to other chains. - const alice = uniqueDonor; await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); }); @@ -996,84 +1032,13 @@ unitsPerSecond, numAssetsWeightHint, }); - const proposalHash = blake2AsHex(encodedProposal); console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal); - console.log('Encoded length %d', encodedProposal.length); - console.log('Encoded proposal hash for batch utility after schedule is %s', proposalHash); - - // >>> Note motion preimage >>> - console.log('Note motion preimage.......'); - await helper.democracy.notePreimage(baltatharAccount, encodedProposal); - console.log('Note motion preimage.......DONE'); - // <<< Note motion preimage <<< - - // >>> Propose external motion through council >>> - console.log('Propose external motion through council.......'); - const externalMotion = helper.democracy.externalProposeMajority({Legacy: proposalHash}); - const encodedMotion = externalMotion?.method.toHex() || ''; - const motionHash = blake2AsHex(encodedMotion); - console.log('Motion hash is %s', motionHash); - - await helper.collective.council.propose(baltatharAccount, councilVotingThreshold, externalMotion, externalMotion.encodedLength); - - const councilProposalIdx = await helper.collective.council.proposalCount() - 1; - await helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true); - await helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true); - - await helper.collective.council.close( - dorothyAccount, - motionHash, - councilProposalIdx, - { - refTime: 1_000_000_000, - proofSize: 1_000_000, - }, - externalMotion.encodedLength, - ); - console.log('Propose external motion through council.......DONE'); - // <<< Propose external motion through council <<< - // >>> Fast track proposal through technical committee >>> - console.log('Fast track proposal through technical committee.......'); - const fastTrack = helper.democracy.fastTrack(proposalHash, votingPeriod, delayPeriod); - const encodedFastTrack = fastTrack?.method.toHex() || ''; - const fastTrackHash = blake2AsHex(encodedFastTrack); - console.log('FastTrack hash is %s', fastTrackHash); - - await helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength); - - const techProposalIdx = await helper.collective.techCommittee.proposalCount() - 1; - await helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true); - await helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true); - - await helper.collective.techCommittee.close( - baltatharAccount, - fastTrackHash, - techProposalIdx, - { - refTime: 1_000_000_000, - proofSize: 1_000_000, - }, - fastTrack.encodedLength, - ); - console.log('Fast track proposal through technical committee.......DONE'); - // <<< Fast track proposal through technical committee <<< + await helper.fastDemocracy.executeProposal('register UNQ foreign asset', encodedProposal); - // >>> Referendum voting >>> - console.log('Referendum voting.......'); - await helper.democracy.referendumVote(dorothyAccount, 0, { - balance: 10_000_000_000_000_000_000n, - vote: {aye: true, conviction: 1}, - }); - console.log('Referendum voting.......DONE'); - // <<< Referendum voting <<< - // >>> Acquire Unique AssetId Info on Moonbeam >>> console.log('Acquire Unique AssetId Info on Moonbeam.......'); - - // Wait for the democracy execute - await helper.wait.newBlocks(5); assetId = (await helper.assetManager.assetTypeId(uniqueAssetLocation)).toString(); @@ -1091,7 +1056,7 @@ }); await usingPlaygrounds(async (helper) => { - await helper.balance.transferToSubstrate(uniqueDonor, randomAccountUnique.address, 10n * TRANSFER_AMOUNT); + await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, 10n * TRANSFER_AMOUNT); balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccountUnique.address); }); }); @@ -1200,14 +1165,197 @@ 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'); + itSub('Moonbeam can send only up to its balance', async ({helper}) => { + // set Moonbeam's sovereign account's balance + const moonbeamBalance = 10000n * (10n ** UNQ_DECIMALS); + const moonbeamSovereignAccount = helper.address.paraSiblingSovereignAccount(MOONBEAM_CHAIN); + await helper.getSudo().balance.setBalanceSubstrate(alice, moonbeamSovereignAccount, moonbeamBalance); + + const moreThanMoonbeamHas = moonbeamBalance * 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', + }, + }, + moreThanMoonbeamHas, + ); + + // Try to trick Unique + await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => { + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueMultilocation, maliciousXcmProgram]); + + // Needed to bypass the call filter. + const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); + await helper.fastDemocracy.executeProposal('try to spend more UNQ than Moonbeam has', batchCall); + }); + + const maxWaitBlocks = 3; + const outcomeField = 1; + + const xcmpQueueFailEvent = await helper.wait.eventData( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + outcomeField, + ); + + 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 Moonbeam still can send the correct amount + const validTransferAmount = moonbeamBalance / 2n; + const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + validTransferAmount, + ); + + await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => { + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueMultilocation, validXcmProgram]); + + // Needed to bypass the call filter. + const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); + await helper.fastDemocracy.executeProposal('Spend the correct amount of UNQ', batchCall); + }); + + await helper.wait.newBlocks(maxWaitBlocks); + + targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(validTransferAmount); }); - // eslint-disable-next-line require-await - itSub.skip('Should not accept reserve transfer of UNQ from Moonbeam', async ({helper}) => { - throw Error('Not yet implemented'); + itSub.only('Should not accept reserve transfer of UNQ from Moonbeam', 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 maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: { + X1: { + Parachain: UNIQUE_CHAIN, + }, + }, + }, + }, + testAmount, + ); + + const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + testAmount, + ); + + // Try to trick Unique using full UNQ identification + await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => { + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueMultilocation, maliciousXcmProgramFullId]); + + // Needed to bypass the call filter. + const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); + await helper.fastDemocracy.executeProposal('try to act like a reserve location for UNQ using path asset identification', batchCall); + }); + + const maxWaitBlocks = 3; + const outcomeField = 1; + + let xcmpQueueFailEvent = await helper.wait.eventData( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + outcomeField, + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; + + let accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); + + // Try to trick Unique using shortened UNQ identification + await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => { + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueMultilocation, maliciousXcmProgramHereId]); + + // Needed to bypass the call filter. + const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); + await helper.fastDemocracy.executeProposal('try to act like a reserve location for UNQ using "here" asset identification', batchCall); + }); + + xcmpQueueFailEvent = await helper.wait.eventData( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + outcomeField, + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; + + accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); }); }); @@ -1215,14 +1363,14 @@ let alice: IKeyringPair; let randomAccount: IKeyringPair; - const UNQ_ASSET_ID_ON_SHIDEN = 1; - const UNQ_MINIMAL_BALANCE_ON_SHIDEN = 1n; + const UNQ_ASSET_ID_ON_ASTAR = 1; + const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; // Unique -> Astar - const astarInitialBalance = 1n * (10n ** ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Shiden. + const astarInitialBalance = 1n * (10n ** ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Astar. const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours? const unqToAstarTransferred = 10n * (10n ** UNQ_DECIMALS); // 10 UNQ - const unqToAstarArrived = 9_999_999_999_088_000_000n; // 9.999 ... UNQ, Shiden takes a commision in foreign tokens + const unqToAstarArrived = 9_999_999_999_088_000_000n; // 9.999 ... UNQ, Astar takes a commision in foreign tokens // Astar -> Unique const unqFromAstarTransfered = 5n * (10n ** UNQ_DECIMALS); // 5 UNQ @@ -1245,14 +1393,14 @@ // TODO update metadata with values from production await helper.assets.create( alice, - UNQ_ASSET_ID_ON_SHIDEN, + UNQ_ASSET_ID_ON_ASTAR, alice.address, - UNQ_MINIMAL_BALANCE_ON_SHIDEN, + UNQ_MINIMAL_BALANCE_ON_ASTAR, ); await helper.assets.setMetadata( alice, - UNQ_ASSET_ID_ON_SHIDEN, + UNQ_ASSET_ID_ON_ASTAR, 'Cross chain UNQ', 'xcUNQ', Number(UNQ_DECIMALS), @@ -1270,7 +1418,7 @@ }, }; - await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, UNQ_ASSET_ID_ON_SHIDEN]); + await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, UNQ_ASSET_ID_ON_ASTAR]); console.log('3. Set UNQ payment for XCM execution on Astar'); await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]); @@ -1337,7 +1485,7 @@ await usingAstarPlaygrounds(astarUrl, async (helper) => { await helper.wait.newBlocks(3); - const xcUNQbalance = await helper.assets.account(UNQ_ASSET_ID_ON_SHIDEN, randomAccount.address); + const xcUNQbalance = await helper.assets.account(UNQ_ASSET_ID_ON_ASTAR, randomAccount.address); const astarBalance = await helper.balance.getSubstrate(randomAccount.address); console.log(`xcUNQ balance on Astar after XCM is: ${xcUNQbalance}`); @@ -1405,7 +1553,7 @@ // this is non-standard polkadotXcm extension for Astar only. It calls InitiateReserveWithdraw await helper.executeExtrinsic(randomAccount, 'api.tx.polkadotXcm.reserveWithdrawAssets', [destination, beneficiary, assets, feeAssetItem]); - const xcUNQbalance = await helper.assets.account(UNQ_ASSET_ID_ON_SHIDEN, randomAccount.address); + const xcUNQbalance = await helper.assets.account(UNQ_ASSET_ID_ON_ASTAR, randomAccount.address); const balanceAstar = await helper.balance.getSubstrate(randomAccount.address); console.log(`xcUNQ balance on Astar after XCM is: ${xcUNQbalance}`); @@ -1427,7 +1575,7 @@ const astarSovereignAccount = helper.address.paraSiblingSovereignAccount(ASTAR_CHAIN); await helper.getSudo().balance.setBalanceSubstrate(alice, astarSovereignAccount, astarBalance); - const moreThanShidenHas = astarBalance * 2n; + const moreThanAstarHas = astarBalance * 2n; let targetAccountBalance = 0n; const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice); @@ -1449,7 +1597,7 @@ interior: 'Here', }, }, - moreThanShidenHas, + moreThanAstarHas, ); // Try to trick Unique @@ -1458,11 +1606,13 @@ }); const maxWaitBlocks = 3; + const outcomeField = 1; - const xcmpQueueFailEvent = await helper.wait.eventOutcome( + const xcmpQueueFailEvent = await helper.wait.eventData( maxWaitBlocks, 'xcmpQueue', 'Fail', + outcomeField, ); expect( @@ -1501,7 +1651,7 @@ expect(targetAccountBalance).to.be.equal(validTransferAmount); }); - itSub('Should not accept reserve transfer of UNQ from Astar', async ({helper}) => { + itSub.only('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); @@ -1516,7 +1666,7 @@ }, }; - const maliciousXcmProgram = helper.arrange.makeXcmProgramReserveAssetDeposited( + const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited( targetAccount.addressRaw, { Concrete: { @@ -1531,16 +1681,55 @@ testAmount, ); + const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + testAmount, + ); + + // Try to trick Unique using full UNQ identification await usingAstarPlaygrounds(astarUrl, async (helper) => { - await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgram); + await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgramFullId); }); const maxWaitBlocks = 3; + const outcomeField = 1; + + let xcmpQueueFailEvent = await helper.wait.eventData( + maxWaitBlocks, + 'xcmpQueue', + 'Fail', + outcomeField, + ); + + expect( + xcmpQueueFailEvent != null, + '\'xcmpQueue.FailEvent\' event is expected', + ).to.be.true; + + expect( + xcmpQueueFailEvent!.isUntrustedReserveLocation, + 'The XCM error should be \'isUntrustedReserveLocation\'', + ).to.be.true; - const xcmpQueueFailEvent = await helper.wait.eventOutcome( + let accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); + + // Try to trick Unique using shortened UNQ identification + await usingAstarPlaygrounds(astarUrl, async (helper) => { + await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgramHereId); + }); + + xcmpQueueFailEvent = await helper.wait.eventData( maxWaitBlocks, 'xcmpQueue', 'Fail', + outcomeField, ); expect( @@ -1553,7 +1742,7 @@ 'The XCM error should be \'isUntrustedReserveLocation\'', ).to.be.true; - const accountBalance = await helper.balance.getSubstrate(targetAccount.address); + accountBalance = await helper.balance.getSubstrate(targetAccount.address); expect(accountBalance).to.be.equal(0n); }); -- gitstuff