From 668280b1206de9640cc6971283f55bc921a1854d Mon Sep 17 00:00:00 2001 From: PraetorP Date: Fri, 29 Sep 2023 11:32:27 +0000 Subject: [PATCH] feat(xcm tests): `XcmHelper` integraion for QTZ\UNQ --- --- a/.baedeker/.gitignore +++ b/.baedeker/.gitignore @@ -1,4 +1,5 @@ /.bdk-env -/rewrites.jsonnet +/rewrites*.jsonnet /vendor /baedeker-library +!/rewrites.example.jsonnet \ No newline at end of file --- a/.github/workflows/xcm.yml +++ b/.github/workflows/xcm.yml @@ -38,7 +38,7 @@ with: matrix: | network {opal}, relay_branch {${{ env.UNIQUEWEST_MAINNET_BRANCH }}}, acala_version {${{ env.ACALA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONBEAM_BUILD_BRANCH }}}, cumulus_version {${{ env.WESTMINT_BUILD_BRANCH }}}, astar_version {${{ env.ASTAR_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testXcmOpal}, runtime_features {opal-runtime} - network {quartz}, relay_branch {${{ env.KUSAMA_MAINNET_BRANCH }}}, acala_version {${{ env.KARURA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONRIVER_BUILD_BRANCH }}}, cumulus_version {${{ env.STATEMINE_BUILD_BRANCH }}}, astar_version {${{ env.SHIDEN_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testXcmQuartz}, runtime_features {quartz-runtime} + network {quartz}, relay_branch {${{ env.KUSAMA_MAINNET_BRANCH }}}, acala_version {${{ env.KARURA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONRIVER_BUILD_BRANCH }}}, cumulus_version {${{ env.STATEMINE_BUILD_BRANCH }}}, astar_version {${{ env.SHIDEN_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testFullXcmQuartz}, runtime_features {quartz-runtime} network {unique}, relay_branch {${{ env.POLKADOT_MAINNET_BRANCH }}}, acala_version {${{ env.ACALA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONBEAM_BUILD_BRANCH }}}, cumulus_version {${{ env.STATEMINT_BUILD_BRANCH }}}, astar_version {${{ env.ASTAR_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testFullXcmUnique}, runtime_features {unique-runtime} xcm: --- a/tests/src/xcm/lowLevelXcmQuartz.test.ts +++ b/tests/src/xcm/lowLevelXcmQuartz.test.ts @@ -15,35 +15,16 @@ // along with Unique Network. If not, see . import {IKeyringPair} from '@polkadot/types/types'; -import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingMoonriverPlaygrounds, usingShidenPlaygrounds, usingRelayPlaygrounds} from '../util'; -import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev'; -import {STATEMINE_CHAIN, QUARTZ_CHAIN, KARURA_CHAIN, MOONRIVER_CHAIN, SHIDEN_CHAIN, STATEMINE_DECIMALS, KARURA_DECIMALS, QTZ_DECIMALS, RELAY_DECIMALS, SHIDEN_DECIMALS, karuraUrl, moonriverUrl, relayUrl, shidenUrl, statemineUrl, SAFE_XCM_VERSION, XcmTestHelper, TRANSFER_AMOUNT} from './xcm.types'; - +import {itSub, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingMoonriverPlaygrounds, usingShidenPlaygrounds, usingRelayPlaygrounds} from '../util'; +import {QUARTZ_CHAIN, QTZ_DECIMALS, SHIDEN_DECIMALS, karuraUrl, moonriverUrl, shidenUrl, SAFE_XCM_VERSION, XcmTestHelper, TRANSFER_AMOUNT, SENDER_BUDGET, relayUrl} from './xcm.types'; +import {hexToString} from '@polkadot/util'; const testHelper = new XcmTestHelper('quartz'); describeXCM('[XCMLL] Integration test: Exchanging tokens with Karura', () => { let alice: IKeyringPair; let randomAccount: IKeyringPair; - - let balanceQuartzTokenInit: bigint; - let balanceQuartzTokenMiddle: bigint; - let balanceQuartzTokenFinal: bigint; - let balanceKaruraTokenInit: bigint; - let balanceKaruraTokenMiddle: bigint; - let balanceKaruraTokenFinal: bigint; - let balanceQuartzForeignTokenInit: bigint; - let balanceQuartzForeignTokenMiddle: bigint; - let balanceQuartzForeignTokenFinal: bigint; - // computed by a test transfer from prod Quartz to prod Karura. - // 2 QTZ sent https://quartz.subscan.io/xcm_message/kusama-f60d821b049f8835a3005ce7102285006f5b61e9 - // 1.919176000000000000 QTZ received (you can check Karura's chain state in the corresponding block) - const expectedKaruraIncomeFee = 2000000000000000000n - 1919176000000000000n; - const karuraEps = 8n * 10n ** 16n; - - let karuraBackwardTransferAmount: bigint; - before(async () => { await usingPlaygrounds(async (helper, privateKey) => { alice = await privateKey('//Alice'); @@ -72,15 +53,19 @@ minimalBalance: 1000000000000000000n, }; - await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata); + const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v]: [any, any]) => + hexToString(v.toJSON()['symbol'])) as string[]; + + if(!assets.includes('QTZ')) { + await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata); + } else { + console.log('QTZ token already registered on Karura assetRegistry pallet'); + } await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n); - balanceKaruraTokenInit = await helper.balance.getSubstrate(randomAccount.address); - balanceQuartzForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0}); }); await usingPlaygrounds(async (helper) => { - await helper.balance.transferToSubstrate(alice, randomAccount.address, 10n * TRANSFER_AMOUNT); - balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccount.address); + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); }); }); @@ -100,67 +85,17 @@ // the the corresponding foreign assets are not registered describeXCM('[XCMLL] 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; - - let messageSent: any; - - const maxWaitBlocks = 3; - before(async () => { await usingPlaygrounds(async (helper, privateKey) => { alice = await privateKey('//Alice'); - quartzParachainJunction = {Parachain: QUARTZ_CHAIN}; - quartzAccountJunction = { - AccountId32: { - network: 'Any', - id: alice.addressRaw, - }, - }; - - quartzParachainMultilocation = { - V2: { - parents: 1, - interior: { - X1: quartzParachainJunction, - }, - }, - }; - quartzAccountMultilocation = { - V2: { - parents: 0, - interior: { - X1: quartzAccountJunction, - }, - }, - }; - quartzCombinedMultilocation = { - V2: { - parents: 1, - interior: { - X2: [quartzParachainJunction, quartzAccountJunction], - }, - }, - }; - // 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(); - }); }); itSub('Quartz rejects KAR tokens from Karura', async () => { @@ -195,22 +130,12 @@ minimalBalance: 1n, }; - let balanceQuartzTokenInit: bigint; - let balanceQuartzTokenMiddle: bigint; - let balanceQuartzTokenFinal: bigint; - let balanceForeignQtzTokenInit: bigint; - let balanceForeignQtzTokenMiddle: bigint; - let balanceForeignQtzTokenFinal: bigint; - let balanceMovrTokenInit: bigint; - let balanceMovrTokenMiddle: bigint; - let balanceMovrTokenFinal: bigint; before(async () => { await usingPlaygrounds(async (helper, privateKey) => { 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. await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); @@ -239,20 +164,22 @@ const isSufficient = true; const unitsPerSecond = 1n; const numAssetsWeightHint = 0; + if((await helper.assetManager.assetTypeId(quartzAssetLocation)).toJSON()) { + console.log('Quartz asset already registered on Moonriver'); + } else { + const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({ + location: quartzAssetLocation, + metadata: quartzAssetMetadata, + existentialDeposit, + isSufficient, + unitsPerSecond, + numAssetsWeightHint, + }); - const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({ - location: quartzAssetLocation, - metadata: quartzAssetMetadata, - existentialDeposit, - isSufficient, - unitsPerSecond, - numAssetsWeightHint, - }); - - console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal); - - await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal); + console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal); + await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal); + } // >>> Acquire Quartz AssetId Info on Moonriver >>> console.log('Acquire Quartz AssetId Info on Moonriver.......'); @@ -267,13 +194,10 @@ await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonriver.address, 11_000_000_000_000_000_000n); console.log('Sponsoring random Account.......DONE'); // <<< Sponsoring random Account <<< - - balanceMovrTokenInit = await helper.balance.getEthereum(randomAccountMoonriver.address); }); await usingPlaygrounds(async (helper) => { await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT); - balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccountQuartz.address); }); }); @@ -296,7 +220,7 @@ describeXCM('[XCMLL] Integration test: Exchanging tokens with Shiden', () => { let alice: IKeyringPair; - let sender: IKeyringPair; + let randomAccount: IKeyringPair; const QTZ_ASSET_ID_ON_SHIDEN = 1; const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n; @@ -304,71 +228,69 @@ // Quartz -> Shiden const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours? - const qtzToShidenTransferred = 10n * (10n ** QTZ_DECIMALS); // 10 QTZ - const qtzToShidenArrived = 9_999_999_999_088_000_000n; // 9.999 ... QTZ, Shiden takes a commision in foreign tokens - // Shiden -> Quartz - const qtzFromShidenTransfered = 5n * (10n ** QTZ_DECIMALS); // 5 QTZ - const qtzOnShidenLeft = qtzToShidenArrived - qtzFromShidenTransfered; // 4.999_999_999_088_000_000n QTZ - let balanceAfterQuartzToShidenXCM: bigint; before(async () => { await usingPlaygrounds(async (helper, privateKey) => { alice = await privateKey('//Alice'); - [sender] = await helper.arrange.createAccounts([100n], alice); - console.log('sender', sender.address); + randomAccount = helper.arrange.createEmptyAccount(); + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + console.log('sender: ', randomAccount.address); // Set the default version to wrap the first message to other chains. await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); }); await usingShidenPlaygrounds(shidenUrl, async (helper) => { - console.log('1. Create foreign asset and metadata'); - // TODO update metadata with values from production - await helper.assets.create( - alice, - QTZ_ASSET_ID_ON_SHIDEN, - alice.address, - QTZ_MINIMAL_BALANCE_ON_SHIDEN, - ); + if(!(await helper.callRpc('api.query.assets.asset', [QTZ_ASSET_ID_ON_SHIDEN])).toJSON()) { + console.log('1. Create foreign asset and metadata'); + // TODO update metadata with values from production + await helper.assets.create( + alice, + QTZ_ASSET_ID_ON_SHIDEN, + alice.address, + QTZ_MINIMAL_BALANCE_ON_SHIDEN, + ); - await helper.assets.setMetadata( - alice, - QTZ_ASSET_ID_ON_SHIDEN, - 'Cross chain QTZ', - 'xcQTZ', - Number(QTZ_DECIMALS), - ); + await helper.assets.setMetadata( + alice, + QTZ_ASSET_ID_ON_SHIDEN, + 'Cross chain QTZ', + 'xcQTZ', + Number(QTZ_DECIMALS), + ); - console.log('2. Register asset location on Shiden'); - const assetLocation = { - V2: { - parents: 1, - interior: { - X1: { - Parachain: QUARTZ_CHAIN, + console.log('2. Register asset location on Shiden'); + const assetLocation = { + V2: { + parents: 1, + interior: { + X1: { + Parachain: QUARTZ_CHAIN, + }, }, }, - }, - }; - - await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]); + }; - console.log('3. Set QTZ payment for XCM execution on Shiden'); - await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]); + await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]); + console.log('3. Set QTZ payment for XCM execution on Shiden'); + await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]); + } else { + console.log('QTZ is already registered on Shiden'); + } console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)'); - await helper.balance.transferToSubstrate(alice, sender.address, shidenInitialBalance); + await helper.balance.transferToSubstrate(alice, randomAccount.address, shidenInitialBalance); }); }); itSub('Should connect and send QTZ to Shiden', async () => { - await testHelper.sendUnqTo('shiden', sender); + await testHelper.sendUnqTo('shiden', randomAccount); }); itSub('Should connect to Shiden and send QTZ back', async () => { - await testHelper.sendUnqBack('shiden', alice, sender); + await testHelper.sendUnqBack('shiden', alice, randomAccount); }); itSub('Shiden can send only up to its balance', async () => { --- a/tests/src/xcm/lowLevelXcmUnique.test.ts +++ b/tests/src/xcm/lowLevelXcmUnique.test.ts @@ -16,320 +16,16 @@ import {IKeyringPair} from '@polkadot/types/types'; import config from '../config'; -import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds, requirePalletsOrSkip, Pallets} from '../util'; -import {Event} from '../util/playgrounds/unique.dev'; +import {itSub, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds} from '../util'; import {nToBigInt} from '@polkadot/util'; import {hexToString} from '@polkadot/util'; -import {ASTAR_DECIMALS, NETWORKS, SAFE_XCM_VERSION, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper, acalaUrl, astarUrl, expectFailedToTransact, expectUntrustedReserveLocationFail, getDevPlayground, mapToChainId, mapToChainUrl, maxWaitBlocks, moonbeamUrl, polkadexUrl, relayUrl, uniqueAssetId, uniqueVersionedMultilocation} from './xcm.types'; - - -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; - -let balanceUniqueTokenInit: bigint; -let balanceUniqueTokenMiddle: bigint; -let balanceUniqueTokenFinal: bigint; -let unqFees: bigint; +import {ASTAR_DECIMALS, SAFE_XCM_VERSION, SENDER_BUDGET, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper, acalaUrl, astarUrl, moonbeamUrl, polkadexUrl, relayUrl, uniqueAssetId} from './xcm.types'; const testHelper = new XcmTestHelper('unique'); - -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, - uniqueAssetId, - SENDBACK_AMOUNT, - ); - - let xcmProgramSent: any; - - await targetPlayground(networkUrl, async (helper) => { - if('getSudo' in helper) { - await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, xcmProgram); - xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); - } else if('fastDemocracy' in helper) { - const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, xcmProgram]); - // Needed to bypass the call filter. - const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); - await helper.fastDemocracy.executeProposal(`sending ${networkName} -> 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, uniqueVersionedMultilocation, maliciousXcmProgram); - maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); - } else if('fastDemocracy' in helper) { - const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgram]); - // Needed to bypass the call filter. - const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); - await helper.fastDemocracy.executeProposal(`sending ${networkName} -> 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, - uniqueAssetId, - 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, uniqueVersionedMultilocation, 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', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]); - // Needed to bypass the call filter. - const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); - await helper.fastDemocracy.executeProposal(`${netwokrName} 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, uniqueVersionedMultilocation, maliciousXcmProgramHereId); - maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); - } - else if('fastDemocracy' in helper) { - const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramHereId]); - // Needed to bypass the call filter. - const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); - await helper.fastDemocracy.executeProposal(`${netwokrName} 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 genericRejectNativeTokensFrom(networkName: keyof typeof NETWORKS, sudoerOnTargetChain: IKeyringPair) { - const networkUrl = mapToChainUrl(networkName); - const targetPlayground = getDevPlayground(networkName); - let messageSent: any; - - await usingPlaygrounds(async (helper) => { - const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited( - helper.arrange.createEmptyAccount().addressRaw, - { - Concrete: { - parents: 1, - interior: { - X1: { - Parachain: mapToChainId(networkName), - }, - }, - }, - }, - TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT, - ); - await targetPlayground(networkUrl, async (helper) => { - if('getSudo' in helper) { - await helper.getSudo().xcm.send(sudoerOnTargetChain, uniqueVersionedMultilocation, maliciousXcmProgramFullId); - messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); - } else if('fastDemocracy' in helper) { - const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]); - // Needed to bypass the call filter. - const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); - await helper.fastDemocracy.executeProposal(`${networkName} 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; @@ -375,24 +71,23 @@ 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); + await testHelper.sendUnqTo('acala', randomAccount); }); itSub('Should connect to Acala and send UNQ back', async () => { - await genericSendUnqBack('acala', alice, randomAccount); + await testHelper.sendUnqBack('acala', alice, randomAccount); }); itSub('Acala can send only up to its balance', async () => { - await genericSendOnlyOwnedBalance('acala', alice); + await testHelper.sendOnlyOwnedBalance('acala', alice); }); itSub('Should not accept reserve transfer of UNQ from Acala', async () => { - await genericReserveTransferUNQfrom('acala', alice); + await testHelper.reserveTransferUNQfrom('acala', alice); }); }); @@ -430,7 +125,6 @@ await usingPlaygrounds(async (helper) => { await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); - balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address); }); }); @@ -467,19 +161,19 @@ }); itSub('Unique rejects ACA tokens from Acala', async () => { - await genericRejectNativeTokensFrom('acala', alice); + await testHelper.rejectNativeTokensFrom('acala', alice); }); itSub('Unique rejects GLMR tokens from Moonbeam', async () => { - await genericRejectNativeTokensFrom('moonbeam', alice); + await testHelper.rejectNativeTokensFrom('moonbeam', alice); }); itSub('Unique rejects ASTR tokens from Astar', async () => { - await genericRejectNativeTokensFrom('astar', alice); + await testHelper.rejectNativeTokensFrom('astar', alice); }); itSub('Unique rejects PDX tokens from Polkadex', async () => { - await genericRejectNativeTokensFrom('polkadex', alice); + await testHelper.rejectNativeTokensFrom('polkadex', alice); }); }); @@ -570,7 +264,6 @@ await usingPlaygrounds(async (helper) => { await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET); - balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccountUnique.address); }); }); @@ -657,19 +350,19 @@ }); itSub('Should connect and send UNQ to Astar', async () => { - await genericSendUnqTo('astar', randomAccount); + await testHelper.sendUnqTo('astar', randomAccount); }); itSub('Should connect to Astar and send UNQ back', async () => { - await genericSendUnqBack('astar', alice, randomAccount); + await testHelper.sendUnqBack('astar', alice, randomAccount); }); itSub('Astar can send only up to its balance', async () => { - await genericSendOnlyOwnedBalance('astar', alice); + await testHelper.sendOnlyOwnedBalance('astar', alice); }); itSub('Should not accept reserve transfer of UNQ from Astar', async () => { - await genericReserveTransferUNQfrom('astar', alice); + await testHelper.reserveTransferUNQfrom('astar', alice); }); }); --- a/tests/src/xcm/xcm.types.ts +++ b/tests/src/xcm/xcm.types.ts @@ -3,7 +3,6 @@ import {expect, usingAcalaPlaygrounds, usingAstarPlaygrounds, usingKaruraPlaygrounds, usingMoonbeamPlaygrounds, usingMoonriverPlaygrounds, usingPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds, usingShidenPlaygrounds} from '../util'; import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev'; import config from '../config'; -import {blake2AsHex} from '@polkadot/util-crypto'; export const UNIQUE_CHAIN = +(process.env.RELAY_UNIQUE_ID || 2037); export const STATEMINT_CHAIN = +(process.env.RELAY_STATEMINT_ID || 1000); @@ -136,10 +135,10 @@ } export 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; +export const SENDER_BUDGET = 2n * TRANSFER_AMOUNT; +export const SENDBACK_AMOUNT = TRANSFER_AMOUNT / 2n; +export const STAYED_ON_TARGET_CHAIN = TRANSFER_AMOUNT - SENDBACK_AMOUNT; +export const TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT = 100_000_000_000n; export class XcmTestHelper { private _balanceUniqueTokenInit: bigint = 0n; @@ -163,6 +162,7 @@ return UNIQUE_CHAIN; } } + private _isAddress20FormatFor(network: NetworkNames) { switch (network) { case 'moonbeam': @@ -173,6 +173,19 @@ } } + private _runtimeVersionedMultilocation() { + return { + V3: { + parents: 1, + interior: { + X1: { + Parachain: this._getNativeId(), + }, + }, + }, + }; + } + uniqueChainMultilocationForRelay() { return { V3: { @@ -250,8 +263,8 @@ this._balanceUniqueTokenMiddle = await helper.balance.getSubstrate(randomAccount.address); this._unqFees = this._balanceUniqueTokenInit - this._balanceUniqueTokenMiddle - TRANSFER_AMOUNT; - console.log('[Unique -> %s] transaction fees on Unique: %s UNQ', networkName, helper.util.bigIntToDecimals(this._unqFees)); - expect(this._unqFees > 0n, 'Negative fees UNQ, looks like nothing was transferred').to.be.true; + console.log('[%s -> %s] transaction fees: %s', this._nativeRuntime, networkName, helper.util.bigIntToDecimals(this._unqFees)); + expect(this._unqFees > 0n, 'Negative fees, looks like nothing was transferred').to.be.true; await targetPlayground(networkUrl, async (helper) => { /* @@ -302,10 +315,10 @@ await targetPlayground(networkUrl, async (helper) => { if('getSudo' in helper) { - await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, xcmProgram); + await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), xcmProgram); xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); } else if('fastDemocracy' in helper) { - const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, xcmProgram]); + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), xcmProgram]); // Needed to bypass the call filter. const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall); @@ -354,10 +367,10 @@ await targetPlayground(networkUrl, async (helper) => { if('getSudo' in helper) { - await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgram); + await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), maliciousXcmProgram); maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); } else if('fastDemocracy' in helper) { - const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgram]); + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgram]); // Needed to bypass the call filter. const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall); @@ -413,12 +426,12 @@ // Try to trick Unique using full UNQ identification await targetPlayground(networkUrl, async (helper) => { if('getSudo' in helper) { - await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramFullId); + await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), 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', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]); + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId]); // Needed to bypass the call filter. const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using path asset identification`,batchCall); @@ -436,11 +449,11 @@ // Try to trick Unique using shortened UNQ identification await targetPlayground(networkUrl, async (helper) => { if('getSudo' in helper) { - await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramHereId); + await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), maliciousXcmProgramHereId); maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); } else if('fastDemocracy' in helper) { - const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramHereId]); + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgramHereId]); // Needed to bypass the call filter. const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using "here" asset identification`, batchCall); @@ -478,10 +491,10 @@ ); await targetPlayground(networkUrl, async (helper) => { if('getSudo' in helper) { - await helper.getSudo().xcm.send(sudoerOnTargetChain, uniqueVersionedMultilocation, maliciousXcmProgramFullId); + await helper.getSudo().xcm.send(sudoerOnTargetChain, this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId); messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); } else if('fastDemocracy' in helper) { - const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]); + const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId]); // Needed to bypass the call filter. const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); await helper.fastDemocracy.executeProposal(`${networkName} sending native tokens to the Unique via fast democracy`, batchCall); --- a/tests/src/xcm/xcmQuartz.test.ts +++ b/tests/src/xcm/xcmQuartz.test.ts @@ -18,6 +18,7 @@ import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util'; import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev'; import {STATEMINE_CHAIN, QUARTZ_CHAIN, KARURA_CHAIN, MOONRIVER_CHAIN, SHIDEN_CHAIN, STATEMINE_DECIMALS, KARURA_DECIMALS, QTZ_DECIMALS, RELAY_DECIMALS, SHIDEN_DECIMALS, karuraUrl, moonriverUrl, relayUrl, shidenUrl, statemineUrl} from './xcm.types'; +import {hexToString} from '@polkadot/util'; @@ -483,7 +484,14 @@ minimalBalance: 1000000000000000000n, }; - await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata); + const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v]: [any, any]) => + hexToString(v.toJSON()['symbol'])) as string[]; + + if(!assets.includes('QTZ')) { + await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata); + } else { + console.log('QTZ token already registered on Karura assetRegistry pallet'); + } await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n); balanceKaruraTokenInit = await helper.balance.getSubstrate(randomAccount.address); balanceQuartzForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0}); @@ -969,19 +977,22 @@ const unitsPerSecond = 1n; const numAssetsWeightHint = 0; - const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({ - location: quartzAssetLocation, - metadata: quartzAssetMetadata, - existentialDeposit, - isSufficient, - unitsPerSecond, - numAssetsWeightHint, - }); - - console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal); + if((await helper.assetManager.assetTypeId(quartzAssetLocation)).toJSON()) { + console.log('Quartz asset already registered on Moonriver'); + } else { + const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({ + location: quartzAssetLocation, + metadata: quartzAssetMetadata, + existentialDeposit, + isSufficient, + unitsPerSecond, + numAssetsWeightHint, + }); - await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal); + console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal); + await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal); + } // >>> Acquire Quartz AssetId Info on Moonriver >>> console.log('Acquire Quartz AssetId Info on Moonriver.......'); @@ -1298,40 +1309,43 @@ }); await usingShidenPlaygrounds(shidenUrl, async (helper) => { - console.log('1. Create foreign asset and metadata'); - // TODO update metadata with values from production - await helper.assets.create( - alice, - QTZ_ASSET_ID_ON_SHIDEN, - alice.address, - QTZ_MINIMAL_BALANCE_ON_SHIDEN, - ); + if(!(await helper.callRpc('api.query.assets.asset', [QTZ_ASSET_ID_ON_SHIDEN])).toJSON()) { + console.log('1. Create foreign asset and metadata'); + // TODO update metadata with values from production + await helper.assets.create( + alice, + QTZ_ASSET_ID_ON_SHIDEN, + alice.address, + QTZ_MINIMAL_BALANCE_ON_SHIDEN, + ); - await helper.assets.setMetadata( - alice, - QTZ_ASSET_ID_ON_SHIDEN, - 'Cross chain QTZ', - 'xcQTZ', - Number(QTZ_DECIMALS), - ); + await helper.assets.setMetadata( + alice, + QTZ_ASSET_ID_ON_SHIDEN, + 'Cross chain QTZ', + 'xcQTZ', + Number(QTZ_DECIMALS), + ); - console.log('2. Register asset location on Shiden'); - const assetLocation = { - V2: { - parents: 1, - interior: { - X1: { - Parachain: QUARTZ_CHAIN, + console.log('2. Register asset location on Shiden'); + const assetLocation = { + V2: { + parents: 1, + interior: { + X1: { + Parachain: QUARTZ_CHAIN, + }, }, }, - }, - }; + }; - await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]); - - console.log('3. Set QTZ payment for XCM execution on Shiden'); - await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]); + await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]); + console.log('3. Set QTZ payment for XCM execution on Shiden'); + await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]); + } else { + console.log('QTZ is already registered on Shiden'); + } console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)'); await helper.balance.transferToSubstrate(alice, sender.address, shidenInitialBalance); }); -- gitstuff