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
before · tests/src/xcm/lowLevelXcmUnique.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import config from '../config';19import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds} from '../util';20import {Event} from '../util/playgrounds/unique.dev';21import {nToBigInt} from '@polkadot/util';22import {hexToString} from '@polkadot/util';23import {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';242526const TRANSFER_AMOUNT = 2000000_000_000_000_000_000_000n;27const SENDER_BUDGET = 2n * TRANSFER_AMOUNT;28const SENDBACK_AMOUNT = TRANSFER_AMOUNT / 2n;29const STAYED_ON_TARGET_CHAIN = TRANSFER_AMOUNT - SENDBACK_AMOUNT;30const TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT = 100_000_000_000n;3132let balanceUniqueTokenInit: bigint;33let balanceUniqueTokenMiddle: bigint;34let balanceUniqueTokenFinal: bigint;35let unqFees: bigint;363738async function genericSendUnqTo(39  networkName: keyof typeof NETWORKS,40  randomAccount: IKeyringPair,41  randomAccountOnTargetChain = randomAccount,42) {43  const networkUrl = mapToChainUrl(networkName);44  const targetPlayground = getDevPlayground(networkName);45  await usingPlaygrounds(async (helper) => {46    balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);47    const destination = {48      V2: {49        parents: 1,50        interior: {51          X1: {52            Parachain: mapToChainId(networkName),53          },54        },55      },56    };5758    const beneficiary = {59      V2: {60        parents: 0,61        interior: {62          X1: (63            networkName == 'moonbeam' ?64              {65                AccountKey20: {66                  network: 'Any',67                  key: randomAccountOnTargetChain.address,68                },69              }70              :71              {72                AccountId32: {73                  network: 'Any',74                  id: randomAccountOnTargetChain.addressRaw,75                },76              }77          ),78        },79      },80    };8182    const assets = {83      V2: [84        {85          id: {86            Concrete: {87              parents: 0,88              interior: 'Here',89            },90          },91          fun: {92            Fungible: TRANSFER_AMOUNT,93          },94        },95      ],96    };97    const feeAssetItem = 0;9899    await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');100    const messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);101    balanceUniqueTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);102103    unqFees = balanceUniqueTokenInit - balanceUniqueTokenMiddle - TRANSFER_AMOUNT;104    console.log('[Unique -> %s] transaction fees on Unique: %s UNQ', networkName, helper.util.bigIntToDecimals(unqFees));105    expect(unqFees > 0n, 'Negative fees UNQ, looks like nothing was transferred').to.be.true;106107    await targetPlayground(networkUrl, async (helper) => {108      /*109        Since only the parachain part of the Polkadex110        infrastructure is launched (without their111        solochain validators), processing incoming112        assets will lead to an error.113        This error indicates that the Polkadex chain114        received a message from the Unique network,115        since the hash is being checked to ensure116        it matches what was sent.117      */118      if(networkName == 'polkadex') {119        await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash);120      } else {121        await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == messageSent.messageHash);122      }123    });124125  });126}127128async function genericSendUnqBack(129  networkName: keyof typeof NETWORKS,130  sudoer: IKeyringPair,131  randomAccountOnUnq: IKeyringPair,132) {133  const networkUrl = mapToChainUrl(networkName);134135  const targetPlayground = getDevPlayground(networkName);136  await usingPlaygrounds(async (helper) => {137138    const xcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(139      randomAccountOnUnq.addressRaw,140      uniqueAssetId,141      SENDBACK_AMOUNT,142    );143144    let xcmProgramSent: any;145146147    await targetPlayground(networkUrl, async (helper) => {148      if('getSudo' in helper) {149        await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, xcmProgram);150        xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);151      } else if('fastDemocracy' in helper) {152        const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, xcmProgram]);153        // Needed to bypass the call filter.154        const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);155        await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);156        xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);157      }158    });159160    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == xcmProgramSent.messageHash);161162    balanceUniqueTokenFinal = await helper.balance.getSubstrate(randomAccountOnUnq.address);163164    expect(balanceUniqueTokenFinal).to.be.equal(balanceUniqueTokenInit - unqFees - STAYED_ON_TARGET_CHAIN);165166  });167}168169async function genericSendOnlyOwnedBalance(170  networkName: keyof typeof NETWORKS,171  sudoer: IKeyringPair,172) {173  const networkUrl = mapToChainUrl(networkName);174  const targetPlayground = getDevPlayground(networkName);175176  const targetChainBalance = 10000n * (10n ** UNQ_DECIMALS);177178  await usingPlaygrounds(async (helper) => {179    const targetChainSovereignAccount = helper.address.paraSiblingSovereignAccount(mapToChainId(networkName));180    await helper.getSudo().balance.setBalanceSubstrate(sudoer, targetChainSovereignAccount, targetChainBalance);181    const moreThanTargetChainHas = 2n * targetChainBalance;182183    const targetAccount = helper.arrange.createEmptyAccount();184185    const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(186      targetAccount.addressRaw,187      {188        Concrete: {189          parents: 0,190          interior: 'Here',191        },192      },193      moreThanTargetChainHas,194    );195196    let maliciousXcmProgramSent: any;197198199    await targetPlayground(networkUrl, async (helper) => {200      if('getSudo' in helper) {201        await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgram);202        maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);203      } else if('fastDemocracy' in helper) {204        const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgram]);205        // Needed to bypass the call filter.206        const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);207        await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);208        maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);209      }210    });211212    await expectFailedToTransact(helper, maliciousXcmProgramSent);213214    const targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);215    expect(targetAccountBalance).to.be.equal(0n);216  });217}218219async function genericReserveTransferUNQfrom(netwokrName: keyof typeof NETWORKS, sudoer: IKeyringPair) {220  const networkUrl = mapToChainUrl(netwokrName);221  const targetPlayground = getDevPlayground(netwokrName);222223  await usingPlaygrounds(async (helper) => {224    const testAmount = 10_000n * (10n ** UNQ_DECIMALS);225    const targetAccount = helper.arrange.createEmptyAccount();226227    const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(228      targetAccount.addressRaw,229      uniqueAssetId,230      testAmount,231    );232233    const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(234      targetAccount.addressRaw,235      {236        Concrete: {237          parents: 0,238          interior: 'Here',239        },240      },241      testAmount,242    );243244    let maliciousXcmProgramFullIdSent: any;245    let maliciousXcmProgramHereIdSent: any;246    const maxWaitBlocks = 3;247248    // Try to trick Unique using full UNQ identification249    await targetPlayground(networkUrl, async (helper) => {250      if('getSudo' in helper) {251        await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramFullId);252        maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);253      }254      // Moonbeam case255      else if('fastDemocracy' in helper) {256        const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]);257        // Needed to bypass the call filter.258        const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);259        await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using path asset identification`,batchCall);260261        maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);262      }263    });264265266    await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramFullIdSent);267268    let accountBalance = await helper.balance.getSubstrate(targetAccount.address);269    expect(accountBalance).to.be.equal(0n);270271    // Try to trick Unique using shortened UNQ identification272    await targetPlayground(networkUrl, async (helper) => {273      if('getSudo' in helper) {274        await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramHereId);275        maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);276      }277      else if('fastDemocracy' in helper) {278        const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramHereId]);279        // Needed to bypass the call filter.280        const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);281        await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using "here" asset identification`, batchCall);282283        maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);284      }285    });286287    await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramHereIdSent);288289    accountBalance = await helper.balance.getSubstrate(targetAccount.address);290    expect(accountBalance).to.be.equal(0n);291  });292}293294async function genericRejectNativeTokensFrom(networkName: keyof typeof NETWORKS, sudoerOnTargetChain: IKeyringPair) {295  const networkUrl = mapToChainUrl(networkName);296  const targetPlayground = getDevPlayground(networkName);297  let messageSent: any;298299  await usingPlaygrounds(async (helper) => {300    const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(301      helper.arrange.createEmptyAccount().addressRaw,302      {303        Concrete: {304          parents: 1,305          interior: {306            X1: {307              Parachain: mapToChainId(networkName),308            },309          },310        },311      },312      TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT,313    );314    await targetPlayground(networkUrl, async (helper) => {315      if('getSudo' in helper) {316        await helper.getSudo().xcm.send(sudoerOnTargetChain, uniqueVersionedMultilocation, maliciousXcmProgramFullId);317        messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);318      } else if('fastDemocracy' in helper) {319        const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]);320        // Needed to bypass the call filter.321        const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);322        await helper.fastDemocracy.executeProposal(`${networkName} sending native tokens to the Unique via fast democracy`, batchCall);323324        messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);325      }326    });327    await expectFailedToTransact(helper, messageSent);328  });329}330331332describeXCM('[XCMLL] Integration test: Exchanging tokens with Acala', () => {333  let alice: IKeyringPair;334  let randomAccount: IKeyringPair;335336  before(async () => {337    await usingPlaygrounds(async (helper, privateKey) => {338      alice = await privateKey('//Alice');339      console.log(config.acalaUrl);340      randomAccount = helper.arrange.createEmptyAccount();341342      // Set the default version to wrap the first message to other chains.343      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);344    });345346    await usingAcalaPlaygrounds(acalaUrl, async (helper) => {347      const destination = {348        V2: {349          parents: 1,350          interior: {351            X1: {352              Parachain: UNIQUE_CHAIN,353            },354          },355        },356      };357358      const metadata = {359        name: 'Unique Network',360        symbol: 'UNQ',361        decimals: 18,362        minimalBalance: 1250_000_000_000_000_000n,363      };364      const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v] : [any, any]) =>365        hexToString(v.toJSON()['symbol'])) as string[];366367      if(!assets.includes('UNQ')) {368        await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);369      } else {370        console.log('UNQ token already registered on Acala assetRegistry pallet');371      }372      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);373    });374375    await usingPlaygrounds(async (helper) => {376      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);377      balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);378    });379  });380381  itSub('Should connect and send UNQ to Acala', async () => {382    await genericSendUnqTo('acala', randomAccount);383  });384385  itSub('Should connect to Acala and send UNQ back', async () => {386    await genericSendUnqBack('acala', alice, randomAccount);387  });388389  itSub('Acala can send only up to its balance', async () => {390    await genericSendOnlyOwnedBalance('acala', alice);391  });392393  itSub('Should not accept reserve transfer of UNQ from Acala', async () => {394    await genericReserveTransferUNQfrom('acala', alice);395  });396});397398describeXCM('[XCMLL] Integration test: Exchanging tokens with Polkadex', () => {399  let alice: IKeyringPair;400  let randomAccount: IKeyringPair;401402  before(async () => {403    await usingPlaygrounds(async (helper, privateKey) => {404      alice = await privateKey('//Alice');405      randomAccount = helper.arrange.createEmptyAccount();406407      // Set the default version to wrap the first message to other chains.408      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);409    });410411    await usingPolkadexPlaygrounds(polkadexUrl, async (helper) => {412      const isWhitelisted = ((await helper.callRpc('api.query.xcmHelper.whitelistedTokens', []))413        .toJSON() as [])414        .map(nToBigInt).length != 0;415      /*416      Check whether the Unique token has been added417      to the whitelist, since an error will occur418      if it is added again. Needed for debugging419      when this test is run multiple times.420      */421      if(isWhitelisted) {422        console.log('UNQ token is already whitelisted on Polkadex');423      } else {424        await helper.getSudo().xcmHelper.whitelistToken(alice, uniqueAssetId);425      }426427      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);428    });429430    await usingPlaygrounds(async (helper) => {431      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);432      balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);433    });434  });435436  itSub('Should connect and send UNQ to Polkadex', async () => {437    await genericSendUnqTo('polkadex', randomAccount);438  });439440441  itSub('Should connect to Polkadex and send UNQ back', async () => {442    await genericSendUnqBack('polkadex', alice, randomAccount);443  });444445  itSub('Polkadex can send only up to its balance', async () => {446    await genericSendOnlyOwnedBalance('polkadex', alice);447  });448449  itSub('Should not accept reserve transfer of UNQ from Polkadex', async () => {450    await genericReserveTransferUNQfrom('polkadex', alice);451  });452});453454// These tests are relevant only when455// the the corresponding foreign assets are not registered456describeXCM('[XCMLL] Integration test: Unique rejects non-native tokens', () => {457  let alice: IKeyringPair;458459  before(async () => {460    await usingPlaygrounds(async (helper, privateKey) => {461      alice = await privateKey('//Alice');462463      // Set the default version to wrap the first message to other chains.464      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);465    });466  });467468  itSub('Unique rejects ACA tokens from Acala', async () => {469    await genericRejectNativeTokensFrom('acala', alice);470  });471472  itSub('Unique rejects GLMR tokens from Moonbeam', async () => {473    await genericRejectNativeTokensFrom('moonbeam', alice);474  });475476  itSub('Unique rejects ASTR tokens from Astar', async () => {477    await genericRejectNativeTokensFrom('astar', alice);478  });479480  itSub('Unique rejects PDX tokens from Polkadex', async () => {481    await genericRejectNativeTokensFrom('polkadex', alice);482  });483});484485describeXCM('[XCMLL] Integration test: Exchanging UNQ with Moonbeam', () => {486  // Unique constants487  let alice: IKeyringPair;488  let uniqueAssetLocation;489490  let randomAccountUnique: IKeyringPair;491  let randomAccountMoonbeam: IKeyringPair;492493  // Moonbeam constants494  let assetId: string;495496  const uniqueAssetMetadata = {497    name: 'xcUnique',498    symbol: 'xcUNQ',499    decimals: 18,500    isFrozen: false,501    minimalBalance: 1n,502  };503504505  before(async () => {506    await usingPlaygrounds(async (helper, privateKey) => {507      alice = await privateKey('//Alice');508      [randomAccountUnique] = await helper.arrange.createAccounts([0n], alice);509510511      // Set the default version to wrap the first message to other chains.512      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);513    });514515    await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => {516      const alithAccount = helper.account.alithAccount();517      const baltatharAccount = helper.account.baltatharAccount();518      const dorothyAccount = helper.account.dorothyAccount();519520      randomAccountMoonbeam = helper.account.create();521522      // >>> Sponsoring Dorothy >>>523      console.log('Sponsoring Dorothy.......');524      await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n);525      console.log('Sponsoring Dorothy.......DONE');526      // <<< Sponsoring Dorothy <<<527      uniqueAssetLocation = {528        XCM: {529          parents: 1,530          interior: {X1: {Parachain: UNIQUE_CHAIN}},531        },532      };533      const existentialDeposit = 1n;534      const isSufficient = true;535      const unitsPerSecond = 1n;536      const numAssetsWeightHint = 0;537538      if((await helper.assetManager.assetTypeId(uniqueAssetLocation)).toJSON()) {539        console.log('Unique asset already registered on Moonbeam');540      } else {541        const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({542          location: uniqueAssetLocation,543          metadata: uniqueAssetMetadata,544          existentialDeposit,545          isSufficient,546          unitsPerSecond,547          numAssetsWeightHint,548        });549550        console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);551552        await helper.fastDemocracy.executeProposal('register UNQ foreign asset', encodedProposal);553      }554555      // >>> Acquire Unique AssetId Info on Moonbeam >>>556      console.log('Acquire Unique AssetId Info on Moonbeam.......');557558      assetId = (await helper.assetManager.assetTypeId(uniqueAssetLocation)).toString();559560      console.log('UNQ asset ID is %s', assetId);561      console.log('Acquire Unique AssetId Info on Moonbeam.......DONE');562563      // >>> Sponsoring random Account >>>564      console.log('Sponsoring random Account.......');565      await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonbeam.address, 11_000_000_000_000_000_000n);566      console.log('Sponsoring random Account.......DONE');567      // <<< Sponsoring random Account <<<568    });569570    await usingPlaygrounds(async (helper) => {571      await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET);572      balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccountUnique.address);573    });574  });575576  itSub('Should connect and send UNQ to Moonbeam', async () => {577    await genericSendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);578  });579580  itSub('Should connect to Moonbeam and send UNQ back', async () => {581    await genericSendUnqBack('moonbeam', alice, randomAccountUnique);582  });583584  itSub('Moonbeam can send only up to its balance', async () => {585    await genericSendOnlyOwnedBalance('moonbeam', alice);586  });587588  itSub('Should not accept reserve transfer of UNQ from Moonbeam', async () => {589    await genericReserveTransferUNQfrom('moonbeam', alice);590  });591});592593describeXCM('[XCMLL] Integration test: Exchanging tokens with Astar', () => {594  let alice: IKeyringPair;595  let randomAccount: IKeyringPair;596597  const UNQ_ASSET_ID_ON_ASTAR = 1;598  const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n;599600  // Unique -> Astar601  const astarInitialBalance = 1n * (10n ** ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Astar.602  const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?603604  before(async () => {605    await usingPlaygrounds(async (helper, privateKey) => {606      alice = await privateKey('//Alice');607      randomAccount = helper.arrange.createEmptyAccount();608      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);609      console.log('randomAccount', randomAccount.address);610611      // Set the default version to wrap the first message to other chains.612      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);613    });614615    await usingAstarPlaygrounds(astarUrl, async (helper) => {616      if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) {617        console.log('1. Create foreign asset and metadata');618        // TODO update metadata with values from production619        await helper.assets.create(620          alice,621          UNQ_ASSET_ID_ON_ASTAR,622          alice.address,623          UNQ_MINIMAL_BALANCE_ON_ASTAR,624        );625626        await helper.assets.setMetadata(627          alice,628          UNQ_ASSET_ID_ON_ASTAR,629          'Cross chain UNQ',630          'xcUNQ',631          Number(UNQ_DECIMALS),632        );633634        console.log('2. Register asset location on Astar');635        const assetLocation = {636          V2: {637            parents: 1,638            interior: {639              X1: {640                Parachain: UNIQUE_CHAIN,641              },642            },643          },644        };645646        await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, UNQ_ASSET_ID_ON_ASTAR]);647648        console.log('3. Set UNQ payment for XCM execution on Astar');649        await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);650      } else {651        console.log('UNQ is already registered on Astar');652      }653      console.log('4. Transfer 1 ASTR to recipient to create the account (needed due to existential balance)');654      await helper.balance.transferToSubstrate(alice, randomAccount.address, astarInitialBalance);655    });656  });657658  itSub('Should connect and send UNQ to Astar', async () => {659    await genericSendUnqTo('astar', randomAccount);660  });661662  itSub('Should connect to Astar and send UNQ back', async () => {663    await genericSendUnqBack('astar', alice, randomAccount);664  });665666  itSub('Astar can send only up to its balance', async () => {667    await genericSendOnlyOwnedBalance('astar', alice);668  });669670  itSub('Should not accept reserve transfer of UNQ from Astar', async () => {671    await genericReserveTransferUNQfrom('astar', alice);672  });673});
after · tests/src/xcm/lowLevelXcmUnique.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import config from '../config';19import {itSub, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds} from '../util';20import {nToBigInt} from '@polkadot/util';21import {hexToString} from '@polkadot/util';22import {ASTAR_DECIMALS, SAFE_XCM_VERSION, SENDER_BUDGET, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper, acalaUrl, astarUrl,  moonbeamUrl, polkadexUrl, relayUrl, uniqueAssetId} from './xcm.types';2324const testHelper = new XcmTestHelper('unique');2526272829describeXCM('[XCMLL] Integration test: Exchanging tokens with Acala', () => {30  let alice: IKeyringPair;31  let randomAccount: IKeyringPair;3233  before(async () => {34    await usingPlaygrounds(async (helper, privateKey) => {35      alice = await privateKey('//Alice');36      console.log(config.acalaUrl);37      randomAccount = helper.arrange.createEmptyAccount();3839      // Set the default version to wrap the first message to other chains.40      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);41    });4243    await usingAcalaPlaygrounds(acalaUrl, async (helper) => {44      const destination = {45        V2: {46          parents: 1,47          interior: {48            X1: {49              Parachain: UNIQUE_CHAIN,50            },51          },52        },53      };5455      const metadata = {56        name: 'Unique Network',57        symbol: 'UNQ',58        decimals: 18,59        minimalBalance: 1250_000_000_000_000_000n,60      };61      const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v] : [any, any]) =>62        hexToString(v.toJSON()['symbol'])) as string[];6364      if(!assets.includes('UNQ')) {65        await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);66      } else {67        console.log('UNQ token already registered on Acala assetRegistry pallet');68      }69      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);70    });7172    await usingPlaygrounds(async (helper) => {73      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);74    });75  });7677  itSub('Should connect and send UNQ to Acala', async () => {78    await testHelper.sendUnqTo('acala', randomAccount);79  });8081  itSub('Should connect to Acala and send UNQ back', async () => {82    await testHelper.sendUnqBack('acala', alice, randomAccount);83  });8485  itSub('Acala can send only up to its balance', async () => {86    await testHelper.sendOnlyOwnedBalance('acala', alice);87  });8889  itSub('Should not accept reserve transfer of UNQ from Acala', async () => {90    await testHelper.rejectReserveTransferUNQfrom('acala', alice);91  });92});9394describeXCM('[XCMLL] Integration test: Exchanging tokens with Polkadex', () => {95  let alice: IKeyringPair;96  let randomAccount: IKeyringPair;9798  before(async () => {99    await usingPlaygrounds(async (helper, privateKey) => {100      alice = await privateKey('//Alice');101      randomAccount = helper.arrange.createEmptyAccount();102103      // Set the default version to wrap the first message to other chains.104      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);105    });106107    await usingPolkadexPlaygrounds(polkadexUrl, async (helper) => {108      const isWhitelisted = ((await helper.callRpc('api.query.xcmHelper.whitelistedTokens', []))109        .toJSON() as [])110        .map(nToBigInt).length != 0;111      /*112      Check whether the Unique token has been added113      to the whitelist, since an error will occur114      if it is added again. Needed for debugging115      when this test is run multiple times.116      */117      if(isWhitelisted) {118        console.log('UNQ token is already whitelisted on Polkadex');119      } else {120        await helper.getSudo().xcmHelper.whitelistToken(alice, uniqueAssetId);121      }122123      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);124    });125126    await usingPlaygrounds(async (helper) => {127      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);128    });129  });130131  itSub('Should connect and send UNQ to Polkadex', async () => {132    await testHelper.sendUnqTo('polkadex', randomAccount);133  });134135136  itSub('Should connect to Polkadex and send UNQ back', async () => {137    await testHelper.sendUnqBack('polkadex', alice, randomAccount);138  });139140  itSub('Polkadex can send only up to its balance', async () => {141    await testHelper.sendOnlyOwnedBalance('polkadex', alice);142  });143144  itSub('Should not accept reserve transfer of UNQ from Polkadex', async () => {145    await testHelper.rejectReserveTransferUNQfrom('polkadex', alice);146  });147});148149// These tests are relevant only when150// the the corresponding foreign assets are not registered151describeXCM('[XCMLL] Integration test: Unique rejects non-native tokens', () => {152  let alice: IKeyringPair;153154  before(async () => {155    await usingPlaygrounds(async (helper, privateKey) => {156      alice = await privateKey('//Alice');157158      // Set the default version to wrap the first message to other chains.159      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);160    });161  });162163  itSub('Unique rejects ACA tokens from Acala', async () => {164    await testHelper.rejectNativeTokensFrom('acala', alice);165  });166167  itSub('Unique rejects GLMR tokens from Moonbeam', async () => {168    await testHelper.rejectNativeTokensFrom('moonbeam', alice);169  });170171  itSub('Unique rejects ASTR tokens from Astar', async () => {172    await testHelper.rejectNativeTokensFrom('astar', alice);173  });174175  itSub('Unique rejects PDX tokens from Polkadex', async () => {176    await testHelper.rejectNativeTokensFrom('polkadex', alice);177  });178});179180describeXCM('[XCMLL] Integration test: Exchanging UNQ with Moonbeam', () => {181  // Unique constants182  let alice: IKeyringPair;183  let uniqueAssetLocation;184185  let randomAccountUnique: IKeyringPair;186  let randomAccountMoonbeam: IKeyringPair;187188  // Moonbeam constants189  let assetId: string;190191  const uniqueAssetMetadata = {192    name: 'xcUnique',193    symbol: 'xcUNQ',194    decimals: 18,195    isFrozen: false,196    minimalBalance: 1n,197  };198199200  before(async () => {201    await usingPlaygrounds(async (helper, privateKey) => {202      alice = await privateKey('//Alice');203      [randomAccountUnique] = await helper.arrange.createAccounts([0n], alice);204205206      // Set the default version to wrap the first message to other chains.207      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);208    });209210    await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => {211      const alithAccount = helper.account.alithAccount();212      const baltatharAccount = helper.account.baltatharAccount();213      const dorothyAccount = helper.account.dorothyAccount();214215      randomAccountMoonbeam = helper.account.create();216217      // >>> Sponsoring Dorothy >>>218      console.log('Sponsoring Dorothy.......');219      await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n);220      console.log('Sponsoring Dorothy.......DONE');221      // <<< Sponsoring Dorothy <<<222      uniqueAssetLocation = {223        XCM: {224          parents: 1,225          interior: {X1: {Parachain: UNIQUE_CHAIN}},226        },227      };228      const existentialDeposit = 1n;229      const isSufficient = true;230      const unitsPerSecond = 1n;231      const numAssetsWeightHint = 0;232233      if((await helper.assetManager.assetTypeId(uniqueAssetLocation)).toJSON()) {234        console.log('Unique asset already registered on Moonbeam');235      } else {236        const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({237          location: uniqueAssetLocation,238          metadata: uniqueAssetMetadata,239          existentialDeposit,240          isSufficient,241          unitsPerSecond,242          numAssetsWeightHint,243        });244245        console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);246247        await helper.fastDemocracy.executeProposal('register UNQ foreign asset', encodedProposal);248      }249250      // >>> Acquire Unique AssetId Info on Moonbeam >>>251      console.log('Acquire Unique AssetId Info on Moonbeam.......');252253      assetId = (await helper.assetManager.assetTypeId(uniqueAssetLocation)).toString();254255      console.log('UNQ asset ID is %s', assetId);256      console.log('Acquire Unique AssetId Info on Moonbeam.......DONE');257258      // >>> Sponsoring random Account >>>259      console.log('Sponsoring random Account.......');260      await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonbeam.address, 11_000_000_000_000_000_000n);261      console.log('Sponsoring random Account.......DONE');262      // <<< Sponsoring random Account <<<263    });264265    await usingPlaygrounds(async (helper) => {266      await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET);267    });268  });269270  itSub('Should connect and send UNQ to Moonbeam', async () => {271    await testHelper.sendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);272  });273274  itSub('Should connect to Moonbeam and send UNQ back', async () => {275    await testHelper.sendUnqBack('moonbeam', alice, randomAccountUnique);276  });277278  itSub('Moonbeam can send only up to its balance', async () => {279    await testHelper.sendOnlyOwnedBalance('moonbeam', alice);280  });281282  itSub('Should not accept reserve transfer of UNQ from Moonbeam', async () => {283    await testHelper.rejectReserveTransferUNQfrom('moonbeam', alice);284  });285});286287describeXCM('[XCMLL] Integration test: Exchanging tokens with Astar', () => {288  let alice: IKeyringPair;289  let randomAccount: IKeyringPair;290291  const UNQ_ASSET_ID_ON_ASTAR = 18_446_744_073_709_551_631n; // The value is taken from the live Astar292  const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; // The value is taken from the live Astar293294  // Unique -> Astar295  const astarInitialBalance = 1n * (10n ** ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Astar.296  const unitsPerSecond = 9_451_000_000_000_000_000n; // The value is taken from the live Astar297298  before(async () => {299    await usingPlaygrounds(async (helper, privateKey) => {300      alice = await privateKey('//Alice');301      randomAccount = helper.arrange.createEmptyAccount();302      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);303      console.log('randomAccount', randomAccount.address);304305      // Set the default version to wrap the first message to other chains.306      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);307    });308309    await usingAstarPlaygrounds(astarUrl, async (helper) => {310      if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) {311        console.log('1. Create foreign asset and metadata');312        await helper.assets.create(313          alice,314          UNQ_ASSET_ID_ON_ASTAR,315          alice.address,316          UNQ_MINIMAL_BALANCE_ON_ASTAR,317        );318319        await helper.assets.setMetadata(320          alice,321          UNQ_ASSET_ID_ON_ASTAR,322          'Unique Network',323          'UNQ',324          Number(UNQ_DECIMALS),325        );326327        console.log('2. Register asset location on Astar');328        const assetLocation = {329          V2: {330            parents: 1,331            interior: {332              X1: {333                Parachain: UNIQUE_CHAIN,334              },335            },336          },337        };338339        await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, UNQ_ASSET_ID_ON_ASTAR]);340341        console.log('3. Set UNQ payment for XCM execution on Astar');342        await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);343      } else {344        console.log('UNQ is already registered on Astar');345      }346      console.log('4. Transfer 1 ASTR to recipient to create the account (needed due to existential balance)');347      await helper.balance.transferToSubstrate(alice, randomAccount.address, astarInitialBalance);348    });349  });350351  itSub('Should connect and send UNQ to Astar', async () => {352    await testHelper.sendUnqTo('astar', randomAccount);353  });354355  itSub('Should connect to Astar and send UNQ back', async () => {356    await testHelper.sendUnqBack('astar', alice, randomAccount);357  });358359  itSub('Astar can send only up to its balance', async () => {360    await testHelper.sendOnlyOwnedBalance('astar', alice);361  });362363  itSub('Should not accept reserve transfer of UNQ from Astar', async () => {364    await testHelper.rejectReserveTransferUNQfrom('astar', alice);365  });366});367368describeXCM('[XCMLL] Integration test: The relay can do some root ops', () => {369  let sudoer: IKeyringPair;370371  before(async function () {372    await usingRelayPlaygrounds(relayUrl, async (_, privateKey) => {373      sudoer = await privateKey('//Alice');374    });375  });376377  // At the moment there is no reliable way378  // to establish the correspondence between the `ExecutedDownward` event379  // and the relay's sent message due to `SetTopic` instruction380  // containing an unpredictable topic silently added by the relay's messages on the router level.381  // This changes the message hash on arrival to our chain.382  //383  // See:384  // * The relay's router: https://github.com/paritytech/polkadot-sdk/blob/f60318f68687e601c47de5ad5ca88e2c3f8139a7/polkadot/runtime/westend/src/xcm_config.rs#L83385  // * The `WithUniqueTopic` helper: https://github.com/paritytech/polkadot-sdk/blob/945ebbbcf66646be13d5b1d1bc26c8b0d3296d9e/polkadot/xcm/xcm-builder/src/routing.rs#L36386  //387  // Because of this, we insert time gaps between tests so388  // different `ExecutedDownward` events won't interfere with each other.389  afterEach(async () => {390    await usingPlaygrounds(async (helper) => {391      await helper.wait.newBlocks(3);392    });393  });394395  itSub('The relay can set storage', async () => {396    await testHelper.relayIsPermittedToSetStorage(sudoer, 'plain');397  });398399  itSub('The relay can batch set storage', async () => {400    await testHelper.relayIsPermittedToSetStorage(sudoer, 'batch');401  });402403  itSub('The relay can batchAll set storage', async () => {404    await testHelper.relayIsPermittedToSetStorage(sudoer, 'batchAll');405  });406407  itSub('The relay can forceBatch set storage', async () => {408    await testHelper.relayIsPermittedToSetStorage(sudoer, 'forceBatch');409  });410411  itSub('[negative] The relay cannot set balance', async () => {412    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'plain');413  });414415  itSub('[negative] The relay cannot set balance via batch', async () => {416    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batch');417  });418419  itSub('[negative] The relay cannot set balance via batchAll', async () => {420    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batchAll');421  });422423  itSub('[negative] The relay cannot set balance via forceBatch', async () => {424    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'forceBatch');425  });426427  itSub('[negative] The relay cannot set balance via dispatchAs', async () => {428    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'dispatchAs');429  });430});
modifiedtests/src/xcm/xcm.types.tsdiffbeforeafterboth
--- 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<T extends keyof typeof NETWORKS>(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);
+    });
+  }
+}
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),
         );