git.delta.rocks / unique-network / refs/commits / bce2cc1e9ab8

difftreelog

Merge pull request #1003 from UniqueNetwork/feature/llxcm-qtz

Yaroslav Bolyukin2023-10-02parents: #22b45b5 #3107202.patch.diff
in: master
added `XcmTestHelper` + allow XCM Transact for Gov/Identity/System calls

14 files changed

modified.baedeker/.gitignorediffbeforeafterboth
--- 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
modified.github/workflows/xcm.ymldiffbeforeafterboth
--- 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:
modifiedruntime/common/config/xcm/mod.rsdiffbeforeafterboth
--- 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 <http://www.gnu.org/licenses/>.
 
 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<UnitWeightCost, RuntimeCall, MaxInstructions>;
 
+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<RuntimeCall> for XcmCallFilter {
+	fn contains(call: &RuntimeCall) -> bool {
+		Self::allow_gov_and_sys_call(call) || Self::allow_utility_call(call)
+	}
+}
+
 pub struct XcmExecutorConfig<T>(PhantomData<T>);
 impl<T> xcm_executor::Config for XcmExecutorConfig<T>
 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")]
modifiedruntime/opal/src/xcm_barrier.rsdiffbeforeafterboth
--- 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 <http://www.gnu.org/licenses/>.
 
-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<Everything>);
+match_types! {
+	pub type ParentOnly: impl Contains<MultiLocation> = {
+		MultiLocation { parents: 1, interior: Here }
+	};
+}
+
+pub type Barrier = (
+	TakeWeightCredit,
+	AllowExplicitUnpaidExecutionFrom<ParentOnly>,
+	AllowTopLevelPaidExecutionFrom<Everything>,
+);
modifiedruntime/quartz/src/xcm_barrier.rsdiffbeforeafterboth
--- 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> = {
+		MultiLocation { parents: 1, interior: Here }
+	};
+
 	pub type ParentOrSiblings: impl Contains<MultiLocation> = {
 		MultiLocation { parents: 1, interior: Here } |
 		MultiLocation { parents: 1, interior: X1(_) }
@@ -32,6 +36,7 @@
 
 pub type Barrier = (
 	TakeWeightCredit,
+	AllowExplicitUnpaidExecutionFrom<ParentOnly>,
 	AllowTopLevelPaidExecutionFrom<Everything>,
 	// Expected responses are OK.
 	AllowKnownQueryResponses<PolkadotXcm>,
modifiedruntime/unique/src/xcm_barrier.rsdiffbeforeafterboth
--- 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> = {
+		MultiLocation { parents: 1, interior: Here }
+	};
+
 	pub type ParentOrSiblings: impl Contains<MultiLocation> = {
 		MultiLocation { parents: 1, interior: Here } |
 		MultiLocation { parents: 1, interior: X1(_) }
@@ -32,6 +36,7 @@
 
 pub type Barrier = (
 	TakeWeightCredit,
+	AllowExplicitUnpaidExecutionFrom<ParentOnly>,
 	AllowTopLevelPaidExecutionFrom<Everything>,
 	// Expected responses are OK.
 	AllowKnownQueryResponses<PolkadotXcm>,
modifiedtests/package.jsondiffbeforeafterboth
--- 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",
modifiedtests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- 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<XcmV2TraitsError>(data, 1),
     }));
   };
+
+  static DmpQueue = class extends EventSection('dmpQueue') {
+    static ExecutedDownward = this.Method('ExecutedDownward', data => ({
+      outcome: eventData<XcmV3TraitsOutcome>(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
+}
modifiedtests/src/util/playgrounds/unique.xcm.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.xcm.ts
+++ b/tests/src/util/playgrounds/unique.xcm.ts
@@ -240,19 +240,19 @@
 }
 
 export class AssetsGroup<T extends ChainHelperBase> extends HelperGroup<T> {
-  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;
addedtests/src/xcm/lowLevelXcmQuartz.test.tsdiffbeforeafterboth
--- /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 <http://www.gnu.org/licenses/>.
+
+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');
+  });
+});
modifiedtests/src/xcm/lowLevelXcmUnique.test.tsdiffbeforeafterboth
--- 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');
   });
 });
