From bce2cc1e9ab8e0e579f3a280647486713e4c7234 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Mon, 02 Oct 2023 19:21:41 +0000 Subject: [PATCH] Merge pull request #1003 from UniqueNetwork/feature/llxcm-qtz added `XcmTestHelper` + allow XCM Transact for Gov/Identity/System calls --- --- 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/runtime/common/config/xcm/mod.rs +++ b/runtime/common/config/xcm/mod.rs @@ -15,7 +15,7 @@ // along with Unique Network. If not, see . use frame_support::{ - traits::{Everything, Nothing, Get, ConstU32, ProcessMessageError}, + traits::{Everything, Nothing, Get, ConstU32, ProcessMessageError, Contains}, parameter_types, }; use frame_system::EnsureRoot; @@ -162,6 +162,54 @@ pub type Weigher = FixedWeightBounds; +pub struct XcmCallFilter; +impl XcmCallFilter { + fn allow_gov_and_sys_call(call: &RuntimeCall) -> bool { + match call { + RuntimeCall::System(..) => true, + + #[cfg(feature = "governance")] + RuntimeCall::Identity(..) + | RuntimeCall::Preimage(..) + | RuntimeCall::Democracy(..) + | RuntimeCall::Council(..) + | RuntimeCall::TechnicalCommittee(..) + | RuntimeCall::CouncilMembership(..) + | RuntimeCall::TechnicalCommitteeMembership(..) + | RuntimeCall::FellowshipCollective(..) + | RuntimeCall::FellowshipReferenda(..) => true, + _ => false, + } + } + + fn allow_utility_call(call: &RuntimeCall) -> bool { + match call { + RuntimeCall::Utility(pallet_utility::Call::batch { calls, .. }) => { + calls.iter().all(Self::allow_gov_and_sys_call) + } + RuntimeCall::Utility(pallet_utility::Call::batch_all { calls, .. }) => { + calls.iter().all(Self::allow_gov_and_sys_call) + } + RuntimeCall::Utility(pallet_utility::Call::as_derivative { call, .. }) => { + Self::allow_gov_and_sys_call(call) + } + RuntimeCall::Utility(pallet_utility::Call::dispatch_as { call, .. }) => { + Self::allow_gov_and_sys_call(call) + } + RuntimeCall::Utility(pallet_utility::Call::force_batch { calls, .. }) => { + calls.iter().all(Self::allow_gov_and_sys_call) + } + _ => false, + } + } +} + +impl Contains for XcmCallFilter { + fn contains(call: &RuntimeCall) -> bool { + Self::allow_gov_and_sys_call(call) || Self::allow_utility_call(call) + } +} + pub struct XcmExecutorConfig(PhantomData); impl xcm_executor::Config for XcmExecutorConfig where @@ -191,9 +239,7 @@ type MessageExporter = (); type UniversalAliases = Nothing; type CallDispatcher = RuntimeCall; - - // Deny all XCM Transacts. - type SafeCallFilter = Nothing; + type SafeCallFilter = XcmCallFilter; } #[cfg(feature = "runtime-benchmarks")] --- a/runtime/opal/src/xcm_barrier.rs +++ b/runtime/opal/src/xcm_barrier.rs @@ -14,7 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -use frame_support::traits::Everything; -use xcm_builder::{AllowTopLevelPaidExecutionFrom, TakeWeightCredit}; +use frame_support::{match_types, traits::Everything}; +use xcm::latest::{Junctions::*, MultiLocation}; +use xcm_builder::{AllowTopLevelPaidExecutionFrom, TakeWeightCredit, AllowExplicitUnpaidExecutionFrom}; -pub type Barrier = (TakeWeightCredit, AllowTopLevelPaidExecutionFrom); +match_types! { + pub type ParentOnly: impl Contains = { + MultiLocation { parents: 1, interior: Here } + }; +} + +pub type Barrier = ( + TakeWeightCredit, + AllowExplicitUnpaidExecutionFrom, + AllowTopLevelPaidExecutionFrom, +); --- a/runtime/quartz/src/xcm_barrier.rs +++ b/runtime/quartz/src/xcm_barrier.rs @@ -18,12 +18,16 @@ use xcm::latest::{Junctions::*, MultiLocation}; use xcm_builder::{ AllowKnownQueryResponses, AllowSubscriptionsFrom, TakeWeightCredit, - AllowTopLevelPaidExecutionFrom, + AllowTopLevelPaidExecutionFrom, AllowExplicitUnpaidExecutionFrom, }; use crate::PolkadotXcm; match_types! { + pub type ParentOnly: impl Contains = { + MultiLocation { parents: 1, interior: Here } + }; + pub type ParentOrSiblings: impl Contains = { MultiLocation { parents: 1, interior: Here } | MultiLocation { parents: 1, interior: X1(_) } @@ -32,6 +36,7 @@ pub type Barrier = ( TakeWeightCredit, + AllowExplicitUnpaidExecutionFrom, AllowTopLevelPaidExecutionFrom, // Expected responses are OK. AllowKnownQueryResponses, --- a/runtime/unique/src/xcm_barrier.rs +++ b/runtime/unique/src/xcm_barrier.rs @@ -18,12 +18,16 @@ use xcm::latest::{Junctions::*, MultiLocation}; use xcm_builder::{ AllowKnownQueryResponses, AllowSubscriptionsFrom, TakeWeightCredit, - AllowTopLevelPaidExecutionFrom, + AllowTopLevelPaidExecutionFrom, AllowExplicitUnpaidExecutionFrom, }; use crate::PolkadotXcm; match_types! { + pub type ParentOnly: impl Contains = { + MultiLocation { parents: 1, interior: Here } + }; + pub type ParentOrSiblings: impl Contains = { MultiLocation { parents: 1, interior: Here } | MultiLocation { parents: 1, interior: X1(_) } @@ -32,6 +36,7 @@ pub type Barrier = ( TakeWeightCredit, + AllowExplicitUnpaidExecutionFrom, AllowTopLevelPaidExecutionFrom, // Expected responses are OK. AllowKnownQueryResponses, --- a/tests/package.json +++ b/tests/package.json @@ -117,6 +117,8 @@ "testXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmUnique.test.ts", "testFullXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/*Unique.test.ts", "testXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmQuartz.test.ts", + "testLowLevelXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/lowLevelXcmQuartz.test.ts", + "testFullXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/*Quartz.test.ts", "testXcmOpal": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmOpal.test.ts", "testXcmTransferAcala": "yarn _test ./**/xcm/xcmTransferAcala.test.ts acalaId=2000 uniqueId=5000", "testXcmTransferStatemine": "yarn _test ./**/xcm/xcmTransferStatemine.test.ts statemineId=1000 uniqueId=5000", --- a/tests/src/util/playgrounds/unique.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -9,7 +9,7 @@ import {IKeyringPair} from '@polkadot/types/types'; import {EventRecord} from '@polkadot/types/interfaces'; import {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from './types'; -import {FrameSystemEventRecord, XcmV2TraitsError} from '@polkadot/types/lookup'; +import {FrameSystemEventRecord, XcmV2TraitsError, XcmV3TraitsOutcome} from '@polkadot/types/lookup'; import {SignerOptions, VoidFn} from '@polkadot/api/types'; import {Pallets} from '..'; import {spawnSync} from 'child_process'; @@ -260,6 +260,12 @@ outcome: eventData(data, 1), })); }; + + static DmpQueue = class extends EventSection('dmpQueue') { + static ExecutedDownward = this.Method('ExecutedDownward', data => ({ + outcome: eventData(data, 1), + })); + }; } // eslint-disable-next-line @typescript-eslint/naming-convention @@ -559,6 +565,12 @@ super(logger, options); this.wait = new WaitGroup(this); } + + getSudo() { + // eslint-disable-next-line @typescript-eslint/naming-convention + const SudoHelperType = SudoHelper(this.helperBase); + return this.clone(SudoHelperType) as DevRelayHelper; + } } export class DevWestmintHelper extends WestmintHelper { @@ -968,6 +980,31 @@ ], }; } + + makeUnpaidSudoTransactProgram(info: {weightMultiplier: number, call: string}) { + return { + V3: [ + { + UnpaidExecution: { + weightLimit: 'Unlimited', + checkOrigin: null, + }, + }, + { + Transact: { + originKind: 'Superuser', + requireWeightAtMost: { + refTime: info.weightMultiplier * 200000000, + proofSize: info.weightMultiplier * 3000, + }, + call: { + encoded: info.call, + }, + }, + }, + ], + }; + } } class MoonbeamAccountGroup { @@ -1501,4 +1538,4 @@ ); } }; -} \ No newline at end of file +} --- a/tests/src/util/playgrounds/unique.xcm.ts +++ b/tests/src/util/playgrounds/unique.xcm.ts @@ -240,19 +240,19 @@ } export class AssetsGroup extends HelperGroup { - async create(signer: TSigner, assetId: number, admin: string, minimalBalance: bigint) { + async create(signer: TSigner, assetId: number | bigint, admin: string, minimalBalance: bigint) { await this.helper.executeExtrinsic(signer, 'api.tx.assets.create', [assetId, admin, minimalBalance], true); } - async setMetadata(signer: TSigner, assetId: number, name: string, symbol: string, decimals: number) { + async setMetadata(signer: TSigner, assetId: number | bigint, name: string, symbol: string, decimals: number) { await this.helper.executeExtrinsic(signer, 'api.tx.assets.setMetadata', [assetId, name, symbol, decimals], true); } - async mint(signer: TSigner, assetId: number, beneficiary: string, amount: bigint) { + async mint(signer: TSigner, assetId: number | bigint, beneficiary: string, amount: bigint) { await this.helper.executeExtrinsic(signer, 'api.tx.assets.mint', [assetId, beneficiary, amount], true); } - async account(assetId: string | number, address: string) { + async account(assetId: string | number | bigint, address: string) { const accountAsset = ( await this.helper.callRpc('api.query.assets.account', [assetId, address]) ).toJSON()! as any; --- /dev/null +++ b/tests/src/xcm/lowLevelXcmQuartz.test.ts @@ -0,0 +1,364 @@ +// 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 {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; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + [randomAccount] = 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 usingKaruraPlaygrounds(karuraUrl, async (helper) => { + const destination = { + V2: { + parents: 1, + interior: { + X1: { + Parachain: QUARTZ_CHAIN, + }, + }, + }, + }; + + const metadata = { + name: 'Quartz', + symbol: 'QTZ', + decimals: 18, + minimalBalance: 1000000000000000000n, + }; + + 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); + }); + + await usingPlaygrounds(async (helper) => { + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + }); + }); + + itSub('Should connect and send QTZ to Karura', async () => { + await testHelper.sendUnqTo('karura', randomAccount); + }); + + itSub('Should connect to Karura and send QTZ back', async () => { + await testHelper.sendUnqBack('karura', alice, randomAccount); + }); + + itSub('Karura can send only up to its balance', async () => { + await testHelper.sendOnlyOwnedBalance('karura', alice); + }); +}); +// These tests are relevant only when +// the the corresponding foreign assets are not registered +describeXCM('[XCMLL] Integration test: Quartz 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('Quartz rejects KAR tokens from Karura', async () => { + await testHelper.rejectNativeTokensFrom('karura', alice); + }); + + itSub('Quartz rejects MOVR tokens from Moonriver', async () => { + await testHelper.rejectNativeTokensFrom('moonriver', alice); + }); + + itSub('Quartz rejects SDN tokens from Shiden', async () => { + await testHelper.rejectNativeTokensFrom('shiden', alice); + }); +}); + +describeXCM('[XCMLL] Integration test: Exchanging QTZ with Moonriver', () => { + // Quartz constants + let alice: IKeyringPair; + let quartzAssetLocation; + + let randomAccountQuartz: IKeyringPair; + let randomAccountMoonriver: IKeyringPair; + + // Moonriver constants + let assetId: string; + + const quartzAssetMetadata = { + name: 'xcQuartz', + symbol: 'xcQTZ', + decimals: 18, + isFrozen: false, + minimalBalance: 1n, + }; + + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + [randomAccountQuartz] = 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 usingMoonriverPlaygrounds(moonriverUrl, async (helper) => { + const alithAccount = helper.account.alithAccount(); + const baltatharAccount = helper.account.baltatharAccount(); + const dorothyAccount = helper.account.dorothyAccount(); + + randomAccountMoonriver = 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 <<< + + quartzAssetLocation = { + XCM: { + parents: 1, + interior: {X1: {Parachain: QUARTZ_CHAIN}}, + }, + }; + const existentialDeposit = 1n; + 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, + }); + + 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.......'); + + assetId = (await helper.assetManager.assetTypeId(quartzAssetLocation)).toString(); + + console.log('QTZ asset ID is %s', assetId); + console.log('Acquire Quartz AssetId Info on Moonriver.......DONE'); + // >>> Acquire Quartz AssetId Info on Moonriver >>> + + // >>> Sponsoring random Account >>> + console.log('Sponsoring random Account.......'); + await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonriver.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, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT); + }); + }); + + itSub('Should connect and send QTZ to Moonriver', async () => { + await testHelper.sendUnqTo('moonriver', randomAccountQuartz, randomAccountMoonriver); + }); + + itSub('Should connect to Moonriver and send QTZ back', async () => { + await testHelper.sendUnqBack('moonriver', alice, randomAccountQuartz); + }); + + itSub('Moonriver can send only up to its balance', async () => { + await testHelper.sendOnlyOwnedBalance('moonriver', alice); + }); + + itSub('Should not accept reserve transfer of QTZ from Moonriver', async () => { + await testHelper.rejectReserveTransferUNQfrom('moonriver', alice); + }); +}); + +describeXCM('[XCMLL] Integration test: Exchanging tokens with Shiden', () => { + let alice: IKeyringPair; + let randomAccount: IKeyringPair; + + const QTZ_ASSET_ID_ON_SHIDEN = 18_446_744_073_709_551_633n; // The value is taken from the live Shiden + const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n; // The value is taken from the live Shiden + + // Quartz -> Shiden + const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden + const unitsPerSecond = 500_451_000_000_000_000_000n; // The value is taken from the live Shiden + + 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('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) => { + if(!(await helper.callRpc('api.query.assets.asset', [QTZ_ASSET_ID_ON_SHIDEN])).toJSON()) { + console.log('1. Create foreign asset and metadata'); + 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, + 'Quartz', + 'QTZ', + Number(QTZ_DECIMALS), + ); + + 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]); + } 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, randomAccount.address, shidenInitialBalance); + }); + }); + + itSub('Should connect and send QTZ to Shiden', async () => { + await testHelper.sendUnqTo('shiden', randomAccount); + }); + + itSub('Should connect to Shiden and send QTZ back', async () => { + await testHelper.sendUnqBack('shiden', alice, randomAccount); + }); + + itSub('Shiden can send only up to its balance', async () => { + await testHelper.sendOnlyOwnedBalance('shiden', alice); + }); + + itSub('Should not accept reserve transfer of QTZ from Shiden', async () => { + await testHelper.rejectReserveTransferUNQfrom('shiden', alice); + }); +}); + +describeXCM('[XCMLL] Integration test: The relay can do some root ops', () => { + let sudoer: IKeyringPair; + + before(async function () { + await usingRelayPlaygrounds(relayUrl, async (_, privateKey) => { + sudoer = await privateKey('//Alice'); + }); + }); + + // At the moment there is no reliable way + // to establish the correspondence between the `ExecutedDownward` event + // and the relay's sent message due to `SetTopic` instruction + // containing an unpredictable topic silently added by the relay's messages on the router level. + // This changes the message hash on arrival to our chain. + // + // See: + // * The relay's router: https://github.com/paritytech/polkadot-sdk/blob/f60318f68687e601c47de5ad5ca88e2c3f8139a7/polkadot/runtime/westend/src/xcm_config.rs#L83 + // * The `WithUniqueTopic` helper: https://github.com/paritytech/polkadot-sdk/blob/945ebbbcf66646be13d5b1d1bc26c8b0d3296d9e/polkadot/xcm/xcm-builder/src/routing.rs#L36 + // + // Because of this, we insert time gaps between tests so + // different `ExecutedDownward` events won't interfere with each other. + afterEach(async () => { + await usingPlaygrounds(async (helper) => { + await helper.wait.newBlocks(3); + }); + }); + + itSub('The relay can set storage', async () => { + await testHelper.relayIsPermittedToSetStorage(sudoer, 'plain'); + }); + + itSub('The relay can batch set storage', async () => { + await testHelper.relayIsPermittedToSetStorage(sudoer, 'batch'); + }); + + itSub('The relay can batchAll set storage', async () => { + await testHelper.relayIsPermittedToSetStorage(sudoer, 'batchAll'); + }); + + itSub('The relay can forceBatch set storage', async () => { + await testHelper.relayIsPermittedToSetStorage(sudoer, 'forceBatch'); + }); + + itSub('[negative] The relay cannot set balance', async () => { + await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'plain'); + }); + + itSub('[negative] The relay cannot set balance via batch', async () => { + await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batch'); + }); + + itSub('[negative] The relay cannot set balance via batchAll', async () => { + await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batchAll'); + }); + + itSub('[negative] The relay cannot set balance via forceBatch', async () => { + await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'forceBatch'); + }); + + itSub('[negative] The relay cannot set balance via dispatchAs', async () => { + await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'dispatchAs'); + }); +}); --- a/tests/src/xcm/lowLevelXcmUnique.test.ts +++ b/tests/src/xcm/lowLevelXcmUnique.test.ts @@ -16,317 +16,14 @@ import {IKeyringPair} from '@polkadot/types/types'; import config from '../config'; -import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds} 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, acalaUrl, astarUrl, expectFailedToTransact, expectUntrustedReserveLocationFail, getDevPlayground, mapToChainId, mapToChainUrl, maxWaitBlocks, moonbeamUrl, polkadexUrl, 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; - - -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); +import {ASTAR_DECIMALS, SAFE_XCM_VERSION, SENDER_BUDGET, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper, acalaUrl, astarUrl, moonbeamUrl, polkadexUrl, relayUrl, uniqueAssetId} from './xcm.types'; - accountBalance = await helper.balance.getSubstrate(targetAccount.address); - expect(accountBalance).to.be.equal(0n); - }); -} +const testHelper = new XcmTestHelper('unique'); -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', () => { @@ -374,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.rejectReserveTransferUNQfrom('acala', alice); }); }); @@ -429,25 +125,24 @@ 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); + await testHelper.sendUnqTo('polkadex', randomAccount); }); itSub('Should connect to Polkadex and send UNQ back', async () => { - await genericSendUnqBack('polkadex', alice, randomAccount); + await testHelper.sendUnqBack('polkadex', alice, randomAccount); }); itSub('Polkadex can send only up to its balance', async () => { - await genericSendOnlyOwnedBalance('polkadex', alice); + await testHelper.sendOnlyOwnedBalance('polkadex', alice); }); itSub('Should not accept reserve transfer of UNQ from Polkadex', async () => { - await genericReserveTransferUNQfrom('polkadex', alice); + await testHelper.rejectReserveTransferUNQfrom('polkadex', alice); }); }); @@ -466,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); }); }); @@ -569,24 +264,23 @@ 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); + await testHelper.sendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam); }); itSub('Should connect to Moonbeam and send UNQ back', async () => { - await genericSendUnqBack('moonbeam', alice, randomAccountUnique); + await testHelper.sendUnqBack('moonbeam', alice, randomAccountUnique); }); itSub('Moonbeam can send only up to its balance', async () => { - await genericSendOnlyOwnedBalance('moonbeam', alice); + await testHelper.sendOnlyOwnedBalance('moonbeam', alice); }); itSub('Should not accept reserve transfer of UNQ from Moonbeam', async () => { - await genericReserveTransferUNQfrom('moonbeam', alice); + await testHelper.rejectReserveTransferUNQfrom('moonbeam', alice); }); }); @@ -594,12 +288,12 @@ let alice: IKeyringPair; let randomAccount: IKeyringPair; - const UNQ_ASSET_ID_ON_ASTAR = 1; - const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; + const UNQ_ASSET_ID_ON_ASTAR = 18_446_744_073_709_551_631n; // The value is taken from the live Astar + const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; // The value is taken from the live Astar // 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? + const unitsPerSecond = 9_451_000_000_000_000_000n; // The value is taken from the live Astar before(async () => { await usingPlaygrounds(async (helper, privateKey) => { @@ -615,7 +309,6 @@ 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, @@ -626,8 +319,8 @@ await helper.assets.setMetadata( alice, UNQ_ASSET_ID_ON_ASTAR, - 'Cross chain UNQ', - 'xcUNQ', + 'Unique Network', + 'UNQ', Number(UNQ_DECIMALS), ); @@ -656,18 +349,82 @@ }); 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.rejectReserveTransferUNQfrom('astar', alice); + }); +}); + +describeXCM('[XCMLL] Integration test: The relay can do some root ops', () => { + let sudoer: IKeyringPair; + + before(async function () { + await usingRelayPlaygrounds(relayUrl, async (_, privateKey) => { + sudoer = await privateKey('//Alice'); + }); + }); + + // At the moment there is no reliable way + // to establish the correspondence between the `ExecutedDownward` event + // and the relay's sent message due to `SetTopic` instruction + // containing an unpredictable topic silently added by the relay's messages on the router level. + // This changes the message hash on arrival to our chain. + // + // See: + // * The relay's router: https://github.com/paritytech/polkadot-sdk/blob/f60318f68687e601c47de5ad5ca88e2c3f8139a7/polkadot/runtime/westend/src/xcm_config.rs#L83 + // * The `WithUniqueTopic` helper: https://github.com/paritytech/polkadot-sdk/blob/945ebbbcf66646be13d5b1d1bc26c8b0d3296d9e/polkadot/xcm/xcm-builder/src/routing.rs#L36 + // + // Because of this, we insert time gaps between tests so + // different `ExecutedDownward` events won't interfere with each other. + afterEach(async () => { + await usingPlaygrounds(async (helper) => { + await helper.wait.newBlocks(3); + }); + }); + + itSub('The relay can set storage', async () => { + await testHelper.relayIsPermittedToSetStorage(sudoer, 'plain'); + }); + + itSub('The relay can batch set storage', async () => { + await testHelper.relayIsPermittedToSetStorage(sudoer, 'batch'); + }); + + itSub('The relay can batchAll set storage', async () => { + await testHelper.relayIsPermittedToSetStorage(sudoer, 'batchAll'); + }); + + itSub('The relay can forceBatch set storage', async () => { + await testHelper.relayIsPermittedToSetStorage(sudoer, 'forceBatch'); + }); + + itSub('[negative] The relay cannot set balance', async () => { + await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'plain'); + }); + + itSub('[negative] The relay cannot set balance via batch', async () => { + await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batch'); + }); + + itSub('[negative] The relay cannot set balance via batchAll', async () => { + await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batchAll'); + }); + + itSub('[negative] The relay cannot set balance via forceBatch', async () => { + await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'forceBatch'); + }); + + itSub('[negative] The relay cannot set balance via dispatchAs', async () => { + await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'dispatchAs'); }); }); --- a/tests/src/xcm/xcm.types.ts +++ b/tests/src/xcm/xcm.types.ts @@ -1,4 +1,6 @@ -import {usingAcalaPlaygrounds, usingAstarPlaygrounds, usingMoonbeamPlaygrounds, usingPolkadexPlaygrounds} from '../util'; +import {IKeyringPair} from '@polkadot/types/types'; +import {hexToString} from '@polkadot/util'; +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'; @@ -9,19 +11,39 @@ export const ASTAR_CHAIN = +(process.env.RELAY_ASTAR_ID || 2006); export const POLKADEX_CHAIN = +(process.env.RELAY_POLKADEX_ID || 2040); +export const QUARTZ_CHAIN = +(process.env.RELAY_QUARTZ_ID || 2095); +export const STATEMINE_CHAIN = +(process.env.RELAY_STATEMINE_ID || 1000); +export const KARURA_CHAIN = +(process.env.RELAY_KARURA_ID || 2000); +export const MOONRIVER_CHAIN = +(process.env.RELAY_MOONRIVER_ID || 2023); +export const SHIDEN_CHAIN = +(process.env.RELAY_SHIDEN_ID || 2007); + +export const relayUrl = config.relayUrl; +export const statemintUrl = config.statemintUrl; +export const statemineUrl = config.statemineUrl; + export const acalaUrl = config.acalaUrl; export const moonbeamUrl = config.moonbeamUrl; export const astarUrl = config.astarUrl; export const polkadexUrl = config.polkadexUrl; +export const karuraUrl = config.karuraUrl; +export const moonriverUrl = config.moonriverUrl; +export const shidenUrl = config.shidenUrl; + export const SAFE_XCM_VERSION = 3; -export const maxWaitBlocks = 6; +export const RELAY_DECIMALS = 12; +export const STATEMINE_DECIMALS = 12; +export const KARURA_DECIMALS = 12; +export const SHIDEN_DECIMALS = 18n; +export const QTZ_DECIMALS = 18n; export const ASTAR_DECIMALS = 18n; export const UNQ_DECIMALS = 18n; +export const maxWaitBlocks = 6; + export const uniqueMultilocation = { parents: 1, interior: { @@ -47,14 +69,30 @@ && event.outcome.isUntrustedReserveLocation); }; +export const expectDownwardXcmNoPermission = async (helper: DevUniqueHelper) => { + // The correct messageHash for downward messages can't be reliably obtained + await helper.wait.expectEvent(maxWaitBlocks, Event.DmpQueue.ExecutedDownward, event => event.outcome.asIncomplete[1].isNoPermission); +}; + +export const expectDownwardXcmComplete = async (helper: DevUniqueHelper) => { + // The correct messageHash for downward messages can't be reliably obtained + await helper.wait.expectEvent(maxWaitBlocks, Event.DmpQueue.ExecutedDownward, event => event.outcome.isComplete); +}; + export const NETWORKS = { acala: usingAcalaPlaygrounds, astar: usingAstarPlaygrounds, polkadex: usingPolkadexPlaygrounds, moonbeam: usingMoonbeamPlaygrounds, + moonriver: usingMoonriverPlaygrounds, + karura: usingKaruraPlaygrounds, + shiden: usingShidenPlaygrounds, } as const; +type NetworkNames = keyof typeof NETWORKS; + +type NativeRuntime = 'opal' | 'quartz' | 'unique'; -export function mapToChainId(networkName: keyof typeof NETWORKS) { +export function mapToChainId(networkName: keyof typeof NETWORKS): number { switch (networkName) { case 'acala': return ACALA_CHAIN; @@ -64,10 +102,16 @@ return MOONBEAM_CHAIN; case 'polkadex': return POLKADEX_CHAIN; + case 'moonriver': + return MOONRIVER_CHAIN; + case 'karura': + return KARURA_CHAIN; + case 'shiden': + return SHIDEN_CHAIN; } } -export function mapToChainUrl(networkName: keyof typeof NETWORKS): string { +export function mapToChainUrl(networkName: NetworkNames): string { switch (networkName) { case 'acala': return acalaUrl; @@ -77,9 +121,501 @@ return moonbeamUrl; case 'polkadex': return polkadexUrl; + case 'moonriver': + return moonriverUrl; + case 'karura': + return karuraUrl; + case 'shiden': + return shidenUrl; } } -export function getDevPlayground(name: T) { +export function getDevPlayground(name: NetworkNames) { return NETWORKS[name]; -} \ No newline at end of file +} + +export const TRANSFER_AMOUNT = 2000000_000_000_000_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; + private _balanceUniqueTokenMiddle: bigint = 0n; + private _balanceUniqueTokenFinal: bigint = 0n; + private _unqFees: bigint = 0n; + private _nativeRuntime: NativeRuntime; + + constructor(runtime: NativeRuntime) { + this._nativeRuntime = runtime; + } + + private _getNativeId() { + switch (this._nativeRuntime) { + case 'opal': + // To-Do + return 1001; + case 'quartz': + return QUARTZ_CHAIN; + case 'unique': + return UNIQUE_CHAIN; + } + } + + private _isAddress20FormatFor(network: NetworkNames) { + switch (network) { + case 'moonbeam': + case 'moonriver': + return true; + default: + return false; + } + } + + private _runtimeVersionedMultilocation() { + return { + V3: { + parents: 1, + interior: { + X1: { + Parachain: this._getNativeId(), + }, + }, + }, + }; + } + + private _uniqueChainMultilocationForRelay() { + return { + V3: { + parents: 0, + interior: { + X1: {Parachain: this._getNativeId()}, + }, + }, + }; + } + + async sendUnqTo( + networkName: keyof typeof NETWORKS, + randomAccount: IKeyringPair, + randomAccountOnTargetChain = randomAccount, + ) { + const networkUrl = mapToChainUrl(networkName); + const targetPlayground = getDevPlayground(networkName); + await usingPlaygrounds(async (helper) => { + this._balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address); + const destination = { + V2: { + parents: 1, + interior: { + X1: { + Parachain: mapToChainId(networkName), + }, + }, + }, + }; + + const beneficiary = { + V2: { + parents: 0, + interior: { + X1: ( + this._isAddress20FormatFor(networkName) ? + { + 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); + this._balanceUniqueTokenMiddle = await helper.balance.getSubstrate(randomAccount.address); + + this._unqFees = this._balanceUniqueTokenInit - this._balanceUniqueTokenMiddle - TRANSFER_AMOUNT; + 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) => { + /* + 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 sendUnqBack( + 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: this._getNativeId()}, + }, + }, + }, + SENDBACK_AMOUNT, + ); + + let xcmProgramSent: any; + + + await targetPlayground(networkUrl, async (helper) => { + if('getSudo' in helper) { + 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', [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); + xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } + }); + + await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == xcmProgramSent.messageHash); + + this._balanceUniqueTokenFinal = await helper.balance.getSubstrate(randomAccountOnUnq.address); + + expect(this._balanceUniqueTokenFinal).to.be.equal(this._balanceUniqueTokenInit - this._unqFees - STAYED_ON_TARGET_CHAIN); + + }); + } + + async sendOnlyOwnedBalance( + 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, 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', [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); + 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 rejectReserveTransferUNQfrom(networkName: keyof typeof NETWORKS, sudoer: IKeyringPair) { + const networkUrl = mapToChainUrl(networkName); + const targetPlayground = getDevPlayground(networkName); + + 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: this._getNativeId(), + }, + }, + }, + }, + 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, 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', [this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId]); + // Needed to bypass the call filter. + const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); + await helper.fastDemocracy.executeProposal(`${networkName} 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, 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', [this._runtimeVersionedMultilocation(), maliciousXcmProgramHereId]); + // Needed to bypass the call filter. + const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]); + await helper.fastDemocracy.executeProposal(`${networkName} 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 rejectNativeTokensFrom(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, 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', [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); + + messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent); + } + }); + await expectFailedToTransact(helper, messageSent); + }); + } + + private async _relayXcmTransactSetStorage(variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch') { + // eslint-disable-next-line require-await + return await usingPlaygrounds(async (helper) => { + const relayForceKV = () => { + const random = Math.random(); + const key = `relay-forced-key (instance: ${random})`; + const val = `relay-forced-value (instance: ${random})`; + const call = helper.constructApiCall('api.tx.system.setStorage', [[[key, val]]]).method.toHex(); + + return { + call, + key, + val, + }; + }; + + if(variant == 'plain') { + const kv = relayForceKV(); + return { + program: helper.arrange.makeUnpaidSudoTransactProgram({ + weightMultiplier: 1, + call: kv.call, + }), + kvs: [kv], + }; + } else { + const kv0 = relayForceKV(); + const kv1 = relayForceKV(); + + const batchCall = helper.constructApiCall(`api.tx.utility.${variant}`, [[kv0.call, kv1.call]]).method.toHex(); + return { + program: helper.arrange.makeUnpaidSudoTransactProgram({ + weightMultiplier: 2, + call: batchCall, + }), + kvs: [kv0, kv1], + }; + } + }); + } + + async relayIsPermittedToSetStorage(relaySudoer: IKeyringPair, variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch') { + const {program, kvs} = await this._relayXcmTransactSetStorage(variant); + + await usingRelayPlaygrounds(relayUrl, async (helper) => { + await helper.getSudo().executeExtrinsic(relaySudoer, 'api.tx.xcmPallet.send', [ + this._uniqueChainMultilocationForRelay(), + program, + ]); + }); + + await usingPlaygrounds(async (helper) => { + await expectDownwardXcmComplete(helper); + + for(const kv of kvs) { + const forcedValue = await helper.callRpc('api.rpc.state.getStorage', [kv.key]); + expect(hexToString(forcedValue.toHex())).to.be.equal(kv.val); + } + }); + } + + private async _relayXcmTransactSetBalance(variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch' | 'dispatchAs') { + // eslint-disable-next-line require-await + return await usingPlaygrounds(async (helper) => { + const emptyAccount = helper.arrange.createEmptyAccount().address; + + const forceSetBalanceCall = helper.constructApiCall('api.tx.balances.forceSetBalance', [emptyAccount, 10_000n]).method.toHex(); + + let call; + + if(variant == 'plain') { + call = forceSetBalanceCall; + + } else if(variant == 'dispatchAs') { + call = helper.constructApiCall('api.tx.utility.dispatchAs', [ + { + system: 'Root', + }, + forceSetBalanceCall, + ]).method.toHex(); + } else { + call = helper.constructApiCall(`api.tx.utility.${variant}`, [[forceSetBalanceCall]]).method.toHex(); + } + + return { + program: helper.arrange.makeUnpaidSudoTransactProgram({ + weightMultiplier: 1, + call, + }), + emptyAccount, + }; + }); + } + + async relayIsNotPermittedToSetBalance( + relaySudoer: IKeyringPair, + variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch' | 'dispatchAs', + ) { + const {program, emptyAccount} = await this._relayXcmTransactSetBalance(variant); + + await usingRelayPlaygrounds(relayUrl, async (helper) => { + await helper.getSudo().executeExtrinsic(relaySudoer, 'api.tx.xcmPallet.send', [ + this._uniqueChainMultilocationForRelay(), + program, + ]); + }); + + await usingPlaygrounds(async (helper) => { + await expectDownwardXcmNoPermission(helper); + expect(await helper.balance.getSubstrate(emptyAccount)).to.be.equal(0n); + }); + } +} --- a/tests/src/xcm/xcmQuartz.test.ts +++ b/tests/src/xcm/xcmQuartz.test.ts @@ -15,30 +15,15 @@ // along with Unique Network. If not, see . import {IKeyringPair} from '@polkadot/types/types'; -import config from '../config'; 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'; -const QUARTZ_CHAIN = +(process.env.RELAY_QUARTZ_ID || 2095); -const STATEMINE_CHAIN = +(process.env.RELAY_STATEMINE_ID || 1000); -const KARURA_CHAIN = +(process.env.RELAY_KARURA_ID || 2000); -const MOONRIVER_CHAIN = +(process.env.RELAY_MOONRIVER_ID || 2023); -const SHIDEN_CHAIN = +(process.env.RELAY_SHIDEN_ID || 2007); -const STATEMINE_PALLET_INSTANCE = 50; -const relayUrl = config.relayUrl; -const statemineUrl = config.statemineUrl; -const karuraUrl = config.karuraUrl; -const moonriverUrl = config.moonriverUrl; -const shidenUrl = config.shidenUrl; +const STATEMINE_PALLET_INSTANCE = 50; -const RELAY_DECIMALS = 12; -const STATEMINE_DECIMALS = 12; -const KARURA_DECIMALS = 12; -const SHIDEN_DECIMALS = 18n; -const QTZ_DECIMALS = 18n; - const TRANSFER_AMOUNT = 2000000000000000000000000n; const FUNDING_AMOUNT = 3_500_000_0000_000_000n; @@ -499,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}); @@ -985,19 +977,22 @@ const unitsPerSecond = 1n; const numAssetsWeightHint = 0; - const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({ - location: quartzAssetLocation, - metadata: quartzAssetMetadata, - existentialDeposit, - isSufficient, - unitsPerSecond, - numAssetsWeightHint, - }); + 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, + }); - 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.......'); @@ -1288,12 +1283,12 @@ let alice: IKeyringPair; let sender: IKeyringPair; - const QTZ_ASSET_ID_ON_SHIDEN = 1; - const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n; + const QTZ_ASSET_ID_ON_SHIDEN = 18_446_744_073_709_551_633n; // The value is taken from the live Shiden + const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n; // The value is taken from the live Shiden // 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 unitsPerSecond = 500_451_000_000_000_000_000n; // The value is taken from the live Shiden 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 @@ -1314,40 +1309,42 @@ }); 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'); + 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, + 'Quartz', + 'QTZ', + 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]); + 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]); - + 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); }); --- a/tests/src/xcm/xcmUnique.test.ts +++ b/tests/src/xcm/xcmUnique.test.ts @@ -1511,12 +1511,12 @@ let alice: IKeyringPair; let randomAccount: IKeyringPair; - const UNQ_ASSET_ID_ON_ASTAR = 1; - const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; + const UNQ_ASSET_ID_ON_ASTAR = 18_446_744_073_709_551_631n; // The value is taken from the live Astar + const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; // The value is taken from the live Astar // 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? + const unitsPerSecond = 9_451_000_000_000_000_000n; // The value is taken from the live Astar const unqToAstarTransferred = 10n * (10n ** UNQ_DECIMALS); // 10 UNQ const unqToAstarArrived = 9_999_999_999_088_000_000n; // 9.999 ... UNQ, Astar takes a commision in foreign tokens @@ -1539,7 +1539,6 @@ 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, @@ -1550,8 +1549,8 @@ await helper.assets.setMetadata( alice, UNQ_ASSET_ID_ON_ASTAR, - 'Cross chain UNQ', - 'xcUNQ', + 'Unique Network', + 'UNQ', Number(UNQ_DECIMALS), ); -- gitstuff