difftreelog
Merge pull request #1003 from UniqueNetwork/feature/llxcm-qtz
in: master
added `XcmTestHelper` + allow XCM Transact for Gov/Identity/System calls
14 files changed
.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
.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:
runtime/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")]
runtime/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>,
+);
runtime/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>,
runtime/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>,
tests/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",
tests/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
+}
tests/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;
tests/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');
+ });
+});
tests/src/xcm/lowLevelXcmUnique.test.tsdiffbeforeafterboth161617import {IKeyringPair} from '@polkadot/types/types';17import {IKeyringPair} from '@polkadot/types/types';18import config from '../config';18import config from '../config';19import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds} from '../util';19import {itSub, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds} from '../util';20import {Event} from '../util/playgrounds/unique.dev';21import {nToBigInt} from '@polkadot/util';20import {nToBigInt} from '@polkadot/util';22import {hexToString} from '@polkadot/util';21import {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';22import {ASTAR_DECIMALS, SAFE_XCM_VERSION, SENDER_BUDGET, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper, acalaUrl, astarUrl, moonbeamUrl, polkadexUrl, relayUrl, uniqueAssetId} from './xcm.types';242324const testHelper = new XcmTestHelper('unique');252526const 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;312632let balanceUniqueTokenInit: bigint;33let balanceUniqueTokenMiddle: bigint;34let balanceUniqueTokenFinal: bigint;35let unqFees: bigint;3627372838async 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', () => {29describeXCM('[XCMLL] Integration test: Exchanging tokens with Acala', () => {37471375 await usingPlaygrounds(async (helper) => {72 await usingPlaygrounds(async (helper) => {376 await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);73 await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);377 balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);378 });74 });379 });75 });38076381 itSub('Should connect and send UNQ to Acala', async () => {77 itSub('Should connect and send UNQ to Acala', async () => {382 await genericSendUnqTo('acala', randomAccount);78 await testHelper.sendUnqTo('acala', randomAccount);383 });79 });38480385 itSub('Should connect to Acala and send UNQ back', async () => {81 itSub('Should connect to Acala and send UNQ back', async () => {386 await genericSendUnqBack('acala', alice, randomAccount);82 await testHelper.sendUnqBack('acala', alice, randomAccount);387 });83 });38884389 itSub('Acala can send only up to its balance', async () => {85 itSub('Acala can send only up to its balance', async () => {390 await genericSendOnlyOwnedBalance('acala', alice);86 await testHelper.sendOnlyOwnedBalance('acala', alice);391 });87 });39288393 itSub('Should not accept reserve transfer of UNQ from Acala', async () => {89 itSub('Should not accept reserve transfer of UNQ from Acala', async () => {394 await genericReserveTransferUNQfrom('acala', alice);90 await testHelper.rejectReserveTransferUNQfrom('acala', alice);395 });91 });396});92});39793429125430 await usingPlaygrounds(async (helper) => {126 await usingPlaygrounds(async (helper) => {431 await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);127 await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);432 balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);433 });128 });434 });129 });435130436 itSub('Should connect and send UNQ to Polkadex', async () => {131 itSub('Should connect and send UNQ to Polkadex', async () => {437 await genericSendUnqTo('polkadex', randomAccount);132 await testHelper.sendUnqTo('polkadex', randomAccount);438 });133 });439134440135441 itSub('Should connect to Polkadex and send UNQ back', async () => {136 itSub('Should connect to Polkadex and send UNQ back', async () => {442 await genericSendUnqBack('polkadex', alice, randomAccount);137 await testHelper.sendUnqBack('polkadex', alice, randomAccount);443 });138 });444139445 itSub('Polkadex can send only up to its balance', async () => {140 itSub('Polkadex can send only up to its balance', async () => {446 await genericSendOnlyOwnedBalance('polkadex', alice);141 await testHelper.sendOnlyOwnedBalance('polkadex', alice);447 });142 });448143449 itSub('Should not accept reserve transfer of UNQ from Polkadex', async () => {144 itSub('Should not accept reserve transfer of UNQ from Polkadex', async () => {450 await genericReserveTransferUNQfrom('polkadex', alice);145 await testHelper.rejectReserveTransferUNQfrom('polkadex', alice);451 });146 });452});147});453148466 });161 });467162468 itSub('Unique rejects ACA tokens from Acala', async () => {163 itSub('Unique rejects ACA tokens from Acala', async () => {469 await genericRejectNativeTokensFrom('acala', alice);164 await testHelper.rejectNativeTokensFrom('acala', alice);470 });165 });471166472 itSub('Unique rejects GLMR tokens from Moonbeam', async () => {167 itSub('Unique rejects GLMR tokens from Moonbeam', async () => {473 await genericRejectNativeTokensFrom('moonbeam', alice);168 await testHelper.rejectNativeTokensFrom('moonbeam', alice);474 });169 });475170476 itSub('Unique rejects ASTR tokens from Astar', async () => {171 itSub('Unique rejects ASTR tokens from Astar', async () => {477 await genericRejectNativeTokensFrom('astar', alice);172 await testHelper.rejectNativeTokensFrom('astar', alice);478 });173 });479174480 itSub('Unique rejects PDX tokens from Polkadex', async () => {175 itSub('Unique rejects PDX tokens from Polkadex', async () => {481 await genericRejectNativeTokensFrom('polkadex', alice);176 await testHelper.rejectNativeTokensFrom('polkadex', alice);482 });177 });483});178});484179569264570 await usingPlaygrounds(async (helper) => {265 await usingPlaygrounds(async (helper) => {571 await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET);266 await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET);572 balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccountUnique.address);573 });267 });574 });268 });575269576 itSub('Should connect and send UNQ to Moonbeam', async () => {270 itSub('Should connect and send UNQ to Moonbeam', async () => {577 await genericSendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);271 await testHelper.sendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);578 });272 });579273580 itSub('Should connect to Moonbeam and send UNQ back', async () => {274 itSub('Should connect to Moonbeam and send UNQ back', async () => {581 await genericSendUnqBack('moonbeam', alice, randomAccountUnique);275 await testHelper.sendUnqBack('moonbeam', alice, randomAccountUnique);582 });276 });583277584 itSub('Moonbeam can send only up to its balance', async () => {278 itSub('Moonbeam can send only up to its balance', async () => {585 await genericSendOnlyOwnedBalance('moonbeam', alice);279 await testHelper.sendOnlyOwnedBalance('moonbeam', alice);586 });280 });587281588 itSub('Should not accept reserve transfer of UNQ from Moonbeam', async () => {282 itSub('Should not accept reserve transfer of UNQ from Moonbeam', async () => {589 await genericReserveTransferUNQfrom('moonbeam', alice);283 await testHelper.rejectReserveTransferUNQfrom('moonbeam', alice);590 });284 });591});285});592286593describeXCM('[XCMLL] Integration test: Exchanging tokens with Astar', () => {287describeXCM('[XCMLL] Integration test: Exchanging tokens with Astar', () => {594 let alice: IKeyringPair;288 let alice: IKeyringPair;595 let randomAccount: IKeyringPair;289 let randomAccount: IKeyringPair;596290597 const UNQ_ASSET_ID_ON_ASTAR = 1;291 const UNQ_ASSET_ID_ON_ASTAR = 18_446_744_073_709_551_631n; // The value is taken from the live Astar598 const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n;292 const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; // The value is taken from the live Astar599293600 // Unique -> Astar294 // Unique -> Astar601 const astarInitialBalance = 1n * (10n ** ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Astar.295 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?296 const unitsPerSecond = 9_451_000_000_000_000_000n; // The value is taken from the live Astar603297604 before(async () => {298 before(async () => {605 await usingPlaygrounds(async (helper, privateKey) => {299 await usingPlaygrounds(async (helper, privateKey) => {615 await usingAstarPlaygrounds(astarUrl, async (helper) => {309 await usingAstarPlaygrounds(astarUrl, async (helper) => {616 if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) {310 if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) {617 console.log('1. Create foreign asset and metadata');311 console.log('1. Create foreign asset and metadata');618 // TODO update metadata with values from production619 await helper.assets.create(312 await helper.assets.create(620 alice,313 alice,621 UNQ_ASSET_ID_ON_ASTAR,314 UNQ_ASSET_ID_ON_ASTAR,626 await helper.assets.setMetadata(319 await helper.assets.setMetadata(627 alice,320 alice,628 UNQ_ASSET_ID_ON_ASTAR,321 UNQ_ASSET_ID_ON_ASTAR,629 'Cross chain UNQ',322 'Unique Network',630 'xcUNQ',323 'UNQ',631 Number(UNQ_DECIMALS),324 Number(UNQ_DECIMALS),632 );325 );633326656 });349 });657350658 itSub('Should connect and send UNQ to Astar', async () => {351 itSub('Should connect and send UNQ to Astar', async () => {659 await genericSendUnqTo('astar', randomAccount);352 await testHelper.sendUnqTo('astar', randomAccount);660 });353 });661354662 itSub('Should connect to Astar and send UNQ back', async () => {355 itSub('Should connect to Astar and send UNQ back', async () => {663 await genericSendUnqBack('astar', alice, randomAccount);356 await testHelper.sendUnqBack('astar', alice, randomAccount);664 });357 });665358666 itSub('Astar can send only up to its balance', async () => {359 itSub('Astar can send only up to its balance', async () => {667 await genericSendOnlyOwnedBalance('astar', alice);360 await testHelper.sendOnlyOwnedBalance('astar', alice);668 });361 });669362670 itSub('Should not accept reserve transfer of UNQ from Astar', async () => {363 itSub('Should not accept reserve transfer of UNQ from Astar', async () => {671 await genericReserveTransferUNQfrom('astar', alice);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');672 });429 });673});430});674431tests/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);
+ });
+ }
+}
tests/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);
});
tests/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),
);