modifiedtests/src/xcm/xcm.types.tsdiffbeforeafterboth
1import {IKeyringPair} from '@polkadot/types/types';
2import {hexToString} from '@polkadot/util';
1import {usingAcalaPlaygrounds, usingAstarPlaygrounds, usingMoonbeamPlaygrounds, usingPolkadexPlaygrounds} from '../util';3import {expect, usingAcalaPlaygrounds, usingAstarPlaygrounds, usingKaruraPlaygrounds, usingMoonbeamPlaygrounds, usingMoonriverPlaygrounds, usingPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds, usingShidenPlaygrounds} from '../util';
2import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';4import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';
3import config from '../config';5import config from '../config';
46
9export const ASTAR_CHAIN = +(process.env.RELAY_ASTAR_ID || 2006);11export const ASTAR_CHAIN = +(process.env.RELAY_ASTAR_ID || 2006);
10export const POLKADEX_CHAIN = +(process.env.RELAY_POLKADEX_ID || 2040);12export const POLKADEX_CHAIN = +(process.env.RELAY_POLKADEX_ID || 2040);
13
14export const QUARTZ_CHAIN = +(process.env.RELAY_QUARTZ_ID || 2095);
15export const STATEMINE_CHAIN = +(process.env.RELAY_STATEMINE_ID || 1000);
16export const KARURA_CHAIN = +(process.env.RELAY_KARURA_ID || 2000);
17export const MOONRIVER_CHAIN = +(process.env.RELAY_MOONRIVER_ID || 2023);
18export const SHIDEN_CHAIN = +(process.env.RELAY_SHIDEN_ID || 2007);
19
20export const relayUrl = config.relayUrl;
21export const statemintUrl = config.statemintUrl;
22export const statemineUrl = config.statemineUrl;
1123
12export const acalaUrl = config.acalaUrl;24export const acalaUrl = config.acalaUrl;
13export const moonbeamUrl = config.moonbeamUrl;25export const moonbeamUrl = config.moonbeamUrl;
14export const astarUrl = config.astarUrl;26export const astarUrl = config.astarUrl;
15export const polkadexUrl = config.polkadexUrl;27export const polkadexUrl = config.polkadexUrl;
28
29export const karuraUrl = config.karuraUrl;
30export const moonriverUrl = config.moonriverUrl;
31export const shidenUrl = config.shidenUrl;
1632
17export const SAFE_XCM_VERSION = 3;33export const SAFE_XCM_VERSION = 3;
34
1835
19export const maxWaitBlocks = 6;36export const RELAY_DECIMALS = 12;
2037export const STATEMINE_DECIMALS = 12;
38export const KARURA_DECIMALS = 12;
39export const SHIDEN_DECIMALS = 18n;
40export const QTZ_DECIMALS = 18n;
2141
22export const ASTAR_DECIMALS = 18n;42export const ASTAR_DECIMALS = 18n;
23export const UNQ_DECIMALS = 18n;43export const UNQ_DECIMALS = 18n;
44
45export const maxWaitBlocks = 6;
2446
25export const uniqueMultilocation = {47export const uniqueMultilocation = {
26 parents: 1,48 parents: 1,
47 && event.outcome.isUntrustedReserveLocation);69 && event.outcome.isUntrustedReserveLocation);
48};70};
71
72export const expectDownwardXcmNoPermission = async (helper: DevUniqueHelper) => {
73 // The correct messageHash for downward messages can't be reliably obtained
74 await helper.wait.expectEvent(maxWaitBlocks, Event.DmpQueue.ExecutedDownward, event => event.outcome.asIncomplete[1].isNoPermission);
75};
76
77export const expectDownwardXcmComplete = async (helper: DevUniqueHelper) => {
78 // The correct messageHash for downward messages can't be reliably obtained
79 await helper.wait.expectEvent(maxWaitBlocks, Event.DmpQueue.ExecutedDownward, event => event.outcome.isComplete);
80};
4981
50export const NETWORKS = {82export const NETWORKS = {
51 acala: usingAcalaPlaygrounds,83 acala: usingAcalaPlaygrounds,
52 astar: usingAstarPlaygrounds,84 astar: usingAstarPlaygrounds,
53 polkadex: usingPolkadexPlaygrounds,85 polkadex: usingPolkadexPlaygrounds,
54 moonbeam: usingMoonbeamPlaygrounds,86 moonbeam: usingMoonbeamPlaygrounds,
87 moonriver: usingMoonriverPlaygrounds,
88 karura: usingKaruraPlaygrounds,
89 shiden: usingShidenPlaygrounds,
55} as const;90} as const;
91type NetworkNames = keyof typeof NETWORKS;
92
93type NativeRuntime = 'opal' | 'quartz' | 'unique';
5694
57export function mapToChainId(networkName: keyof typeof NETWORKS) {95export function mapToChainId(networkName: keyof typeof NETWORKS): number {
58 switch (networkName) {96 switch (networkName) {
59 case 'acala':97 case 'acala':
60 return ACALA_CHAIN;98 return ACALA_CHAIN;
64 return MOONBEAM_CHAIN;102 return MOONBEAM_CHAIN;
65 case 'polkadex':103 case 'polkadex':
66 return POLKADEX_CHAIN;104 return POLKADEX_CHAIN;
105 case 'moonriver':
106 return MOONRIVER_CHAIN;
107 case 'karura':
108 return KARURA_CHAIN;
109 case 'shiden':
110 return SHIDEN_CHAIN;
67 }111 }
68}112}
69113
70export function mapToChainUrl(networkName: keyof typeof NETWORKS): string {114export function mapToChainUrl(networkName: NetworkNames): string {
71 switch (networkName) {115 switch (networkName) {
72 case 'acala':116 case 'acala':
73 return acalaUrl;117 return acalaUrl;
77 return moonbeamUrl;121 return moonbeamUrl;
78 case 'polkadex':122 case 'polkadex':
79 return polkadexUrl;123 return polkadexUrl;
124 case 'moonriver':
125 return moonriverUrl;
126 case 'karura':
127 return karuraUrl;
128 case 'shiden':
129 return shidenUrl;
80 }130 }
81}131}
132
133export function getDevPlayground(name: NetworkNames) {
134 return NETWORKS[name];
135}
136
137export const TRANSFER_AMOUNT = 2000000_000_000_000_000_000_000n;
138export const SENDER_BUDGET = 2n * TRANSFER_AMOUNT;
139export const SENDBACK_AMOUNT = TRANSFER_AMOUNT / 2n;
140export const STAYED_ON_TARGET_CHAIN = TRANSFER_AMOUNT - SENDBACK_AMOUNT;
141export const TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT = 100_000_000_000n;
82142
83export function getDevPlayground<T extends keyof typeof NETWORKS>(name: T) {143export class XcmTestHelper {
144 private _balanceUniqueTokenInit: bigint = 0n;
145 private _balanceUniqueTokenMiddle: bigint = 0n;
146 private _balanceUniqueTokenFinal: bigint = 0n;
147 private _unqFees: bigint = 0n;
148 private _nativeRuntime: NativeRuntime;
149
150 constructor(runtime: NativeRuntime) {
151 this._nativeRuntime = runtime;
152 }
153
154 private _getNativeId() {
155 switch (this._nativeRuntime) {
156 case 'opal':
157 // To-Do
158 return 1001;
159 case 'quartz':
160 return QUARTZ_CHAIN;
161 case 'unique':
162 return UNIQUE_CHAIN;
163 }
164 }
165
166 private _isAddress20FormatFor(network: NetworkNames) {
167 switch (network) {
168 case 'moonbeam':
169 case 'moonriver':
170 return true;
171 default:
172 return false;
173 }
174 }
175
176 private _runtimeVersionedMultilocation() {
177 return {
178 V3: {
179 parents: 1,
180 interior: {
181 X1: {
182 Parachain: this._getNativeId(),
183 },
184 },
185 },
186 };
187 }
188
189 private _uniqueChainMultilocationForRelay() {
190 return {
191 V3: {
192 parents: 0,
193 interior: {
194 X1: {Parachain: this._getNativeId()},
195 },
196 },
197 };
198 }
199
200 async sendUnqTo(
201 networkName: keyof typeof NETWORKS,
202 randomAccount: IKeyringPair,
203 randomAccountOnTargetChain = randomAccount,
204 ) {
205 const networkUrl = mapToChainUrl(networkName);
206 const targetPlayground = getDevPlayground(networkName);
207 await usingPlaygrounds(async (helper) => {
208 this._balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
209 const destination = {
210 V2: {
211 parents: 1,
212 interior: {
213 X1: {
214 Parachain: mapToChainId(networkName),
215 },
216 },
217 },
218 };
219
220 const beneficiary = {
221 V2: {
222 parents: 0,
223 interior: {
224 X1: (
225 this._isAddress20FormatFor(networkName) ?
226 {
227 AccountKey20: {
228 network: 'Any',
229 key: randomAccountOnTargetChain.address,
230 },
231 }
232 :
233 {
234 AccountId32: {
235 network: 'Any',
236 id: randomAccountOnTargetChain.addressRaw,
237 },
238 }
239 ),
240 },
241 },
242 };
243
244 const assets = {
245 V2: [
246 {
247 id: {
248 Concrete: {
249 parents: 0,
250 interior: 'Here',
251 },
252 },
253 fun: {
254 Fungible: TRANSFER_AMOUNT,
255 },
256 },
257 ],
258 };
259 const feeAssetItem = 0;
260
261 await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');
262 const messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
263 this._balanceUniqueTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);
264
265 this._unqFees = this._balanceUniqueTokenInit - this._balanceUniqueTokenMiddle - TRANSFER_AMOUNT;
266 console.log('[%s -> %s] transaction fees: %s', this._nativeRuntime, networkName, helper.util.bigIntToDecimals(this._unqFees));
267 expect(this._unqFees > 0n, 'Negative fees, looks like nothing was transferred').to.be.true;
268
269 await targetPlayground(networkUrl, async (helper) => {
270 /*
271 Since only the parachain part of the Polkadex
272 infrastructure is launched (without their
273 solochain validators), processing incoming
274 assets will lead to an error.
275 This error indicates that the Polkadex chain
276 received a message from the Unique network,
277 since the hash is being checked to ensure
278 it matches what was sent.
279 */
280 if(networkName == 'polkadex') {
281 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash);
282 } else {
283 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == messageSent.messageHash);
284 }
285 });
286
287 });
288 }
289
290 async sendUnqBack(
291 networkName: keyof typeof NETWORKS,
292 sudoer: IKeyringPair,
293 randomAccountOnUnq: IKeyringPair,
294 ) {
295 const networkUrl = mapToChainUrl(networkName);
296
297 const targetPlayground = getDevPlayground(networkName);
298 await usingPlaygrounds(async (helper) => {
299
300 const xcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
301 randomAccountOnUnq.addressRaw,
302 {
303 Concrete: {
304 parents: 1,
305 interior: {
306 X1: {Parachain: this._getNativeId()},
307 },
308 },
309 },
310 SENDBACK_AMOUNT,
311 );
312
313 let xcmProgramSent: any;
314
315
316 await targetPlayground(networkUrl, async (helper) => {
317 if('getSudo' in helper) {
318 await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), xcmProgram);
319 xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
320 } else if('fastDemocracy' in helper) {
321 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), xcmProgram]);
322 // Needed to bypass the call filter.
323 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
324 await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
325 xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
326 }
327 });
328
329 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == xcmProgramSent.messageHash);
330
331 this._balanceUniqueTokenFinal = await helper.balance.getSubstrate(randomAccountOnUnq.address);
332
333 expect(this._balanceUniqueTokenFinal).to.be.equal(this._balanceUniqueTokenInit - this._unqFees - STAYED_ON_TARGET_CHAIN);
334
335 });
336 }
337
338 async sendOnlyOwnedBalance(
339 networkName: keyof typeof NETWORKS,
340 sudoer: IKeyringPair,
341 ) {
342 const networkUrl = mapToChainUrl(networkName);
343 const targetPlayground = getDevPlayground(networkName);
344
345 const targetChainBalance = 10000n * (10n ** UNQ_DECIMALS);
346
347 await usingPlaygrounds(async (helper) => {
348 const targetChainSovereignAccount = helper.address.paraSiblingSovereignAccount(mapToChainId(networkName));
349 await helper.getSudo().balance.setBalanceSubstrate(sudoer, targetChainSovereignAccount, targetChainBalance);
350 const moreThanTargetChainHas = 2n * targetChainBalance;
351
352 const targetAccount = helper.arrange.createEmptyAccount();
353
354 const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
355 targetAccount.addressRaw,
356 {
357 Concrete: {
358 parents: 0,
359 interior: 'Here',
360 },
361 },
362 moreThanTargetChainHas,
363 );
364
365 let maliciousXcmProgramSent: any;
366
367
368 await targetPlayground(networkUrl, async (helper) => {
369 if('getSudo' in helper) {
370 await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), maliciousXcmProgram);
371 maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
372 } else if('fastDemocracy' in helper) {
373 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgram]);
374 // Needed to bypass the call filter.
375 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
376 await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
377 maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
378 }
379 });
380
381 await expectFailedToTransact(helper, maliciousXcmProgramSent);
382
383 const targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
384 expect(targetAccountBalance).to.be.equal(0n);
385 });
386 }
387
388 async rejectReserveTransferUNQfrom(networkName: keyof typeof NETWORKS, sudoer: IKeyringPair) {
389 const networkUrl = mapToChainUrl(networkName);
390 const targetPlayground = getDevPlayground(networkName);
391
392 await usingPlaygrounds(async (helper) => {
393 const testAmount = 10_000n * (10n ** UNQ_DECIMALS);
394 const targetAccount = helper.arrange.createEmptyAccount();
395
396 const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
397 targetAccount.addressRaw,
398 {
399 Concrete: {
400 parents: 1,
401 interior: {
402 X1: {
403 Parachain: this._getNativeId(),
404 },
405 },
406 },
407 },
408 testAmount,
409 );
410
411 const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(
412 targetAccount.addressRaw,
413 {
414 Concrete: {
415 parents: 0,
416 interior: 'Here',
417 },
418 },
419 testAmount,
420 );
421
422 let maliciousXcmProgramFullIdSent: any;
423 let maliciousXcmProgramHereIdSent: any;
424 const maxWaitBlocks = 3;
425
426 // Try to trick Unique using full UNQ identification
427 await targetPlayground(networkUrl, async (helper) => {
428 if('getSudo' in helper) {
429 await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId);
430 maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
431 }
432 // Moonbeam case
433 else if('fastDemocracy' in helper) {
434 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId]);
435 // Needed to bypass the call filter.
436 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
437 await helper.fastDemocracy.executeProposal(`${networkName} try to act like a reserve location for UNQ using path asset identification`,batchCall);
438
439 maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
440 }
441 });
442
443
444 await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramFullIdSent);
445
446 let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
447 expect(accountBalance).to.be.equal(0n);
448
449 // Try to trick Unique using shortened UNQ identification
450 await targetPlayground(networkUrl, async (helper) => {
451 if('getSudo' in helper) {
452 await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), maliciousXcmProgramHereId);
453 maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
454 }
455 else if('fastDemocracy' in helper) {
456 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgramHereId]);
457 // Needed to bypass the call filter.
458 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
459 await helper.fastDemocracy.executeProposal(`${networkName} try to act like a reserve location for UNQ using "here" asset identification`, batchCall);
460
461 maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
462 }
463 });
464
465 await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramHereIdSent);
466
467 accountBalance = await helper.balance.getSubstrate(targetAccount.address);
468 expect(accountBalance).to.be.equal(0n);
469 });
470 }
471
472 async rejectNativeTokensFrom(networkName: keyof typeof NETWORKS, sudoerOnTargetChain: IKeyringPair) {
473 const networkUrl = mapToChainUrl(networkName);
474 const targetPlayground = getDevPlayground(networkName);
475 let messageSent: any;
476
477 await usingPlaygrounds(async (helper) => {
478 const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
479 helper.arrange.createEmptyAccount().addressRaw,
480 {
481 Concrete: {
482 parents: 1,
483 interior: {
484 X1: {
485 Parachain: mapToChainId(networkName),
486 },
487 },
488 },
489 },
490 TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT,
491 );
492 await targetPlayground(networkUrl, async (helper) => {
493 if('getSudo' in helper) {
494 await helper.getSudo().xcm.send(sudoerOnTargetChain, this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId);
495 messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
496 } else if('fastDemocracy' in helper) {
497 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId]);
498 // Needed to bypass the call filter.
499 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
500 await helper.fastDemocracy.executeProposal(`${networkName} sending native tokens to the Unique via fast democracy`, batchCall);
501
502 messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
503 }
504 });
505 await expectFailedToTransact(helper, messageSent);
506 });
507 }
508
509 private async _relayXcmTransactSetStorage(variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch') {
510 // eslint-disable-next-line require-await
84 return NETWORKS[name];511 return await usingPlaygrounds(async (helper) => {
512 const relayForceKV = () => {
513 const random = Math.random();
514 const key = `relay-forced-key (instance: ${random})`;
515 const val = `relay-forced-value (instance: ${random})`;
516 const call = helper.constructApiCall('api.tx.system.setStorage', [[[key, val]]]).method.toHex();
517
518 return {
519 call,
520 key,
521 val,
522 };
523 };
524
525 if(variant == 'plain') {
526 const kv = relayForceKV();
527 return {
528 program: helper.arrange.makeUnpaidSudoTransactProgram({
529 weightMultiplier: 1,
530 call: kv.call,
531 }),
532 kvs: [kv],
533 };
534 } else {
535 const kv0 = relayForceKV();
536 const kv1 = relayForceKV();
537
538 const batchCall = helper.constructApiCall(`api.tx.utility.${variant}`, [[kv0.call, kv1.call]]).method.toHex();
539 return {
540 program: helper.arrange.makeUnpaidSudoTransactProgram({
541 weightMultiplier: 2,
542 call: batchCall,
543 }),
544 kvs: [kv0, kv1],
545 };
546 }
547 });
85}548 }
549
550 async relayIsPermittedToSetStorage(relaySudoer: IKeyringPair, variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch') {
551 const {program, kvs} = await this._relayXcmTransactSetStorage(variant);
552
553 await usingRelayPlaygrounds(relayUrl, async (helper) => {
554 await helper.getSudo().executeExtrinsic(relaySudoer, 'api.tx.xcmPallet.send', [
555 this._uniqueChainMultilocationForRelay(),
556 program,
557 ]);
558 });
559
560 await usingPlaygrounds(async (helper) => {
561 await expectDownwardXcmComplete(helper);
562
563 for(const kv of kvs) {
564 const forcedValue = await helper.callRpc('api.rpc.state.getStorage', [kv.key]);
565 expect(hexToString(forcedValue.toHex())).to.be.equal(kv.val);
566 }
567 });
568 }
569
570 private async _relayXcmTransactSetBalance(variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch' | 'dispatchAs') {
571 // eslint-disable-next-line require-await
572 return await usingPlaygrounds(async (helper) => {
573 const emptyAccount = helper.arrange.createEmptyAccount().address;
574
575 const forceSetBalanceCall = helper.constructApiCall('api.tx.balances.forceSetBalance', [emptyAccount, 10_000n]).method.toHex();
576
577 let call;
578
579 if(variant == 'plain') {
580 call = forceSetBalanceCall;
581
582 } else if(variant == 'dispatchAs') {
583 call = helper.constructApiCall('api.tx.utility.dispatchAs', [
584 {
585 system: 'Root',
586 },
587 forceSetBalanceCall,
588 ]).method.toHex();
589 } else {
590 call = helper.constructApiCall(`api.tx.utility.${variant}`, [[forceSetBalanceCall]]).method.toHex();
591 }
592
593 return {
594 program: helper.arrange.makeUnpaidSudoTransactProgram({
595 weightMultiplier: 1,
596 call,
597 }),
598 emptyAccount,
599 };
600 });
601 }
602
603 async relayIsNotPermittedToSetBalance(
604 relaySudoer: IKeyringPair,
605 variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch' | 'dispatchAs',
606 ) {
607 const {program, emptyAccount} = await this._relayXcmTransactSetBalance(variant);
608
609 await usingRelayPlaygrounds(relayUrl, async (helper) => {
610 await helper.getSudo().executeExtrinsic(relaySudoer, 'api.tx.xcmPallet.send', [
611 this._uniqueChainMultilocationForRelay(),
612 program,
613 ]);
614 });
615
616 await usingPlaygrounds(async (helper) => {
617 await expectDownwardXcmNoPermission(helper);
618 expect(await helper.balance.getSubstrate(emptyAccount)).to.be.equal(0n);
619 });
620 }
621}
622
modifiedtests/src/xcm/xcmQuartz.test.tsdiffbeforeafterboth
--- 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 <http://www.gnu.org/licenses/>.
 
 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);
     });
modifiedtests/src/xcm/xcmUnique.test.tsdiffbeforeafterboth
--- 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),
         );