--- a/tests/package.json +++ b/tests/package.json @@ -112,6 +112,7 @@ "testCollators": "RUN_COLLATOR_TESTS=1 yarn _test ./**/collator-selection/**.*test.ts --timeout 49999999", "testCollatorSelection": "RUN_COLLATOR_TESTS=1 yarn _test ./**/collatorSelection.*test.ts --timeout 49999999", "testIdentity": "RUN_COLLATOR_TESTS=1 yarn _test ./**/identity.*test.ts --timeout 49999999", + "testLowLevelXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/lowLevelXcmUnique.test.ts", "testXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmUnique.test.ts", "testXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmQuartz.test.ts", "testXcmOpal": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmOpal.test.ts", @@ -154,4 +155,4 @@ }, "type": "module", "packageManager": "yarn@3.6.1" -} +} \ No newline at end of file --- a/tests/src/util/playgrounds/unique.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -458,9 +458,9 @@ session: SessionGroup; testUtils: TestUtilGroup; foreignAssets: ForeignAssetsGroup; - xcm: XcmGroup; - xTokens: XTokensGroup; - tokens: TokensGroup; + xcm: XcmGroup; + xTokens: XTokensGroup; + tokens: TokensGroup; scheduler: SchedulerGroup; collatorSelection: CollatorSelectionGroup; council: ICollectiveGroup; @@ -627,10 +627,10 @@ super(logger, options); this.wait = new WaitGroup(this); } - getSudo() { + getSudo() { // eslint-disable-next-line @typescript-eslint/naming-convention const SudoHelperType = SudoHelper(this.helperBase); - return this.clone(SudoHelperType) as T; + return this.clone(SudoHelperType) as DevAcalaHelper; } } @@ -643,10 +643,10 @@ this.wait = new WaitGroup(this); } - getSudo() { + getSudo() { // eslint-disable-next-line @typescript-eslint/naming-convention const SudoHelperType = SudoHelper(this.helperBase); - return this.clone(SudoHelperType) as T; + return this.clone(SudoHelperType) as DevPolkadexHelper; } } @@ -1290,7 +1290,8 @@ } else if(maxBlocksToWait > 0) { maxBlocksToWait--; } else { - this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found`); + this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found. + The wait lasted until block ${blockNumber} inclusive`); unsubscribe(); resolve(null); } --- /dev/null +++ b/tests/src/xcm/lowLevelXcmUnique.test.ts @@ -0,0 +1,775 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {IKeyringPair} from '@polkadot/types/types'; +import config from '../config'; +import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds} from '../util'; +import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev'; +import {nToBigInt} from '@polkadot/util'; +import {hexToString} from '@polkadot/util'; + +const UNIQUE_CHAIN = +(process.env.RELAY_UNIQUE_ID || 2037); +const ACALA_CHAIN = +(process.env.RELAY_ACALA_ID || 2000); +const MOONBEAM_CHAIN = +(process.env.RELAY_MOONBEAM_ID || 2004); +const ASTAR_CHAIN = +(process.env.RELAY_ASTAR_ID || 2006); +const POLKADEX_CHAIN = +(process.env.RELAY_POLKADEX_ID || 2040); + + + +const acalaUrl = config.acalaUrl; +const moonbeamUrl = config.moonbeamUrl; +const astarUrl = config.astarUrl; +const polkadexUrl = config.polkadexUrl; + +const ASTAR_DECIMALS = 18n; +const UNQ_DECIMALS = 18n; + +const TRANSFER_AMOUNT = 2000000_000_000_000_000_000_000n; +const SENDER_BUDGET = 2n * TRANSFER_AMOUNT; +const SENDBACK_AMOUNT = TRANSFER_AMOUNT / 2n; +const STAYED_ON_TARGET_CHAIN = TRANSFER_AMOUNT - SENDBACK_AMOUNT; +const TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT = 100_000_000_000n; + +const SAFE_XCM_VERSION = 2; +const maxWaitBlocks = 6; + +const uniqueMultilocation = { + V2: { + parents: 1, + interior: { + X1: { + Parachain: UNIQUE_CHAIN, + }, + }, + }, +}; + +let balanceUniqueTokenInit: bigint; +let balanceUniqueTokenMiddle: bigint; +let balanceUniqueTokenFinal: bigint; +let unqFees: bigint; + +const expectFailedToTransact = async (helper: DevUniqueHelper, messageSent: any) => { + await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash + && event.outcome.isFailedToTransactAsset); +}; +const expectUntrustedReserveLocationFail = async (helper: DevUniqueHelper, messageSent: any) => { + await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash + && event.outcome.isUntrustedReserveLocation); +}; + +const NETWORKS = { + acala: usingAcalaPlaygrounds, + astar: usingAstarPlaygrounds, + polkadex: usingPolkadexPlaygrounds, + moonbeam: usingMoonbeamPlaygrounds, +} as const; + +function mapToChainId(networkName: keyof typeof NETWORKS) { + switch (networkName) { + case 'acala': + return ACALA_CHAIN; + case 'astar': + return ASTAR_CHAIN; + case 'moonbeam': + return MOONBEAM_CHAIN; + case 'polkadex': + return POLKADEX_CHAIN; + } +} + +function mapToChainUrl(networkName: keyof typeof NETWORKS): string { + switch (networkName) { + case 'acala': + return acalaUrl; + case 'astar': + return astarUrl; + case 'moonbeam': + return moonbeamUrl; + case 'polkadex': + return polkadexUrl; + } +} + +function getDevPlayground(name: T) { + return NETWORKS[name]; +} + + +async function genericSendUnqTo( + networkName: keyof typeof NETWORKS, + randomAccount: IKeyringPair, + randomAccountOnTargetChain = randomAccount, +) { + const networkUrl = mapToChainUrl(networkName); + const targetPlayground = getDevPlayground(networkName); + await usingPlaygrounds(async (helper) => { + balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address); + const destination = { + V2: { + parents: 1, + interior: { + X1: { + Parachain: mapToChainId(networkName), + }, + }, + }, + }; + + const beneficiary = { + V2: { + parents: 0, + interior: { + X1: ( + networkName == 'moonbeam' ? + { + AccountKey20: { + network: 'Any', + key: randomAccountOnTargetChain.address, + }, + } + : + { + AccountId32: { + network: 'Any', + id: randomAccountOnTargetChain.addressRaw, + }, + } + ), + }, + }, + }; + + const assets = { + V2: [ + { + id: { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + fun: { + Fungible: TRANSFER_AMOUNT, + }, + }, + ], + }; + const feeAssetItem = 0; + + await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited'); + const messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + balanceUniqueTokenMiddle = await helper.balance.getSubstrate(randomAccount.address); + + unqFees = balanceUniqueTokenInit - balanceUniqueTokenMiddle - TRANSFER_AMOUNT; + console.log('[Unique -> %s] transaction fees on Unique: %s UNQ', networkName, helper.util.bigIntToDecimals(unqFees)); + expect(unqFees > 0n, 'Negative fees UNQ, looks like nothing was transferred').to.be.true; + + await targetPlayground(networkUrl, async (helper) => { + /* + Since only the parachain part of the Polkadex + infrastructure is launched (without their + solochain validators), processing incoming + assets will lead to an error. + This error indicates that the Polkadex chain + received a message from the Unique network, + since the hash is being checked to ensure + it matches what was sent. + */ + if(networkName == 'polkadex') { + await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash); + } else { + await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == messageSent.messageHash); + } + }); + + }); +} + +async function genericSendUnqBack( + networkName: keyof typeof NETWORKS, + sudoer: IKeyringPair, + randomAccountOnUnq: IKeyringPair, +) { + const networkUrl = mapToChainUrl(networkName); + + const targetPlayground = getDevPlayground(networkName); + await usingPlaygrounds(async (helper) => { + + const xcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + randomAccountOnUnq.addressRaw, + { + Concrete: { + parents: 1, + interior: { + X1: {Parachain: UNIQUE_CHAIN}, + }, + }, + }, + SENDBACK_AMOUNT, + ); + + let xcmProgramSent: any; + + + await targetPlayground(networkUrl, async (helper) => { + if('getSudo' in helper) { + await helper.getSudo().xcm.send(sudoer, uniqueMultilocation, xcmProgram); + xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } else if('fastDemocracy' in helper) { + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueMultilocation, xcmProgram]); + // Needed to bypass the call filter. + const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); + await helper.fastDemocracy.executeProposal('sending MoonBeam -> Unique via XCM program', batchCall); + xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } + }); + + await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == xcmProgramSent.messageHash); + + balanceUniqueTokenFinal = await helper.balance.getSubstrate(randomAccountOnUnq.address); + + expect(balanceUniqueTokenFinal).to.be.equal(balanceUniqueTokenInit - unqFees - STAYED_ON_TARGET_CHAIN); + + }); +} + +async function genericSendOnlyOwnedBalance( + networkName: keyof typeof NETWORKS, + sudoer: IKeyringPair, +) { + const networkUrl = mapToChainUrl(networkName); + const targetPlayground = getDevPlayground(networkName); + + const targetChainBalance = 10000n * (10n ** UNQ_DECIMALS); + + await usingPlaygrounds(async (helper) => { + const targetChainSovereignAccount = helper.address.paraSiblingSovereignAccount(mapToChainId(networkName)); + await helper.getSudo().balance.setBalanceSubstrate(sudoer, targetChainSovereignAccount, targetChainBalance); + const moreThanTargetChainHas = 2n * targetChainBalance; + + const targetAccount = helper.arrange.createEmptyAccount(); + + const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + moreThanTargetChainHas, + ); + + let maliciousXcmProgramSent: any; + + + await targetPlayground(networkUrl, async (helper) => { + if('getSudo' in helper) { + await helper.getSudo().xcm.send(sudoer, uniqueMultilocation, maliciousXcmProgram); + maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } else if('fastDemocracy' in 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('sending MoonBeam -> Unique via XCM program', batchCall); + maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } + }); + + await expectFailedToTransact(helper, maliciousXcmProgramSent); + + const targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(targetAccountBalance).to.be.equal(0n); + }); +} + +async function genericReserveTransferUNQfrom(netwokrName: keyof typeof NETWORKS, sudoer: IKeyringPair) { + const networkUrl = mapToChainUrl(netwokrName); + const targetPlayground = getDevPlayground(netwokrName); + + await usingPlaygrounds(async (helper) => { + const testAmount = 10_000n * (10n ** UNQ_DECIMALS); + const targetAccount = helper.arrange.createEmptyAccount(); + + const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 1, + interior: { + X1: { + Parachain: UNIQUE_CHAIN, + }, + }, + }, + }, + testAmount, + ); + + const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited( + targetAccount.addressRaw, + { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + testAmount, + ); + + let maliciousXcmProgramFullIdSent: any; + let maliciousXcmProgramHereIdSent: any; + const maxWaitBlocks = 3; + + // Try to trick Unique using full UNQ identification + await targetPlayground(networkUrl, async (helper) => { + if('getSudo' in helper) { + await helper.getSudo().xcm.send(sudoer, uniqueMultilocation, maliciousXcmProgramFullId); + maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } + // Moonbeam case + else if('fastDemocracy' in 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); + + maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } + }); + + + await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramFullIdSent); + + let accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); + + // Try to trick Unique using shortened UNQ identification + await targetPlayground(networkUrl, async (helper) => { + if('getSudo' in helper) { + await helper.getSudo().xcm.send(sudoer, uniqueMultilocation, maliciousXcmProgramHereId); + maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } + else if('fastDemocracy' in 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); + + maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } + }); + + await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramHereIdSent); + + accountBalance = await helper.balance.getSubstrate(targetAccount.address); + expect(accountBalance).to.be.equal(0n); + }); +} + +async function genericRejectNativeToknsFrom(netwokrName: keyof typeof NETWORKS, sudoerOnTargetChain: IKeyringPair) { + const networkUrl = mapToChainUrl(netwokrName); + const targetPlayground = getDevPlayground(netwokrName); + let messageSent: any; + + await usingPlaygrounds(async (helper) => { + const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited( + helper.arrange.createEmptyAccount().addressRaw, + { + Concrete: { + parents: 1, + interior: { + X1: { + Parachain: mapToChainId(netwokrName), + }, + }, + }, + }, + TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT, + ); + await targetPlayground(networkUrl, async (helper) => { + if('getSudo' in helper) { + await helper.getSudo().xcm.send(sudoerOnTargetChain, uniqueMultilocation, maliciousXcmProgramFullId); + messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } else if('fastDemocracy' in 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('sending native tokens to the Unique via fast democracy', batchCall); + + messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } + }); + + await expectFailedToTransact(helper, messageSent); + }); +} + + +describeXCM('[XCMLL] Integration test: Exchanging tokens with Acala', () => { + let alice: IKeyringPair; + let randomAccount: IKeyringPair; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + console.log(config.acalaUrl); + randomAccount = helper.arrange.createEmptyAccount(); + + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + + await usingAcalaPlaygrounds(acalaUrl, async (helper) => { + const destination = { + V2: { + parents: 1, + interior: { + X1: { + Parachain: UNIQUE_CHAIN, + }, + }, + }, + }; + + const metadata = { + name: 'Unique Network', + symbol: 'UNQ', + decimals: 18, + minimalBalance: 1250_000_000_000_000_000n, + }; + const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v] : [any, any]) => + hexToString(v.toJSON()['symbol'])) as string[]; + + if(!assets.includes('UNQ')) { + await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata); + } else { + console.log('UNQ token already registered on Acala assetRegistry pallet'); + } + await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n); + }); + + await usingPlaygrounds(async (helper) => { + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address); + }); + }); + + itSub('Should connect and send UNQ to Acala', async () => { + await genericSendUnqTo('acala', randomAccount); + }); + + itSub('Should connect to Acala and send UNQ back', async () => { + await genericSendUnqBack('acala', alice, randomAccount); + }); + + itSub('Acala can send only up to its balance', async () => { + await genericSendOnlyOwnedBalance('acala', alice); + }); + + itSub('Should not accept reserve transfer of UNQ from Acala', async () => { + await genericReserveTransferUNQfrom('acala', alice); + }); +}); + +describeXCM('[XCMLL] Integration test: Exchanging tokens with Polkadex', () => { + let alice: IKeyringPair; + let randomAccount: IKeyringPair; + + const uniqueAssetId = { + Concrete: { + parents: 1, + interior: { + X1: { + Parachain: UNIQUE_CHAIN, + }, + }, + }, + }; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + randomAccount = helper.arrange.createEmptyAccount(); + + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + + await usingPolkadexPlaygrounds(polkadexUrl, async (helper) => { + const isWhitelisted = ((await helper.callRpc('api.query.xcmHelper.whitelistedTokens', [])) + .toJSON() as []) + .map(nToBigInt).length != 0; + /* + Check whether the Unique token has been added + to the whitelist, since an error will occur + if it is added again. Needed for debugging + when this test is run multiple times. + */ + if(!isWhitelisted) { + await helper.getSudo().xcmHelper.whitelistToken(alice, uniqueAssetId); + } + + await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n); + }); + + await usingPlaygrounds(async (helper) => { + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address); + }); + }); + + itSub('Should connect and send UNQ to Polkadex', async () => { + await genericSendUnqTo('polkadex', randomAccount); + }); + + + itSub('Should connect to Polkadex and send UNQ back', async () => { + await genericSendUnqBack('polkadex', alice, randomAccount); + }); + + itSub('Polkadex can send only up to its balance', async () => { + await genericSendOnlyOwnedBalance('polkadex', alice); + }); + + itSub('Should not accept reserve transfer of UNQ from Polkadex', async () => { + await genericReserveTransferUNQfrom('polkadex', alice); + }); +}); + +// These tests are relevant only when +// the the corresponding foreign assets are not registered +describeXCM('[XCMLL] Integration test: Unique rejects non-native tokens', () => { + let alice: IKeyringPair; + + 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); + }); + }); + + itSub('Unique rejects ACA tokens from Acala', async () => { + await genericRejectNativeToknsFrom('acala', alice); + }); + + itSub('Unique rejects GLMR tokens from Moonbeam', async () => { + await genericRejectNativeToknsFrom('moonbeam', alice); + }); + + itSub('Unique rejects ASTR tokens from Astar', async () => { + await genericRejectNativeToknsFrom('astar', alice); + }); + + itSub('Unique rejects PDX tokens from Polkadex', async () => { + await genericRejectNativeToknsFrom('polkadex', alice); + }); +}); + +describeXCM('[XCMLL] Integration test: Exchanging UNQ with Moonbeam', () => { + // Unique constants + let alice: IKeyringPair; + let uniqueAssetLocation; + + let randomAccountUnique: IKeyringPair; + let randomAccountMoonbeam: IKeyringPair; + + // Moonbeam constants + let assetId: string; + + const uniqueAssetMetadata = { + name: 'xcUnique', + symbol: 'xcUNQ', + decimals: 18, + isFrozen: false, + minimalBalance: 1n, + }; + + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + [randomAccountUnique] = await helper.arrange.createAccounts([0n], alice); + + + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + + await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => { + const alithAccount = helper.account.alithAccount(); + const baltatharAccount = helper.account.baltatharAccount(); + const dorothyAccount = helper.account.dorothyAccount(); + + randomAccountMoonbeam = helper.account.create(); + + // >>> Sponsoring Dorothy >>> + console.log('Sponsoring Dorothy.......'); + await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n); + console.log('Sponsoring Dorothy.......DONE'); + // <<< Sponsoring Dorothy <<< + uniqueAssetLocation = { + XCM: { + parents: 1, + interior: {X1: {Parachain: UNIQUE_CHAIN}}, + }, + }; + const existentialDeposit = 1n; + const isSufficient = true; + const unitsPerSecond = 1n; + const numAssetsWeightHint = 0; + + if((await helper.assetManager.assetTypeId(uniqueAssetLocation)).toJSON()) { + console.log('Unique asset already registered'); + } else { + const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({ + location: uniqueAssetLocation, + metadata: uniqueAssetMetadata, + existentialDeposit, + isSufficient, + unitsPerSecond, + numAssetsWeightHint, + }); + + console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal); + + await helper.fastDemocracy.executeProposal('register UNQ foreign asset', encodedProposal); + + // >>> Acquire Unique AssetId Info on Moonbeam >>> + console.log('Acquire Unique AssetId Info on Moonbeam.......'); + + assetId = (await helper.assetManager.assetTypeId(uniqueAssetLocation)).toString(); + + console.log('UNQ asset ID is %s', assetId); + console.log('Acquire Unique AssetId Info on Moonbeam.......DONE'); + } + // >>> Acquire Unique AssetId Info on Moonbeam >>> + + // >>> Sponsoring random Account >>> + console.log('Sponsoring random Account.......'); + await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonbeam.address, 11_000_000_000_000_000_000n); + console.log('Sponsoring random Account.......DONE'); + // <<< Sponsoring random Account <<< + }); + + await usingPlaygrounds(async (helper) => { + await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET); + balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccountUnique.address); + }); + }); + + itSub('Should connect and send UNQ to Moonbeam', async () => { + await genericSendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam); + }); + + itSub('Should connect to Moonbeam and send UNQ back', async () => { + await genericSendUnqBack('moonbeam', alice, randomAccountUnique); + }); + + itSub('Moonbeam can send only up to its balance', async () => { + await genericSendOnlyOwnedBalance('moonbeam', alice); + }); + + itSub('Should not accept reserve transfer of UNQ from Moonbeam', async () => { + await genericReserveTransferUNQfrom('moonbeam', alice); + }); +}); + +describeXCM('[XCMLL] Integration test: Exchanging tokens with Astar', () => { + let alice: IKeyringPair; + let randomAccount: IKeyringPair; + + 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 Astar. + const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours? + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + randomAccount = helper.arrange.createEmptyAccount(); + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + console.log('randomAccount', randomAccount.address); + + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + + await usingAstarPlaygrounds(astarUrl, async (helper) => { + if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) { + console.log('1. Create foreign asset and metadata'); + // TODO update metadata with values from production + await helper.assets.create( + alice, + UNQ_ASSET_ID_ON_ASTAR, + alice.address, + UNQ_MINIMAL_BALANCE_ON_ASTAR, + ); + + await helper.assets.setMetadata( + alice, + UNQ_ASSET_ID_ON_ASTAR, + 'Cross chain UNQ', + 'xcUNQ', + Number(UNQ_DECIMALS), + ); + + console.log('2. Register asset location on Astar'); + const assetLocation = { + V2: { + parents: 1, + interior: { + X1: { + Parachain: UNIQUE_CHAIN, + }, + }, + }, + }; + + 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]); + } + console.log('4. Transfer 1 ASTR to recipient to create the account (needed due to existential balance)'); + await helper.balance.transferToSubstrate(alice, randomAccount.address, astarInitialBalance); + }); + }); + + itSub('Should connect and send UNQ to Astar', async () => { + await genericSendUnqTo('astar', randomAccount); + }); + + itSub('Should connect to Astar and send UNQ back', async () => { + await genericSendUnqBack('astar', alice, randomAccount); + }); + + itSub('Astar can send only up to its balance', async () => { + await genericSendOnlyOwnedBalance('astar', alice); + }); + + itSub('Should not accept reserve transfer of UNQ from Astar', async () => { + await genericReserveTransferUNQfrom('astar', alice); + }); +}); --- a/tests/src/xcm/xcmUnique.test.ts +++ b/tests/src/xcm/xcmUnique.test.ts @@ -18,7 +18,7 @@ import config from '../config'; import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingRelayPlaygrounds, usingMoonbeamPlaygrounds, usingStatemintPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds} from '../util'; import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev'; -import {nToBigInt} from '@polkadot/util'; +import {hexToString, nToBigInt} from '@polkadot/util'; const UNIQUE_CHAIN = +(process.env.RELAY_UNIQUE_ID || 2037); const STATEMINT_CHAIN = +(process.env.RELAY_STATEMINT_ID || 1000); @@ -57,6 +57,18 @@ const SAFE_XCM_VERSION = 2; const maxWaitBlocks = 6; + +const uniqueMultilocation = { + V2: { + parents: 1, + interior: { + X1: { + Parachain: UNIQUE_CHAIN, + }, + }, + }, +}; + const expectFailedToTransact = async (helper: DevUniqueHelper, messageSent: any) => { await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash && event.outcome.isFailedToTransactAsset); @@ -509,11 +521,15 @@ decimals: 18, minimalBalance: 1250000000000000000n, }; + const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v] : [any, any]) => + hexToString(v.toJSON()['symbol'])) as string[]; - await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata); + if(!assets.includes('UNQ')) { + await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata); + } else { + console.log('UNQ token already registered on Acala assetRegistry pallet'); + } await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n); - balanceAcalaTokenInit = await helper.balance.getSubstrate(randomAccount.address); - balanceUniqueForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0}); }); await usingPlaygrounds(async (helper) => { @@ -669,15 +685,6 @@ let targetAccountBalance = 0n; const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice); - - const uniqueMultilocation = { - V2: { - parents: 1, - interior: { - X1: {Parachain: UNIQUE_CHAIN}, - }, - }, - }; const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( targetAccount.addressRaw, @@ -925,15 +932,6 @@ itSub('Should connect to Polkadex and send UNQ back', async ({helper}) => { - const uniqueMultilocation = { - V2: { - parents: 1, - interior: { - X1: {Parachain: UNIQUE_CHAIN}, - }, - }, - }; - const xcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( randomAccount.addressRaw, { @@ -971,15 +969,6 @@ const targetAccount = helper.arrange.createEmptyAccount(); - const uniqueMultilocation = { - V2: { - parents: 1, - interior: { - X1: {Parachain: UNIQUE_CHAIN}, - }, - }, - }; - const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( targetAccount.addressRaw, { @@ -1304,26 +1293,30 @@ const unitsPerSecond = 1n; const numAssetsWeightHint = 0; - const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({ - location: uniqueAssetLocation, - metadata: uniqueAssetMetadata, - existentialDeposit, - isSufficient, - unitsPerSecond, - numAssetsWeightHint, - }); + if((await helper.assetManager.assetTypeId(uniqueAssetLocation)).toJSON) { + console.log('Unique asset is already registered on MoonBeam'); + } else { + const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({ + location: uniqueAssetLocation, + metadata: uniqueAssetMetadata, + existentialDeposit, + isSufficient, + unitsPerSecond, + numAssetsWeightHint, + }); - console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal); + console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal); - await helper.fastDemocracy.executeProposal('register UNQ foreign asset', encodedProposal); + await helper.fastDemocracy.executeProposal('register UNQ foreign asset', encodedProposal); - // >>> Acquire Unique AssetId Info on Moonbeam >>> - console.log('Acquire Unique AssetId Info on Moonbeam.......'); + // >>> Acquire Unique AssetId Info on Moonbeam >>> + console.log('Acquire Unique AssetId Info on Moonbeam.......'); - assetId = (await helper.assetManager.assetTypeId(uniqueAssetLocation)).toString(); + assetId = (await helper.assetManager.assetTypeId(uniqueAssetLocation)).toString(); - console.log('UNQ asset ID is %s', assetId); - console.log('Acquire Unique AssetId Info on Moonbeam.......DONE'); + console.log('UNQ asset ID is %s', assetId); + console.log('Acquire Unique AssetId Info on Moonbeam.......DONE'); + } // >>> Acquire Unique AssetId Info on Moonbeam >>> // >>> Sponsoring random Account >>> @@ -1455,15 +1448,6 @@ let targetAccountBalance = 0n; const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice); - - const uniqueMultilocation = { - V2: { - parents: 1, - interior: { - X1: {Parachain: UNIQUE_CHAIN}, - }, - }, - }; const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( targetAccount.addressRaw, @@ -1527,17 +1511,6 @@ const testAmount = 10_000n * (10n ** UNQ_DECIMALS); const [targetAccount] = await helper.arrange.createAccounts([0n], alice); - const uniqueMultilocation = { - V2: { - parents: 1, - interior: { - X1: { - Parachain: UNIQUE_CHAIN, - }, - }, - }, - }; - const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited( targetAccount.addressRaw, { @@ -1634,40 +1607,41 @@ }); await usingAstarPlaygrounds(astarUrl, async (helper) => { - console.log('1. Create foreign asset and metadata'); - // TODO update metadata with values from production - await helper.assets.create( - alice, - UNQ_ASSET_ID_ON_ASTAR, - alice.address, - UNQ_MINIMAL_BALANCE_ON_ASTAR, - ); + if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) { + console.log('1. Create foreign asset and metadata'); + // TODO update metadata with values from production + await helper.assets.create( + alice, + UNQ_ASSET_ID_ON_ASTAR, + alice.address, + UNQ_MINIMAL_BALANCE_ON_ASTAR, + ); - await helper.assets.setMetadata( - alice, - UNQ_ASSET_ID_ON_ASTAR, - 'Cross chain UNQ', - 'xcUNQ', - Number(UNQ_DECIMALS), - ); + await helper.assets.setMetadata( + alice, + UNQ_ASSET_ID_ON_ASTAR, + 'Cross chain UNQ', + 'xcUNQ', + Number(UNQ_DECIMALS), + ); - console.log('2. Register asset location on Astar'); - const assetLocation = { - V2: { - parents: 1, - interior: { - X1: { - Parachain: UNIQUE_CHAIN, + console.log('2. Register asset location on Astar'); + const assetLocation = { + V2: { + parents: 1, + interior: { + X1: { + Parachain: UNIQUE_CHAIN, + }, }, }, - }, - }; - - 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]); + 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]); + } console.log('4. Transfer 1 ASTR to recipient to create the account (needed due to existential balance)'); await helper.balance.transferToSubstrate(alice, randomAccount.address, astarInitialBalance); }); @@ -1824,15 +1798,6 @@ let targetAccountBalance = 0n; const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice); - - const uniqueMultilocation = { - V2: { - parents: 1, - interior: { - X1: {Parachain: UNIQUE_CHAIN}, - }, - }, - }; const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit( targetAccount.addressRaw, @@ -1887,17 +1852,6 @@ 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 = { - V2: { - parents: 1, - interior: { - X1: { - Parachain: UNIQUE_CHAIN, - }, - }, - }, - }; const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited( targetAccount.addressRaw,