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.tsdiffbeforeafterboth--- a/tests/src/xcm/lowLevelXcmUnique.test.ts
+++ b/tests/src/xcm/lowLevelXcmUnique.test.ts
@@ -16,317 +16,14 @@
import {IKeyringPair} from '@polkadot/types/types';
import config from '../config';
-import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds} from '../util';
-import {Event} from '../util/playgrounds/unique.dev';
+import {itSub, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds} from '../util';
import {nToBigInt} from '@polkadot/util';
import {hexToString} from '@polkadot/util';
-import {ASTAR_DECIMALS, NETWORKS, SAFE_XCM_VERSION, UNIQUE_CHAIN, UNQ_DECIMALS, acalaUrl, astarUrl, expectFailedToTransact, expectUntrustedReserveLocationFail, getDevPlayground, mapToChainId, mapToChainUrl, maxWaitBlocks, moonbeamUrl, polkadexUrl, uniqueAssetId, uniqueVersionedMultilocation} from './xcm.types';
-
-
-const TRANSFER_AMOUNT = 2000000_000_000_000_000_000_000n;
-const SENDER_BUDGET = 2n * TRANSFER_AMOUNT;
-const SENDBACK_AMOUNT = TRANSFER_AMOUNT / 2n;
-const STAYED_ON_TARGET_CHAIN = TRANSFER_AMOUNT - SENDBACK_AMOUNT;
-const TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT = 100_000_000_000n;
-
-let balanceUniqueTokenInit: bigint;
-let balanceUniqueTokenMiddle: bigint;
-let balanceUniqueTokenFinal: bigint;
-let unqFees: bigint;
-
-
-async function genericSendUnqTo(
- networkName: keyof typeof NETWORKS,
- randomAccount: IKeyringPair,
- randomAccountOnTargetChain = randomAccount,
-) {
- const networkUrl = mapToChainUrl(networkName);
- const targetPlayground = getDevPlayground(networkName);
- await usingPlaygrounds(async (helper) => {
- balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
- const destination = {
- V2: {
- parents: 1,
- interior: {
- X1: {
- Parachain: mapToChainId(networkName),
- },
- },
- },
- };
-
- const beneficiary = {
- V2: {
- parents: 0,
- interior: {
- X1: (
- networkName == 'moonbeam' ?
- {
- AccountKey20: {
- network: 'Any',
- key: randomAccountOnTargetChain.address,
- },
- }
- :
- {
- AccountId32: {
- network: 'Any',
- id: randomAccountOnTargetChain.addressRaw,
- },
- }
- ),
- },
- },
- };
-
- const assets = {
- V2: [
- {
- id: {
- Concrete: {
- parents: 0,
- interior: 'Here',
- },
- },
- fun: {
- Fungible: TRANSFER_AMOUNT,
- },
- },
- ],
- };
- const feeAssetItem = 0;
-
- await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');
- const messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- balanceUniqueTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);
-
- unqFees = balanceUniqueTokenInit - balanceUniqueTokenMiddle - TRANSFER_AMOUNT;
- console.log('[Unique -> %s] transaction fees on Unique: %s UNQ', networkName, helper.util.bigIntToDecimals(unqFees));
- expect(unqFees > 0n, 'Negative fees UNQ, looks like nothing was transferred').to.be.true;
-
- await targetPlayground(networkUrl, async (helper) => {
- /*
- Since only the parachain part of the Polkadex
- infrastructure is launched (without their
- solochain validators), processing incoming
- assets will lead to an error.
- This error indicates that the Polkadex chain
- received a message from the Unique network,
- since the hash is being checked to ensure
- it matches what was sent.
- */
- if(networkName == 'polkadex') {
- await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash);
- } else {
- await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == messageSent.messageHash);
- }
- });
-
- });
-}
-
-async function genericSendUnqBack(
- networkName: keyof typeof NETWORKS,
- sudoer: IKeyringPair,
- randomAccountOnUnq: IKeyringPair,
-) {
- const networkUrl = mapToChainUrl(networkName);
-
- const targetPlayground = getDevPlayground(networkName);
- await usingPlaygrounds(async (helper) => {
-
- const xcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
- randomAccountOnUnq.addressRaw,
- uniqueAssetId,
- SENDBACK_AMOUNT,
- );
-
- let xcmProgramSent: any;
-
-
- await targetPlayground(networkUrl, async (helper) => {
- if('getSudo' in helper) {
- await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, xcmProgram);
- xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- } else if('fastDemocracy' in helper) {
- const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, xcmProgram]);
- // Needed to bypass the call filter.
- const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
- await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
- xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- });
-
- await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == xcmProgramSent.messageHash);
-
- balanceUniqueTokenFinal = await helper.balance.getSubstrate(randomAccountOnUnq.address);
-
- expect(balanceUniqueTokenFinal).to.be.equal(balanceUniqueTokenInit - unqFees - STAYED_ON_TARGET_CHAIN);
-
- });
-}
-
-async function genericSendOnlyOwnedBalance(
- networkName: keyof typeof NETWORKS,
- sudoer: IKeyringPair,
-) {
- const networkUrl = mapToChainUrl(networkName);
- const targetPlayground = getDevPlayground(networkName);
-
- const targetChainBalance = 10000n * (10n ** UNQ_DECIMALS);
-
- await usingPlaygrounds(async (helper) => {
- const targetChainSovereignAccount = helper.address.paraSiblingSovereignAccount(mapToChainId(networkName));
- await helper.getSudo().balance.setBalanceSubstrate(sudoer, targetChainSovereignAccount, targetChainBalance);
- const moreThanTargetChainHas = 2n * targetChainBalance;
-
- const targetAccount = helper.arrange.createEmptyAccount();
-
- const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
- targetAccount.addressRaw,
- {
- Concrete: {
- parents: 0,
- interior: 'Here',
- },
- },
- moreThanTargetChainHas,
- );
-
- let maliciousXcmProgramSent: any;
-
-
- await targetPlayground(networkUrl, async (helper) => {
- if('getSudo' in helper) {
- await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgram);
- maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- } else if('fastDemocracy' in helper) {
- const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgram]);
- // Needed to bypass the call filter.
- const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
- await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
- maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- });
-
- await expectFailedToTransact(helper, maliciousXcmProgramSent);
-
- const targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
- expect(targetAccountBalance).to.be.equal(0n);
- });
-}
-
-async function genericReserveTransferUNQfrom(netwokrName: keyof typeof NETWORKS, sudoer: IKeyringPair) {
- const networkUrl = mapToChainUrl(netwokrName);
- const targetPlayground = getDevPlayground(netwokrName);
-
- await usingPlaygrounds(async (helper) => {
- const testAmount = 10_000n * (10n ** UNQ_DECIMALS);
- const targetAccount = helper.arrange.createEmptyAccount();
-
- const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
- targetAccount.addressRaw,
- uniqueAssetId,
- testAmount,
- );
-
- const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(
- targetAccount.addressRaw,
- {
- Concrete: {
- parents: 0,
- interior: 'Here',
- },
- },
- testAmount,
- );
-
- let maliciousXcmProgramFullIdSent: any;
- let maliciousXcmProgramHereIdSent: any;
- const maxWaitBlocks = 3;
-
- // Try to trick Unique using full UNQ identification
- await targetPlayground(networkUrl, async (helper) => {
- if('getSudo' in helper) {
- await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramFullId);
- maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- // Moonbeam case
- else if('fastDemocracy' in helper) {
- const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]);
- // Needed to bypass the call filter.
- const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
- await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using path asset identification`,batchCall);
-
- maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- });
-
-
- await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramFullIdSent);
-
- let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
- expect(accountBalance).to.be.equal(0n);
-
- // Try to trick Unique using shortened UNQ identification
- await targetPlayground(networkUrl, async (helper) => {
- if('getSudo' in helper) {
- await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramHereId);
- maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- else if('fastDemocracy' in helper) {
- const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramHereId]);
- // Needed to bypass the call filter.
- const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
- await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using "here" asset identification`, batchCall);
-
- maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- });
-
- await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramHereIdSent);
+import {ASTAR_DECIMALS, SAFE_XCM_VERSION, SENDER_BUDGET, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper, acalaUrl, astarUrl, moonbeamUrl, polkadexUrl, relayUrl, uniqueAssetId} from './xcm.types';
- accountBalance = await helper.balance.getSubstrate(targetAccount.address);
- expect(accountBalance).to.be.equal(0n);
- });
-}
+const testHelper = new XcmTestHelper('unique');
-async function genericRejectNativeTokensFrom(networkName: keyof typeof NETWORKS, sudoerOnTargetChain: IKeyringPair) {
- const networkUrl = mapToChainUrl(networkName);
- const targetPlayground = getDevPlayground(networkName);
- let messageSent: any;
- await usingPlaygrounds(async (helper) => {
- const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
- helper.arrange.createEmptyAccount().addressRaw,
- {
- Concrete: {
- parents: 1,
- interior: {
- X1: {
- Parachain: mapToChainId(networkName),
- },
- },
- },
- },
- TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT,
- );
- await targetPlayground(networkUrl, async (helper) => {
- if('getSudo' in helper) {
- await helper.getSudo().xcm.send(sudoerOnTargetChain, uniqueVersionedMultilocation, maliciousXcmProgramFullId);
- messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- } else if('fastDemocracy' in helper) {
- const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]);
- // Needed to bypass the call filter.
- const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
- await helper.fastDemocracy.executeProposal(`${networkName} sending native tokens to the Unique via fast democracy`, batchCall);
-
- messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- });
- await expectFailedToTransact(helper, messageSent);
- });
-}
describeXCM('[XCMLL] Integration test: Exchanging tokens with Acala', () => {
@@ -374,24 +71,23 @@
await usingPlaygrounds(async (helper) => {
await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);
- balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
});
});
itSub('Should connect and send UNQ to Acala', async () => {
- await genericSendUnqTo('acala', randomAccount);
+ await testHelper.sendUnqTo('acala', randomAccount);
});
itSub('Should connect to Acala and send UNQ back', async () => {
- await genericSendUnqBack('acala', alice, randomAccount);
+ await testHelper.sendUnqBack('acala', alice, randomAccount);
});
itSub('Acala can send only up to its balance', async () => {
- await genericSendOnlyOwnedBalance('acala', alice);
+ await testHelper.sendOnlyOwnedBalance('acala', alice);
});
itSub('Should not accept reserve transfer of UNQ from Acala', async () => {
- await genericReserveTransferUNQfrom('acala', alice);
+ await testHelper.rejectReserveTransferUNQfrom('acala', alice);
});
});
@@ -429,25 +125,24 @@
await usingPlaygrounds(async (helper) => {
await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);
- balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
});
});
itSub('Should connect and send UNQ to Polkadex', async () => {
- await genericSendUnqTo('polkadex', randomAccount);
+ await testHelper.sendUnqTo('polkadex', randomAccount);
});
itSub('Should connect to Polkadex and send UNQ back', async () => {
- await genericSendUnqBack('polkadex', alice, randomAccount);
+ await testHelper.sendUnqBack('polkadex', alice, randomAccount);
});
itSub('Polkadex can send only up to its balance', async () => {
- await genericSendOnlyOwnedBalance('polkadex', alice);
+ await testHelper.sendOnlyOwnedBalance('polkadex', alice);
});
itSub('Should not accept reserve transfer of UNQ from Polkadex', async () => {
- await genericReserveTransferUNQfrom('polkadex', alice);
+ await testHelper.rejectReserveTransferUNQfrom('polkadex', alice);
});
});
@@ -466,19 +161,19 @@
});
itSub('Unique rejects ACA tokens from Acala', async () => {
- await genericRejectNativeTokensFrom('acala', alice);
+ await testHelper.rejectNativeTokensFrom('acala', alice);
});
itSub('Unique rejects GLMR tokens from Moonbeam', async () => {
- await genericRejectNativeTokensFrom('moonbeam', alice);
+ await testHelper.rejectNativeTokensFrom('moonbeam', alice);
});
itSub('Unique rejects ASTR tokens from Astar', async () => {
- await genericRejectNativeTokensFrom('astar', alice);
+ await testHelper.rejectNativeTokensFrom('astar', alice);
});
itSub('Unique rejects PDX tokens from Polkadex', async () => {
- await genericRejectNativeTokensFrom('polkadex', alice);
+ await testHelper.rejectNativeTokensFrom('polkadex', alice);
});
});
@@ -569,24 +264,23 @@
await usingPlaygrounds(async (helper) => {
await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET);
- balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccountUnique.address);
});
});
itSub('Should connect and send UNQ to Moonbeam', async () => {
- await genericSendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);
+ await testHelper.sendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);
});
itSub('Should connect to Moonbeam and send UNQ back', async () => {
- await genericSendUnqBack('moonbeam', alice, randomAccountUnique);
+ await testHelper.sendUnqBack('moonbeam', alice, randomAccountUnique);
});
itSub('Moonbeam can send only up to its balance', async () => {
- await genericSendOnlyOwnedBalance('moonbeam', alice);
+ await testHelper.sendOnlyOwnedBalance('moonbeam', alice);
});
itSub('Should not accept reserve transfer of UNQ from Moonbeam', async () => {
- await genericReserveTransferUNQfrom('moonbeam', alice);
+ await testHelper.rejectReserveTransferUNQfrom('moonbeam', alice);
});
});
@@ -594,12 +288,12 @@
let alice: IKeyringPair;
let randomAccount: IKeyringPair;
- const UNQ_ASSET_ID_ON_ASTAR = 1;
- const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n;
+ const UNQ_ASSET_ID_ON_ASTAR = 18_446_744_073_709_551_631n; // The value is taken from the live Astar
+ const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; // The value is taken from the live Astar
// Unique -> Astar
const astarInitialBalance = 1n * (10n ** ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Astar.
- const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?
+ const unitsPerSecond = 9_451_000_000_000_000_000n; // The value is taken from the live Astar
before(async () => {
await usingPlaygrounds(async (helper, privateKey) => {
@@ -615,7 +309,6 @@
await usingAstarPlaygrounds(astarUrl, async (helper) => {
if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) {
console.log('1. Create foreign asset and metadata');
- // TODO update metadata with values from production
await helper.assets.create(
alice,
UNQ_ASSET_ID_ON_ASTAR,
@@ -626,8 +319,8 @@
await helper.assets.setMetadata(
alice,
UNQ_ASSET_ID_ON_ASTAR,
- 'Cross chain UNQ',
- 'xcUNQ',
+ 'Unique Network',
+ 'UNQ',
Number(UNQ_DECIMALS),
);
@@ -656,18 +349,82 @@
});
itSub('Should connect and send UNQ to Astar', async () => {
- await genericSendUnqTo('astar', randomAccount);
+ await testHelper.sendUnqTo('astar', randomAccount);
});
itSub('Should connect to Astar and send UNQ back', async () => {
- await genericSendUnqBack('astar', alice, randomAccount);
+ await testHelper.sendUnqBack('astar', alice, randomAccount);
});
itSub('Astar can send only up to its balance', async () => {
- await genericSendOnlyOwnedBalance('astar', alice);
+ await testHelper.sendOnlyOwnedBalance('astar', alice);
});
itSub('Should not accept reserve transfer of UNQ from Astar', async () => {
- await genericReserveTransferUNQfrom('astar', alice);
+ await testHelper.rejectReserveTransferUNQfrom('astar', alice);
+ });
+});
+
+describeXCM('[XCMLL] Integration test: The relay can do some root ops', () => {
+ let sudoer: IKeyringPair;
+
+ before(async function () {
+ await usingRelayPlaygrounds(relayUrl, async (_, privateKey) => {
+ sudoer = await privateKey('//Alice');
+ });
+ });
+
+ // At the moment there is no reliable way
+ // to establish the correspondence between the `ExecutedDownward` event
+ // and the relay's sent message due to `SetTopic` instruction
+ // containing an unpredictable topic silently added by the relay's messages on the router level.
+ // This changes the message hash on arrival to our chain.
+ //
+ // See:
+ // * The relay's router: https://github.com/paritytech/polkadot-sdk/blob/f60318f68687e601c47de5ad5ca88e2c3f8139a7/polkadot/runtime/westend/src/xcm_config.rs#L83
+ // * The `WithUniqueTopic` helper: https://github.com/paritytech/polkadot-sdk/blob/945ebbbcf66646be13d5b1d1bc26c8b0d3296d9e/polkadot/xcm/xcm-builder/src/routing.rs#L36
+ //
+ // Because of this, we insert time gaps between tests so
+ // different `ExecutedDownward` events won't interfere with each other.
+ afterEach(async () => {
+ await usingPlaygrounds(async (helper) => {
+ await helper.wait.newBlocks(3);
+ });
+ });
+
+ itSub('The relay can set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'plain');
+ });
+
+ itSub('The relay can batch set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'batch');
+ });
+
+ itSub('The relay can batchAll set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'batchAll');
+ });
+
+ itSub('The relay can forceBatch set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'forceBatch');
+ });
+
+ itSub('[negative] The relay cannot set balance', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'plain');
+ });
+
+ itSub('[negative] The relay cannot set balance via batch', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batch');
+ });
+
+ itSub('[negative] The relay cannot set balance via batchAll', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batchAll');
+ });
+
+ itSub('[negative] The relay cannot set balance via forceBatch', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'forceBatch');
+ });
+
+ itSub('[negative] The relay cannot set balance via dispatchAs', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'dispatchAs');
});
});
tests/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.tsdiffbeforeafterboth1// 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, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util';20import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';2122const QUARTZ_CHAIN = +(process.env.RELAY_QUARTZ_ID || 2095);23const STATEMINE_CHAIN = +(process.env.RELAY_STATEMINE_ID || 1000);24const KARURA_CHAIN = +(process.env.RELAY_KARURA_ID || 2000);25const MOONRIVER_CHAIN = +(process.env.RELAY_MOONRIVER_ID || 2023);26const SHIDEN_CHAIN = +(process.env.RELAY_SHIDEN_ID || 2007);2728const STATEMINE_PALLET_INSTANCE = 50;2930const relayUrl = config.relayUrl;31const statemineUrl = config.statemineUrl;32const karuraUrl = config.karuraUrl;33const moonriverUrl = config.moonriverUrl;34const shidenUrl = config.shidenUrl;3536const RELAY_DECIMALS = 12;37const STATEMINE_DECIMALS = 12;38const KARURA_DECIMALS = 12;39const SHIDEN_DECIMALS = 18n;40const QTZ_DECIMALS = 18n;4142const TRANSFER_AMOUNT = 2000000000000000000000000n;4344const FUNDING_AMOUNT = 3_500_000_0000_000_000n;4546const TRANSFER_AMOUNT_RELAY = 50_000_000_000_000_000n;4748const USDT_ASSET_ID = 100;49const USDT_ASSET_METADATA_DECIMALS = 18;50const USDT_ASSET_METADATA_NAME = 'USDT';51const USDT_ASSET_METADATA_DESCRIPTION = 'USDT';52const USDT_ASSET_METADATA_MINIMAL_BALANCE = 1n;53const USDT_ASSET_AMOUNT = 10_000_000_000_000_000_000_000_000n;5455const SAFE_XCM_VERSION = 2;5657describeXCM('[XCM] Integration test: Exchanging USDT with Statemine', () => {58 let alice: IKeyringPair;59 let bob: IKeyringPair;6061 let balanceStmnBefore: bigint;62 let balanceStmnAfter: bigint;6364 let balanceQuartzBefore: bigint;65 let balanceQuartzAfter: bigint;66 let balanceQuartzFinal: bigint;6768 let balanceBobBefore: bigint;69 let balanceBobAfter: bigint;70 let balanceBobFinal: bigint;7172 let balanceBobRelayTokenBefore: bigint;73 let balanceBobRelayTokenAfter: bigint;747576 before(async () => {77 await usingPlaygrounds(async (helper, privateKey) => {78 alice = await privateKey('//Alice');79 bob = await privateKey('//Bob'); // sovereign account on Statemine(t) funds donor8081 // Set the default version to wrap the first message to other chains.82 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);83 });8485 await usingRelayPlaygrounds(relayUrl, async (helper) => {86 // Fund accounts on Statemine(t)87 await helper.xcm.teleportNativeAsset(alice, STATEMINE_CHAIN, alice.addressRaw, FUNDING_AMOUNT);88 await helper.xcm.teleportNativeAsset(alice, STATEMINE_CHAIN, bob.addressRaw, FUNDING_AMOUNT);89 });9091 await usingStateminePlaygrounds(statemineUrl, async (helper) => {92 const sovereignFundingAmount = 3_500_000_000n;9394 await helper.assets.create(95 alice,96 USDT_ASSET_ID,97 alice.address,98 USDT_ASSET_METADATA_MINIMAL_BALANCE,99 );100 await helper.assets.setMetadata(101 alice,102 USDT_ASSET_ID,103 USDT_ASSET_METADATA_NAME,104 USDT_ASSET_METADATA_DESCRIPTION,105 USDT_ASSET_METADATA_DECIMALS,106 );107 await helper.assets.mint(108 alice,109 USDT_ASSET_ID,110 alice.address,111 USDT_ASSET_AMOUNT,112 );113114 // funding parachain sovereing account on Statemine(t).115 // The sovereign account should be created before any action116 // (the assets pallet on Statemine(t) check if the sovereign account exists)117 const parachainSovereingAccount = helper.address.paraSiblingSovereignAccount(QUARTZ_CHAIN);118 await helper.balance.transferToSubstrate(bob, parachainSovereingAccount, sovereignFundingAmount);119 });120121122 await usingPlaygrounds(async (helper) => {123 const location = {124 V2: {125 parents: 1,126 interior: {X3: [127 {128 Parachain: STATEMINE_CHAIN,129 },130 {131 PalletInstance: STATEMINE_PALLET_INSTANCE,132 },133 {134 GeneralIndex: USDT_ASSET_ID,135 },136 ]},137 },138 };139140 const metadata =141 {142 name: USDT_ASSET_ID,143 symbol: USDT_ASSET_METADATA_NAME,144 decimals: USDT_ASSET_METADATA_DECIMALS,145 minimalBalance: USDT_ASSET_METADATA_MINIMAL_BALANCE,146 };147 await helper.getSudo().foreignAssets.register(alice, alice.address, location, metadata);148 balanceQuartzBefore = await helper.balance.getSubstrate(alice.address);149 });150151152 // Providing the relay currency to the quartz sender account153 // (fee for USDT XCM are paid in relay tokens)154 await usingRelayPlaygrounds(relayUrl, async (helper) => {155 const destination = {156 V2: {157 parents: 0,158 interior: {X1: {159 Parachain: QUARTZ_CHAIN,160 },161 },162 }};163164 const beneficiary = {165 V2: {166 parents: 0,167 interior: {X1: {168 AccountId32: {169 network: 'Any',170 id: alice.addressRaw,171 },172 }},173 },174 };175176 const assets = {177 V2: [178 {179 id: {180 Concrete: {181 parents: 0,182 interior: 'Here',183 },184 },185 fun: {186 Fungible: TRANSFER_AMOUNT_RELAY,187 },188 },189 ],190 };191192 const feeAssetItem = 0;193194 await helper.xcm.limitedReserveTransferAssets(alice, destination, beneficiary, assets, feeAssetItem, 'Unlimited');195 });196197 });198199 itSub('Should connect and send USDT from Statemine to Quartz', async ({helper}) => {200 await usingStateminePlaygrounds(statemineUrl, async (helper) => {201 const dest = {202 V2: {203 parents: 1,204 interior: {X1: {205 Parachain: QUARTZ_CHAIN,206 },207 },208 }};209210 const beneficiary = {211 V2: {212 parents: 0,213 interior: {X1: {214 AccountId32: {215 network: 'Any',216 id: alice.addressRaw,217 },218 }},219 },220 };221222 const assets = {223 V2: [224 {225 id: {226 Concrete: {227 parents: 0,228 interior: {229 X2: [230 {231 PalletInstance: STATEMINE_PALLET_INSTANCE,232 },233 {234 GeneralIndex: USDT_ASSET_ID,235 },236 ]},237 },238 },239 fun: {240 Fungible: TRANSFER_AMOUNT,241 },242 },243 ],244 };245246 const feeAssetItem = 0;247248 balanceStmnBefore = await helper.balance.getSubstrate(alice.address);249 await helper.xcm.limitedReserveTransferAssets(alice, dest, beneficiary, assets, feeAssetItem, 'Unlimited');250251 balanceStmnAfter = await helper.balance.getSubstrate(alice.address);252253 // common good parachain take commission in it native token254 console.log(255 '[Statemine -> Quartz] transaction fees on Statemine: %s WND',256 helper.util.bigIntToDecimals(balanceStmnBefore - balanceStmnAfter, STATEMINE_DECIMALS),257 );258 expect(balanceStmnBefore > balanceStmnAfter).to.be.true;259260 });261262263 // ensure that asset has been delivered264 await helper.wait.newBlocks(3);265266 // expext collection id will be with id 1267 const free = await helper.ft.getBalance(1, {Substrate: alice.address});268269 balanceQuartzAfter = await helper.balance.getSubstrate(alice.address);270271 console.log(272 '[Statemine -> Quartz] transaction fees on Quartz: %s USDT',273 helper.util.bigIntToDecimals(TRANSFER_AMOUNT - free, USDT_ASSET_METADATA_DECIMALS),274 );275 console.log(276 '[Statemine -> Quartz] transaction fees on Quartz: %s QTZ',277 helper.util.bigIntToDecimals(balanceQuartzAfter - balanceQuartzBefore),278 );279 // commission has not paid in USDT token280 expect(free).to.be.equal(TRANSFER_AMOUNT);281 // ... and parachain native token282 expect(balanceQuartzAfter == balanceQuartzBefore).to.be.true;283 });284285 itSub('Should connect and send USDT from Quartz to Statemine back', async ({helper}) => {286 const destination = {287 V2: {288 parents: 1,289 interior: {X2: [290 {291 Parachain: STATEMINE_CHAIN,292 },293 {294 AccountId32: {295 network: 'Any',296 id: alice.addressRaw,297 },298 },299 ]},300 },301 };302303 const relayFee = 400_000_000_000_000n;304 const currencies: [any, bigint][] = [305 [306 {307 ForeignAssetId: 0,308 },309 TRANSFER_AMOUNT,310 ],311 [312 {313 NativeAssetId: 'Parent',314 },315 relayFee,316 ],317 ];318319 const feeItem = 1;320321 await helper.xTokens.transferMulticurrencies(alice, currencies, feeItem, destination, 'Unlimited');322323 // the commission has been paid in parachain native token324 balanceQuartzFinal = await helper.balance.getSubstrate(alice.address);325 console.log('[Quartz -> Statemine] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(balanceQuartzAfter - balanceQuartzFinal));326 expect(balanceQuartzAfter > balanceQuartzFinal).to.be.true;327328 await usingStateminePlaygrounds(statemineUrl, async (helper) => {329 await helper.wait.newBlocks(3);330331 // The USDT token never paid fees. Its amount not changed from begin value.332 // Also check that xcm transfer has been succeeded333 expect((await helper.assets.account(USDT_ASSET_ID, alice.address))! == USDT_ASSET_AMOUNT).to.be.true;334 });335 });336337 itSub('Should connect and send Relay token to Quartz', async ({helper}) => {338 balanceBobBefore = await helper.balance.getSubstrate(bob.address);339 balanceBobRelayTokenBefore = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});340341 await usingRelayPlaygrounds(relayUrl, async (helper) => {342 const destination = {343 V2: {344 parents: 0,345 interior: {X1: {346 Parachain: QUARTZ_CHAIN,347 },348 },349 }};350351 const beneficiary = {352 V2: {353 parents: 0,354 interior: {X1: {355 AccountId32: {356 network: 'Any',357 id: bob.addressRaw,358 },359 }},360 },361 };362363 const assets = {364 V2: [365 {366 id: {367 Concrete: {368 parents: 0,369 interior: 'Here',370 },371 },372 fun: {373 Fungible: TRANSFER_AMOUNT_RELAY,374 },375 },376 ],377 };378379 const feeAssetItem = 0;380381 await helper.xcm.limitedReserveTransferAssets(bob, destination, beneficiary, assets, feeAssetItem, 'Unlimited');382 });383384 await helper.wait.newBlocks(3);385386 balanceBobAfter = await helper.balance.getSubstrate(bob.address);387 balanceBobRelayTokenAfter = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});388389 const wndFeeOnQuartz = balanceBobRelayTokenAfter - TRANSFER_AMOUNT_RELAY - balanceBobRelayTokenBefore;390 const wndDiffOnQuartz = balanceBobRelayTokenAfter - balanceBobRelayTokenBefore;391 console.log(392 '[Relay (Westend) -> Quartz] transaction fees: %s QTZ',393 helper.util.bigIntToDecimals(balanceBobAfter - balanceBobBefore),394 );395 console.log(396 '[Relay (Westend) -> Quartz] transaction fees: %s WND',397 helper.util.bigIntToDecimals(wndFeeOnQuartz, STATEMINE_DECIMALS),398 );399 console.log('[Relay (Westend) -> Quartz] actually delivered: %s WND', wndDiffOnQuartz);400 expect(wndFeeOnQuartz == 0n, 'No incoming WND fees should be taken').to.be.true;401 expect(balanceBobBefore == balanceBobAfter, 'No incoming QTZ fees should be taken').to.be.true;402 });403404 itSub('Should connect and send Relay token back', async ({helper}) => {405 let relayTokenBalanceBefore: bigint;406 let relayTokenBalanceAfter: bigint;407 await usingRelayPlaygrounds(relayUrl, async (helper) => {408 relayTokenBalanceBefore = await helper.balance.getSubstrate(bob.address);409 });410411 const destination = {412 V2: {413 parents: 1,414 interior: {415 X1:{416 AccountId32: {417 network: 'Any',418 id: bob.addressRaw,419 },420 },421 },422 },423 };424425 const currencies: any = [426 [427 {428 NativeAssetId: 'Parent',429 },430 TRANSFER_AMOUNT_RELAY,431 ],432 ];433434 const feeItem = 0;435436 await helper.xTokens.transferMulticurrencies(bob, currencies, feeItem, destination, 'Unlimited');437438 balanceBobFinal = await helper.balance.getSubstrate(bob.address);439 console.log('[Quartz -> Relay (Westend)] transaction fees: %s QTZ', helper.util.bigIntToDecimals(balanceBobAfter - balanceBobFinal));440441 await usingRelayPlaygrounds(relayUrl, async (helper) => {442 await helper.wait.newBlocks(10);443 relayTokenBalanceAfter = await helper.balance.getSubstrate(bob.address);444445 const diff = relayTokenBalanceAfter - relayTokenBalanceBefore;446 console.log('[Quartz -> Relay (Westend)] actually delivered: %s WND', helper.util.bigIntToDecimals(diff, RELAY_DECIMALS));447 expect(diff > 0, 'Relay tokens was not delivered back').to.be.true;448 });449 });450});451452describeXCM('[XCM] Integration test: Exchanging tokens with Karura', () => {453 let alice: IKeyringPair;454 let randomAccount: IKeyringPair;455456 let balanceQuartzTokenInit: bigint;457 let balanceQuartzTokenMiddle: bigint;458 let balanceQuartzTokenFinal: bigint;459 let balanceKaruraTokenInit: bigint;460 let balanceKaruraTokenMiddle: bigint;461 let balanceKaruraTokenFinal: bigint;462 let balanceQuartzForeignTokenInit: bigint;463 let balanceQuartzForeignTokenMiddle: bigint;464 let balanceQuartzForeignTokenFinal: bigint;465466 // computed by a test transfer from prod Quartz to prod Karura.467 // 2 QTZ sent https://quartz.subscan.io/xcm_message/kusama-f60d821b049f8835a3005ce7102285006f5b61e9468 // 1.919176000000000000 QTZ received (you can check Karura's chain state in the corresponding block)469 const expectedKaruraIncomeFee = 2000000000000000000n - 1919176000000000000n;470 const karuraEps = 8n * 10n ** 16n;471472 let karuraBackwardTransferAmount: bigint;473474 before(async () => {475 await usingPlaygrounds(async (helper, privateKey) => {476 alice = await privateKey('//Alice');477 [randomAccount] = await helper.arrange.createAccounts([0n], alice);478479 // Set the default version to wrap the first message to other chains.480 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);481 });482483 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {484 const destination = {485 V2: {486 parents: 1,487 interior: {488 X1: {489 Parachain: QUARTZ_CHAIN,490 },491 },492 },493 };494495 const metadata = {496 name: 'Quartz',497 symbol: 'QTZ',498 decimals: 18,499 minimalBalance: 1000000000000000000n,500 };501502 await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);503 await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);504 balanceKaruraTokenInit = await helper.balance.getSubstrate(randomAccount.address);505 balanceQuartzForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});506 });507508 await usingPlaygrounds(async (helper) => {509 await helper.balance.transferToSubstrate(alice, randomAccount.address, 10n * TRANSFER_AMOUNT);510 balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccount.address);511 });512 });513514 itSub('Should connect and send QTZ to Karura', async ({helper}) => {515 const destination = {516 V2: {517 parents: 1,518 interior: {519 X1: {520 Parachain: KARURA_CHAIN,521 },522 },523 },524 };525526 const beneficiary = {527 V2: {528 parents: 0,529 interior: {530 X1: {531 AccountId32: {532 network: 'Any',533 id: randomAccount.addressRaw,534 },535 },536 },537 },538 };539540 const assets = {541 V2: [542 {543 id: {544 Concrete: {545 parents: 0,546 interior: 'Here',547 },548 },549 fun: {550 Fungible: TRANSFER_AMOUNT,551 },552 },553 ],554 };555556 const feeAssetItem = 0;557558 await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');559 balanceQuartzTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);560561 const qtzFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT;562 expect(qtzFees > 0n, 'Negative fees QTZ, looks like nothing was transferred').to.be.true;563 console.log('[Quartz -> Karura] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));564565 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {566 await helper.wait.newBlocks(3);567568 balanceQuartzForeignTokenMiddle = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});569 balanceKaruraTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);570571 const karFees = balanceKaruraTokenInit - balanceKaruraTokenMiddle;572 const qtzIncomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenInit;573 karuraBackwardTransferAmount = qtzIncomeTransfer;574575 const karUnqFees = TRANSFER_AMOUNT - qtzIncomeTransfer;576577 console.log(578 '[Quartz -> Karura] transaction fees on Karura: %s KAR',579 helper.util.bigIntToDecimals(karFees, KARURA_DECIMALS),580 );581 console.log(582 '[Quartz -> Karura] transaction fees on Karura: %s QTZ',583 helper.util.bigIntToDecimals(karUnqFees),584 );585 console.log('[Quartz -> Karura] income %s QTZ', helper.util.bigIntToDecimals(qtzIncomeTransfer));586 expect(karFees == 0n).to.be.true;587588 const bigintAbs = (n: bigint) => (n < 0n) ? -n : n;589590 expect(591 bigintAbs(karUnqFees - expectedKaruraIncomeFee) < karuraEps,592 'Karura took different income fee, check the Karura foreign asset config',593 ).to.be.true;594 });595 });596597 itSub('Should connect to Karura and send QTZ back', async ({helper}) => {598 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {599 const destination = {600 V2: {601 parents: 1,602 interior: {603 X2: [604 {Parachain: QUARTZ_CHAIN},605 {606 AccountId32: {607 network: 'Any',608 id: randomAccount.addressRaw,609 },610 },611 ],612 },613 },614 };615616 const id = {617 ForeignAsset: 0,618 };619620 await helper.xTokens.transfer(randomAccount, id, karuraBackwardTransferAmount, destination, 'Unlimited');621 balanceKaruraTokenFinal = await helper.balance.getSubstrate(randomAccount.address);622 balanceQuartzForeignTokenFinal = await helper.tokens.accounts(randomAccount.address, id);623624 const karFees = balanceKaruraTokenMiddle - balanceKaruraTokenFinal;625 const qtzOutcomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenFinal;626627 console.log(628 '[Karura -> Quartz] transaction fees on Karura: %s KAR',629 helper.util.bigIntToDecimals(karFees, KARURA_DECIMALS),630 );631 console.log('[Karura -> Quartz] outcome %s QTZ', helper.util.bigIntToDecimals(qtzOutcomeTransfer));632633 expect(karFees > 0, 'Negative fees KAR, looks like nothing was transferred').to.be.true;634 expect(qtzOutcomeTransfer == karuraBackwardTransferAmount).to.be.true;635 });636637 await helper.wait.newBlocks(3);638639 balanceQuartzTokenFinal = await helper.balance.getSubstrate(randomAccount.address);640 const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle;641 expect(actuallyDelivered > 0).to.be.true;642643 console.log('[Karura -> Quartz] actually delivered %s QTZ', helper.util.bigIntToDecimals(actuallyDelivered));644645 const qtzFees = karuraBackwardTransferAmount - actuallyDelivered;646 console.log('[Karura -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));647 expect(qtzFees == 0n).to.be.true;648 });649650 itSub('Karura can send only up to its balance', async ({helper}) => {651 // set Karura's sovereign account's balance652 const karuraBalance = 10000n * (10n ** QTZ_DECIMALS);653 const karuraSovereignAccount = helper.address.paraSiblingSovereignAccount(KARURA_CHAIN);654 await helper.getSudo().balance.setBalanceSubstrate(alice, karuraSovereignAccount, karuraBalance);655656 const moreThanKaruraHas = karuraBalance * 2n;657658 let targetAccountBalance = 0n;659 const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);660661 const quartzMultilocation = {662 V2: {663 parents: 1,664 interior: {665 X1: {Parachain: QUARTZ_CHAIN},666 },667 },668 };669670 const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(671 targetAccount.addressRaw,672 {673 Concrete: {674 parents: 0,675 interior: 'Here',676 },677 },678 moreThanKaruraHas,679 );680681 let maliciousXcmProgramSent: any;682 const maxWaitBlocks = 5;683684 // Try to trick Quartz685 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {686 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);687688 maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);689 });690691 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash692 && event.outcome.isFailedToTransactAsset);693694 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);695 expect(targetAccountBalance).to.be.equal(0n);696697 // But Karura still can send the correct amount698 const validTransferAmount = karuraBalance / 2n;699 const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(700 targetAccount.addressRaw,701 {702 Concrete: {703 parents: 0,704 interior: 'Here',705 },706 },707 validTransferAmount,708 );709710 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {711 await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram);712 });713714 await helper.wait.newBlocks(maxWaitBlocks);715716 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);717 expect(targetAccountBalance).to.be.equal(validTransferAmount);718 });719720 itSub('Should not accept reserve transfer of QTZ from Karura', async ({helper}) => {721 const testAmount = 10_000n * (10n ** QTZ_DECIMALS);722 const [targetAccount] = await helper.arrange.createAccounts([0n], alice);723724 const quartzMultilocation = {725 V2: {726 parents: 1,727 interior: {728 X1: {729 Parachain: QUARTZ_CHAIN,730 },731 },732 },733 };734735 const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(736 targetAccount.addressRaw,737 {738 Concrete: {739 parents: 1,740 interior: {741 X1: {742 Parachain: QUARTZ_CHAIN,743 },744 },745 },746 },747 testAmount,748 );749750 const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(751 targetAccount.addressRaw,752 {753 Concrete: {754 parents: 0,755 interior: 'Here',756 },757 },758 testAmount,759 );760761 let maliciousXcmProgramFullIdSent: any;762 let maliciousXcmProgramHereIdSent: any;763 const maxWaitBlocks = 3;764765 // Try to trick Quartz using full QTZ identification766 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {767 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);768769 maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);770 });771772 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash773 && event.outcome.isUntrustedReserveLocation);774775 let accountBalance = await helper.balance.getSubstrate(targetAccount.address);776 expect(accountBalance).to.be.equal(0n);777778 // Try to trick Quartz using shortened QTZ identification779 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {780 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);781782 maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);783 });784785 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash786 && event.outcome.isUntrustedReserveLocation);787788 accountBalance = await helper.balance.getSubstrate(targetAccount.address);789 expect(accountBalance).to.be.equal(0n);790 });791});792793// These tests are relevant only when794// the the corresponding foreign assets are not registered795describeXCM('[XCM] Integration test: Quartz rejects non-native tokens', () => {796 let alice: IKeyringPair;797 let alith: IKeyringPair;798799 const testAmount = 100_000_000_000n;800 let quartzParachainJunction;801 let quartzAccountJunction;802803 let quartzParachainMultilocation: any;804 let quartzAccountMultilocation: any;805 let quartzCombinedMultilocation: any;806807 let messageSent: any;808809 const maxWaitBlocks = 3;810811 before(async () => {812 await usingPlaygrounds(async (helper, privateKey) => {813 alice = await privateKey('//Alice');814815 quartzParachainJunction = {Parachain: QUARTZ_CHAIN};816 quartzAccountJunction = {817 AccountId32: {818 network: 'Any',819 id: alice.addressRaw,820 },821 };822823 quartzParachainMultilocation = {824 V2: {825 parents: 1,826 interior: {827 X1: quartzParachainJunction,828 },829 },830 };831832 quartzAccountMultilocation = {833 V2: {834 parents: 0,835 interior: {836 X1: quartzAccountJunction,837 },838 },839 };840841 quartzCombinedMultilocation = {842 V2: {843 parents: 1,844 interior: {845 X2: [quartzParachainJunction, quartzAccountJunction],846 },847 },848 };849850 // Set the default version to wrap the first message to other chains.851 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);852 });853854 // eslint-disable-next-line require-await855 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {856 alith = helper.account.alithAccount();857 });858 });859860 const expectFailedToTransact = async (helper: DevUniqueHelper, messageSent: any) => {861 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash862 && event.outcome.isFailedToTransactAsset);863 };864865 itSub('Quartz rejects KAR tokens from Karura', async ({helper}) => {866 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {867 const id = {868 Token: 'KAR',869 };870 const destination = quartzCombinedMultilocation;871 await helper.xTokens.transfer(alice, id, testAmount, destination, 'Unlimited');872873 messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);874 });875876 await expectFailedToTransact(helper, messageSent);877 });878879 itSub('Quartz rejects MOVR tokens from Moonriver', async ({helper}) => {880 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {881 const id = 'SelfReserve';882 const destination = quartzCombinedMultilocation;883 await helper.xTokens.transfer(alith, id, testAmount, destination, 'Unlimited');884885 messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);886 });887888 await expectFailedToTransact(helper, messageSent);889 });890891 itSub('Quartz rejects SDN tokens from Shiden', async ({helper}) => {892 await usingShidenPlaygrounds(shidenUrl, async (helper) => {893 const destinationParachain = quartzParachainMultilocation;894 const beneficiary = quartzAccountMultilocation;895 const assets = {896 V2: [{897 id: {898 Concrete: {899 parents: 0,900 interior: 'Here',901 },902 },903 fun: {904 Fungible: testAmount,905 },906 }],907 };908 const feeAssetItem = 0;909910 await helper.executeExtrinsic(alice, 'api.tx.polkadotXcm.reserveWithdrawAssets', [911 destinationParachain,912 beneficiary,913 assets,914 feeAssetItem,915 ]);916917 messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);918 });919920 await expectFailedToTransact(helper, messageSent);921 });922});923924describeXCM('[XCM] Integration test: Exchanging QTZ with Moonriver', () => {925 // Quartz constants926 let alice: IKeyringPair;927 let quartzAssetLocation;928929 let randomAccountQuartz: IKeyringPair;930 let randomAccountMoonriver: IKeyringPair;931932 // Moonriver constants933 let assetId: string;934935 const quartzAssetMetadata = {936 name: 'xcQuartz',937 symbol: 'xcQTZ',938 decimals: 18,939 isFrozen: false,940 minimalBalance: 1n,941 };942943 let balanceQuartzTokenInit: bigint;944 let balanceQuartzTokenMiddle: bigint;945 let balanceQuartzTokenFinal: bigint;946 let balanceForeignQtzTokenInit: bigint;947 let balanceForeignQtzTokenMiddle: bigint;948 let balanceForeignQtzTokenFinal: bigint;949 let balanceMovrTokenInit: bigint;950 let balanceMovrTokenMiddle: bigint;951 let balanceMovrTokenFinal: bigint;952953 before(async () => {954 await usingPlaygrounds(async (helper, privateKey) => {955 alice = await privateKey('//Alice');956 [randomAccountQuartz] = await helper.arrange.createAccounts([0n], alice);957958 balanceForeignQtzTokenInit = 0n;959960 // Set the default version to wrap the first message to other chains.961 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);962 });963964 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {965 const alithAccount = helper.account.alithAccount();966 const baltatharAccount = helper.account.baltatharAccount();967 const dorothyAccount = helper.account.dorothyAccount();968969 randomAccountMoonriver = helper.account.create();970971 // >>> Sponsoring Dorothy >>>972 console.log('Sponsoring Dorothy.......');973 await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n);974 console.log('Sponsoring Dorothy.......DONE');975 // <<< Sponsoring Dorothy <<<976977 quartzAssetLocation = {978 XCM: {979 parents: 1,980 interior: {X1: {Parachain: QUARTZ_CHAIN}},981 },982 };983 const existentialDeposit = 1n;984 const isSufficient = true;985 const unitsPerSecond = 1n;986 const numAssetsWeightHint = 0;987988 const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({989 location: quartzAssetLocation,990 metadata: quartzAssetMetadata,991 existentialDeposit,992 isSufficient,993 unitsPerSecond,994 numAssetsWeightHint,995 });996997 console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);998999 await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);10001001 // >>> Acquire Quartz AssetId Info on Moonriver >>>1002 console.log('Acquire Quartz AssetId Info on Moonriver.......');10031004 assetId = (await helper.assetManager.assetTypeId(quartzAssetLocation)).toString();10051006 console.log('QTZ asset ID is %s', assetId);1007 console.log('Acquire Quartz AssetId Info on Moonriver.......DONE');1008 // >>> Acquire Quartz AssetId Info on Moonriver >>>10091010 // >>> Sponsoring random Account >>>1011 console.log('Sponsoring random Account.......');1012 await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonriver.address, 11_000_000_000_000_000_000n);1013 console.log('Sponsoring random Account.......DONE');1014 // <<< Sponsoring random Account <<<10151016 balanceMovrTokenInit = await helper.balance.getEthereum(randomAccountMoonriver.address);1017 });10181019 await usingPlaygrounds(async (helper) => {1020 await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT);1021 balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccountQuartz.address);1022 });1023 });10241025 itSub('Should connect and send QTZ to Moonriver', async ({helper}) => {1026 const currencyId = {1027 NativeAssetId: 'Here',1028 };1029 const dest = {1030 V2: {1031 parents: 1,1032 interior: {1033 X2: [1034 {Parachain: MOONRIVER_CHAIN},1035 {AccountKey20: {network: 'Any', key: randomAccountMoonriver.address}},1036 ],1037 },1038 },1039 };1040 const amount = TRANSFER_AMOUNT;10411042 await helper.xTokens.transfer(randomAccountQuartz, currencyId, amount, dest, 'Unlimited');10431044 balanceQuartzTokenMiddle = await helper.balance.getSubstrate(randomAccountQuartz.address);1045 expect(balanceQuartzTokenMiddle < balanceQuartzTokenInit).to.be.true;10461047 const transactionFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT;1048 console.log('[Quartz -> Moonriver] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(transactionFees));1049 expect(transactionFees > 0, 'Negative fees QTZ, looks like nothing was transferred').to.be.true;10501051 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1052 await helper.wait.newBlocks(3);10531054 balanceMovrTokenMiddle = await helper.balance.getEthereum(randomAccountMoonriver.address);10551056 const movrFees = balanceMovrTokenInit - balanceMovrTokenMiddle;1057 console.log('[Quartz -> Moonriver] transaction fees on Moonriver: %s MOVR',helper.util.bigIntToDecimals(movrFees));1058 expect(movrFees == 0n).to.be.true;10591060 balanceForeignQtzTokenMiddle = (await helper.assets.account(assetId, randomAccountMoonriver.address))!; // BigInt(qtzRandomAccountAsset['balance']);1061 const qtzIncomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenInit;1062 console.log('[Quartz -> Moonriver] income %s QTZ', helper.util.bigIntToDecimals(qtzIncomeTransfer));1063 expect(qtzIncomeTransfer == TRANSFER_AMOUNT).to.be.true;1064 });1065 });10661067 itSub('Should connect to Moonriver and send QTZ back', async ({helper}) => {1068 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1069 const asset = {1070 V2: {1071 id: {1072 Concrete: {1073 parents: 1,1074 interior: {1075 X1: {Parachain: QUARTZ_CHAIN},1076 },1077 },1078 },1079 fun: {1080 Fungible: TRANSFER_AMOUNT,1081 },1082 },1083 };1084 const destination = {1085 V2: {1086 parents: 1,1087 interior: {1088 X2: [1089 {Parachain: QUARTZ_CHAIN},1090 {AccountId32: {network: 'Any', id: randomAccountQuartz.addressRaw}},1091 ],1092 },1093 },1094 };10951096 await helper.xTokens.transferMultiasset(randomAccountMoonriver, asset, destination, 'Unlimited');10971098 balanceMovrTokenFinal = await helper.balance.getEthereum(randomAccountMoonriver.address);10991100 const movrFees = balanceMovrTokenMiddle - balanceMovrTokenFinal;1101 console.log('[Moonriver -> Quartz] transaction fees on Moonriver: %s MOVR', helper.util.bigIntToDecimals(movrFees));1102 expect(movrFees > 0, 'Negative fees MOVR, looks like nothing was transferred').to.be.true;11031104 const qtzRandomAccountAsset = await helper.assets.account(assetId, randomAccountMoonriver.address);11051106 expect(qtzRandomAccountAsset).to.be.null;11071108 balanceForeignQtzTokenFinal = 0n;11091110 const qtzOutcomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenFinal;1111 console.log('[Quartz -> Moonriver] outcome %s QTZ', helper.util.bigIntToDecimals(qtzOutcomeTransfer));1112 expect(qtzOutcomeTransfer == TRANSFER_AMOUNT).to.be.true;1113 });11141115 await helper.wait.newBlocks(3);11161117 balanceQuartzTokenFinal = await helper.balance.getSubstrate(randomAccountQuartz.address);1118 const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle;1119 expect(actuallyDelivered > 0).to.be.true;11201121 console.log('[Moonriver -> Quartz] actually delivered %s QTZ', helper.util.bigIntToDecimals(actuallyDelivered));11221123 const qtzFees = TRANSFER_AMOUNT - actuallyDelivered;1124 console.log('[Moonriver -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));1125 expect(qtzFees == 0n).to.be.true;1126 });11271128 itSub('Moonriver can send only up to its balance', async ({helper}) => {1129 // set Moonriver's sovereign account's balance1130 const moonriverBalance = 10000n * (10n ** QTZ_DECIMALS);1131 const moonriverSovereignAccount = helper.address.paraSiblingSovereignAccount(MOONRIVER_CHAIN);1132 await helper.getSudo().balance.setBalanceSubstrate(alice, moonriverSovereignAccount, moonriverBalance);11331134 const moreThanMoonriverHas = moonriverBalance * 2n;11351136 let targetAccountBalance = 0n;1137 const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);11381139 const quartzMultilocation = {1140 V2: {1141 parents: 1,1142 interior: {1143 X1: {Parachain: QUARTZ_CHAIN},1144 },1145 },1146 };11471148 const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1149 targetAccount.addressRaw,1150 {1151 Concrete: {1152 parents: 0,1153 interior: 'Here',1154 },1155 },1156 moreThanMoonriverHas,1157 );11581159 let maliciousXcmProgramSent: any;1160 const maxWaitBlocks = 3;11611162 // Try to trick Quartz1163 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1164 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgram]);11651166 // Needed to bypass the call filter.1167 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1168 await helper.fastDemocracy.executeProposal('try to spend more QTZ than Moonriver has', batchCall);11691170 maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1171 });11721173 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash1174 && event.outcome.isFailedToTransactAsset);11751176 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1177 expect(targetAccountBalance).to.be.equal(0n);11781179 // But Moonriver still can send the correct amount1180 const validTransferAmount = moonriverBalance / 2n;1181 const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1182 targetAccount.addressRaw,1183 {1184 Concrete: {1185 parents: 0,1186 interior: 'Here',1187 },1188 },1189 validTransferAmount,1190 );11911192 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1193 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, validXcmProgram]);11941195 // Needed to bypass the call filter.1196 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1197 await helper.fastDemocracy.executeProposal('Spend the correct amount of QTZ', batchCall);1198 });11991200 await helper.wait.newBlocks(maxWaitBlocks);12011202 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1203 expect(targetAccountBalance).to.be.equal(validTransferAmount);1204 });12051206 itSub('Should not accept reserve transfer of QTZ from Moonriver', async ({helper}) => {1207 const testAmount = 10_000n * (10n ** QTZ_DECIMALS);1208 const [targetAccount] = await helper.arrange.createAccounts([0n], alice);12091210 const quartzMultilocation = {1211 V2: {1212 parents: 1,1213 interior: {1214 X1: {1215 Parachain: QUARTZ_CHAIN,1216 },1217 },1218 },1219 };12201221 const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(1222 targetAccount.addressRaw,1223 {1224 Concrete: {1225 parents: 0,1226 interior: {1227 X1: {1228 Parachain: QUARTZ_CHAIN,1229 },1230 },1231 },1232 },1233 testAmount,1234 );12351236 const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(1237 targetAccount.addressRaw,1238 {1239 Concrete: {1240 parents: 0,1241 interior: 'Here',1242 },1243 },1244 testAmount,1245 );12461247 let maliciousXcmProgramFullIdSent: any;1248 let maliciousXcmProgramHereIdSent: any;1249 const maxWaitBlocks = 3;12501251 // Try to trick Quartz using full QTZ identification1252 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1253 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramFullId]);12541255 // Needed to bypass the call filter.1256 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1257 await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using path asset identification', batchCall);12581259 maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1260 });12611262 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash1263 && event.outcome.isUntrustedReserveLocation);12641265 let accountBalance = await helper.balance.getSubstrate(targetAccount.address);1266 expect(accountBalance).to.be.equal(0n);12671268 // Try to trick Quartz using shortened QTZ identification1269 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1270 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramHereId]);12711272 // Needed to bypass the call filter.1273 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1274 await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using "here" asset identification', batchCall);12751276 maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1277 });12781279 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash1280 && event.outcome.isUntrustedReserveLocation);12811282 accountBalance = await helper.balance.getSubstrate(targetAccount.address);1283 expect(accountBalance).to.be.equal(0n);1284 });1285});12861287describeXCM('[XCM] Integration test: Exchanging tokens with Shiden', () => {1288 let alice: IKeyringPair;1289 let sender: IKeyringPair;12901291 const QTZ_ASSET_ID_ON_SHIDEN = 1;1292 const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n;12931294 // Quartz -> Shiden1295 const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden1296 const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?1297 const qtzToShidenTransferred = 10n * (10n ** QTZ_DECIMALS); // 10 QTZ1298 const qtzToShidenArrived = 9_999_999_999_088_000_000n; // 9.999 ... QTZ, Shiden takes a commision in foreign tokens12991300 // Shiden -> Quartz1301 const qtzFromShidenTransfered = 5n * (10n ** QTZ_DECIMALS); // 5 QTZ1302 const qtzOnShidenLeft = qtzToShidenArrived - qtzFromShidenTransfered; // 4.999_999_999_088_000_000n QTZ13031304 let balanceAfterQuartzToShidenXCM: bigint;13051306 before(async () => {1307 await usingPlaygrounds(async (helper, privateKey) => {1308 alice = await privateKey('//Alice');1309 [sender] = await helper.arrange.createAccounts([100n], alice);1310 console.log('sender', sender.address);13111312 // Set the default version to wrap the first message to other chains.1313 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);1314 });13151316 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1317 console.log('1. Create foreign asset and metadata');1318 // TODO update metadata with values from production1319 await helper.assets.create(1320 alice,1321 QTZ_ASSET_ID_ON_SHIDEN,1322 alice.address,1323 QTZ_MINIMAL_BALANCE_ON_SHIDEN,1324 );13251326 await helper.assets.setMetadata(1327 alice,1328 QTZ_ASSET_ID_ON_SHIDEN,1329 'Cross chain QTZ',1330 'xcQTZ',1331 Number(QTZ_DECIMALS),1332 );13331334 console.log('2. Register asset location on Shiden');1335 const assetLocation = {1336 V2: {1337 parents: 1,1338 interior: {1339 X1: {1340 Parachain: QUARTZ_CHAIN,1341 },1342 },1343 },1344 };13451346 await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);13471348 console.log('3. Set QTZ payment for XCM execution on Shiden');1349 await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);13501351 console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)');1352 await helper.balance.transferToSubstrate(alice, sender.address, shidenInitialBalance);1353 });1354 });13551356 itSub('Should connect and send QTZ to Shiden', async ({helper}) => {1357 const destination = {1358 V2: {1359 parents: 1,1360 interior: {1361 X1: {1362 Parachain: SHIDEN_CHAIN,1363 },1364 },1365 },1366 };13671368 const beneficiary = {1369 V2: {1370 parents: 0,1371 interior: {1372 X1: {1373 AccountId32: {1374 network: 'Any',1375 id: sender.addressRaw,1376 },1377 },1378 },1379 },1380 };13811382 const assets = {1383 V2: [1384 {1385 id: {1386 Concrete: {1387 parents: 0,1388 interior: 'Here',1389 },1390 },1391 fun: {1392 Fungible: qtzToShidenTransferred,1393 },1394 },1395 ],1396 };13971398 // Initial balance is 100 QTZ1399 const balanceBefore = await helper.balance.getSubstrate(sender.address);1400 console.log(`Initial balance is: ${balanceBefore}`);14011402 const feeAssetItem = 0;1403 await helper.xcm.limitedReserveTransferAssets(sender, destination, beneficiary, assets, feeAssetItem, 'Unlimited');14041405 // Balance after reserve transfer is less than 901406 balanceAfterQuartzToShidenXCM = await helper.balance.getSubstrate(sender.address);1407 console.log(`QTZ Balance on Quartz after XCM is: ${balanceAfterQuartzToShidenXCM}`);1408 console.log(`Quartz's QTZ commission is: ${balanceBefore-balanceAfterQuartzToShidenXCM}`);1409 expect(balanceBefore - balanceAfterQuartzToShidenXCM > 0).to.be.true;14101411 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1412 await helper.wait.newBlocks(3);1413 const xcQTZbalance = await helper.assets.account(QTZ_ASSET_ID_ON_SHIDEN, sender.address);1414 const shidenBalance = await helper.balance.getSubstrate(sender.address);14151416 console.log(`xcQTZ balance on Shiden after XCM is: ${xcQTZbalance}`);1417 console.log(`Shiden's QTZ commission is: ${qtzToShidenTransferred-xcQTZbalance!}`);14181419 expect(xcQTZbalance).to.eq(qtzToShidenArrived);1420 // SHD balance does not changed:1421 expect(shidenBalance).to.eq(shidenInitialBalance);1422 });1423 });14241425 itSub('Should connect to Shiden and send QTZ back', async ({helper}) => {1426 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1427 const destination = {1428 V2: {1429 parents: 1,1430 interior: {1431 X1: {1432 Parachain: QUARTZ_CHAIN,1433 },1434 },1435 },1436 };14371438 const beneficiary = {1439 V2: {1440 parents: 0,1441 interior: {1442 X1: {1443 AccountId32: {1444 network: 'Any',1445 id: sender.addressRaw,1446 },1447 },1448 },1449 },1450 };14511452 const assets = {1453 V2: [1454 {1455 id: {1456 Concrete: {1457 parents: 1,1458 interior: {1459 X1: {1460 Parachain: QUARTZ_CHAIN,1461 },1462 },1463 },1464 },1465 fun: {1466 Fungible: qtzFromShidenTransfered,1467 },1468 },1469 ],1470 };14711472 // Initial balance is 1 SDN1473 const balanceSDNbefore = await helper.balance.getSubstrate(sender.address);1474 console.log(`SDN balance is: ${balanceSDNbefore}, it does not changed`);1475 expect(balanceSDNbefore).to.eq(shidenInitialBalance);14761477 const feeAssetItem = 0;1478 // this is non-standard polkadotXcm extension for Astar only. It calls InitiateReserveWithdraw1479 await helper.executeExtrinsic(sender, 'api.tx.polkadotXcm.reserveWithdrawAssets', [destination, beneficiary, assets, feeAssetItem]);14801481 // Balance after reserve transfer is less than 1 SDN1482 const xcQTZbalance = await helper.assets.account(QTZ_ASSET_ID_ON_SHIDEN, sender.address);1483 const balanceSDN = await helper.balance.getSubstrate(sender.address);1484 console.log(`xcQTZ balance on Shiden after XCM is: ${xcQTZbalance}`);14851486 // Assert: xcQTZ balance correctly decreased1487 expect(xcQTZbalance).to.eq(qtzOnShidenLeft);1488 // Assert: SDN balance is 0.996...1489 expect(balanceSDN / (10n ** (SHIDEN_DECIMALS - 3n))).to.eq(996n);1490 });14911492 await helper.wait.newBlocks(3);1493 const balanceQTZ = await helper.balance.getSubstrate(sender.address);1494 console.log(`QTZ Balance on Quartz after XCM is: ${balanceQTZ}`);1495 expect(balanceQTZ).to.eq(balanceAfterQuartzToShidenXCM + qtzFromShidenTransfered);1496 });14971498 itSub('Shiden can send only up to its balance', async ({helper}) => {1499 // set Shiden's sovereign account's balance1500 const shidenBalance = 10000n * (10n ** QTZ_DECIMALS);1501 const shidenSovereignAccount = helper.address.paraSiblingSovereignAccount(SHIDEN_CHAIN);1502 await helper.getSudo().balance.setBalanceSubstrate(alice, shidenSovereignAccount, shidenBalance);15031504 const moreThanShidenHas = shidenBalance * 2n;15051506 let targetAccountBalance = 0n;1507 const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);15081509 const quartzMultilocation = {1510 V2: {1511 parents: 1,1512 interior: {1513 X1: {Parachain: QUARTZ_CHAIN},1514 },1515 },1516 };15171518 const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1519 targetAccount.addressRaw,1520 {1521 Concrete: {1522 parents: 0,1523 interior: 'Here',1524 },1525 },1526 moreThanShidenHas,1527 );15281529 let maliciousXcmProgramSent: any;1530 const maxWaitBlocks = 3;15311532 // Try to trick Quartz1533 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1534 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);15351536 maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1537 });15381539 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash1540 && event.outcome.isFailedToTransactAsset);15411542 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1543 expect(targetAccountBalance).to.be.equal(0n);15441545 // But Shiden still can send the correct amount1546 const validTransferAmount = shidenBalance / 2n;1547 const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1548 targetAccount.addressRaw,1549 {1550 Concrete: {1551 parents: 0,1552 interior: 'Here',1553 },1554 },1555 validTransferAmount,1556 );15571558 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1559 await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram);1560 });15611562 await helper.wait.newBlocks(maxWaitBlocks);15631564 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1565 expect(targetAccountBalance).to.be.equal(validTransferAmount);1566 });15671568 itSub('Should not accept reserve transfer of QTZ from Shiden', async ({helper}) => {1569 const testAmount = 10_000n * (10n ** QTZ_DECIMALS);1570 const [targetAccount] = await helper.arrange.createAccounts([0n], alice);15711572 const quartzMultilocation = {1573 V2: {1574 parents: 1,1575 interior: {1576 X1: {1577 Parachain: QUARTZ_CHAIN,1578 },1579 },1580 },1581 };15821583 const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(1584 targetAccount.addressRaw,1585 {1586 Concrete: {1587 parents: 1,1588 interior: {1589 X1: {1590 Parachain: QUARTZ_CHAIN,1591 },1592 },1593 },1594 },1595 testAmount,1596 );15971598 const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(1599 targetAccount.addressRaw,1600 {1601 Concrete: {1602 parents: 0,1603 interior: 'Here',1604 },1605 },1606 testAmount,1607 );16081609 let maliciousXcmProgramFullIdSent: any;1610 let maliciousXcmProgramHereIdSent: any;1611 const maxWaitBlocks = 3;16121613 // Try to trick Quartz using full QTZ identification1614 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1615 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);16161617 maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1618 });16191620 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash1621 && event.outcome.isUntrustedReserveLocation);16221623 let accountBalance = await helper.balance.getSubstrate(targetAccount.address);1624 expect(accountBalance).to.be.equal(0n);16251626 // Try to trick Quartz using shortened QTZ identification1627 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1628 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);16291630 maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1631 });16321633 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash1634 && event.outcome.isUntrustedReserveLocation);16351636 accountBalance = await helper.balance.getSubstrate(targetAccount.address);1637 expect(accountBalance).to.be.equal(0n);1638 });1639});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),
);