--- a/tests/package.json +++ b/tests/package.json @@ -116,6 +116,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", --- /dev/null +++ b/tests/src/xcm/lowLevelXcmQuartz.test.ts @@ -0,0 +1,381 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {IKeyringPair} from '@polkadot/types/types'; +import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingMoonriverPlaygrounds, usingShidenPlaygrounds} from '../util'; +import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev'; +import {STATEMINE_CHAIN, QUARTZ_CHAIN, KARURA_CHAIN, MOONRIVER_CHAIN, SHIDEN_CHAIN, STATEMINE_DECIMALS, KARURA_DECIMALS, QTZ_DECIMALS, RELAY_DECIMALS, SHIDEN_DECIMALS, karuraUrl, moonriverUrl, relayUrl, shidenUrl, statemineUrl, SAFE_XCM_VERSION, XcmTestHelper, TRANSFER_AMOUNT} from './xcm.types'; + + +const testHelper = new XcmTestHelper('quartz'); + +describeXCM('[XCMLL] Integration test: Exchanging tokens with Karura', () => { + let alice: IKeyringPair; + let randomAccount: IKeyringPair; + + let balanceQuartzTokenInit: bigint; + let balanceQuartzTokenMiddle: bigint; + let balanceQuartzTokenFinal: bigint; + let balanceKaruraTokenInit: bigint; + let balanceKaruraTokenMiddle: bigint; + let balanceKaruraTokenFinal: bigint; + let balanceQuartzForeignTokenInit: bigint; + let balanceQuartzForeignTokenMiddle: bigint; + let balanceQuartzForeignTokenFinal: bigint; + + // computed by a test transfer from prod Quartz to prod Karura. + // 2 QTZ sent https://quartz.subscan.io/xcm_message/kusama-f60d821b049f8835a3005ce7102285006f5b61e9 + // 1.919176000000000000 QTZ received (you can check Karura's chain state in the corresponding block) + const expectedKaruraIncomeFee = 2000000000000000000n - 1919176000000000000n; + const karuraEps = 8n * 10n ** 16n; + + let karuraBackwardTransferAmount: bigint; + + 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, + }; + + await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata); + await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n); + balanceKaruraTokenInit = await helper.balance.getSubstrate(randomAccount.address); + balanceQuartzForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0}); + }); + + await usingPlaygrounds(async (helper) => { + await helper.balance.transferToSubstrate(alice, randomAccount.address, 10n * TRANSFER_AMOUNT); + balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccount.address); + }); + }); + + 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; + let alith: IKeyringPair; + + const testAmount = 100_000_000_000n; + let quartzParachainJunction; + let quartzAccountJunction; + + let quartzParachainMultilocation: any; + let quartzAccountMultilocation: any; + let quartzCombinedMultilocation: any; + + let messageSent: any; + + const maxWaitBlocks = 3; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + + quartzParachainJunction = {Parachain: QUARTZ_CHAIN}; + quartzAccountJunction = { + AccountId32: { + network: 'Any', + id: alice.addressRaw, + }, + }; + + quartzParachainMultilocation = { + V2: { + parents: 1, + interior: { + X1: quartzParachainJunction, + }, + }, + }; + + quartzAccountMultilocation = { + V2: { + parents: 0, + interior: { + X1: quartzAccountJunction, + }, + }, + }; + + quartzCombinedMultilocation = { + V2: { + parents: 1, + interior: { + X2: [quartzParachainJunction, quartzAccountJunction], + }, + }, + }; + + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + + // eslint-disable-next-line require-await + await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => { + alith = helper.account.alithAccount(); + }); + }); + + 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, + }; + + let balanceQuartzTokenInit: bigint; + let balanceQuartzTokenMiddle: bigint; + let balanceQuartzTokenFinal: bigint; + let balanceForeignQtzTokenInit: bigint; + let balanceForeignQtzTokenMiddle: bigint; + let balanceForeignQtzTokenFinal: bigint; + let balanceMovrTokenInit: bigint; + let balanceMovrTokenMiddle: bigint; + let balanceMovrTokenFinal: bigint; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + [randomAccountQuartz] = await helper.arrange.createAccounts([0n], alice); + + balanceForeignQtzTokenInit = 0n; + + // 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; + + 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 <<< + + balanceMovrTokenInit = await helper.balance.getEthereum(randomAccountMoonriver.address); + }); + + await usingPlaygrounds(async (helper) => { + await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT); + balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccountQuartz.address); + }); + }); + + 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.reserveTransferUNQfrom('moonriver', alice); + }); +}); + +describeXCM('[XCMLL] Integration test: Exchanging tokens with Shiden', () => { + let alice: IKeyringPair; + let sender: IKeyringPair; + + const QTZ_ASSET_ID_ON_SHIDEN = 1; + const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n; + + // Quartz -> Shiden + const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden + const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours? + const qtzToShidenTransferred = 10n * (10n ** QTZ_DECIMALS); // 10 QTZ + const qtzToShidenArrived = 9_999_999_999_088_000_000n; // 9.999 ... QTZ, Shiden takes a commision in foreign tokens + + // Shiden -> Quartz + const qtzFromShidenTransfered = 5n * (10n ** QTZ_DECIMALS); // 5 QTZ + const qtzOnShidenLeft = qtzToShidenArrived - qtzFromShidenTransfered; // 4.999_999_999_088_000_000n QTZ + + let balanceAfterQuartzToShidenXCM: bigint; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + [sender] = await helper.arrange.createAccounts([100n], alice); + console.log('sender', sender.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) => { + console.log('1. Create foreign asset and metadata'); + // TODO update metadata with values from production + await helper.assets.create( + alice, + QTZ_ASSET_ID_ON_SHIDEN, + alice.address, + QTZ_MINIMAL_BALANCE_ON_SHIDEN, + ); + + await helper.assets.setMetadata( + alice, + QTZ_ASSET_ID_ON_SHIDEN, + 'Cross chain QTZ', + 'xcQTZ', + 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]); + + console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)'); + await helper.balance.transferToSubstrate(alice, sender.address, shidenInitialBalance); + }); + }); + + itSub('Should connect and send QTZ to Shiden', async () => { + await testHelper.sendUnqTo('shiden', sender); + }); + + itSub('Should connect to Shiden and send QTZ back', async () => { + await testHelper.sendUnqBack('shiden', alice, sender); + }); + + 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.reserveTransferUNQfrom('shiden', alice); + }); +}); --- a/tests/src/xcm/lowLevelXcmUnique.test.ts +++ b/tests/src/xcm/lowLevelXcmUnique.test.ts @@ -20,7 +20,7 @@ import {Event} from '../util/playgrounds/unique.dev'; 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'; +import {ASTAR_DECIMALS, NETWORKS, SAFE_XCM_VERSION, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper, 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; @@ -34,6 +34,7 @@ let balanceUniqueTokenFinal: bigint; let unqFees: bigint; +const testHelper = new XcmTestHelper('unique'); async function genericSendUnqTo( networkName: keyof typeof NETWORKS, @@ -434,20 +435,20 @@ }); 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.reserveTransferUNQfrom('polkadex', alice); }); }); @@ -574,19 +575,19 @@ }); 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.reserveTransferUNQfrom('moonbeam', alice); }); }); --- a/tests/src/xcm/xcm.types.ts +++ b/tests/src/xcm/xcm.types.ts @@ -1,4 +1,5 @@ -import {usingAcalaPlaygrounds, usingAstarPlaygrounds, usingMoonbeamPlaygrounds, usingPolkadexPlaygrounds} from '../util'; +import {IKeyringPair} from '@polkadot/types/types'; +import {expect, usingAcalaPlaygrounds, usingAstarPlaygrounds, usingKaruraPlaygrounds, usingMoonbeamPlaygrounds, usingMoonriverPlaygrounds, usingPlaygrounds, usingPolkadexPlaygrounds, usingShidenPlaygrounds} from '../util'; import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev'; import config from '../config'; @@ -9,19 +10,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: { @@ -52,9 +73,15 @@ astar: usingAstarPlaygrounds, polkadex: usingPolkadexPlaygrounds, moonbeam: usingMoonbeamPlaygrounds, + moonriver: usingMoonriverPlaygrounds, + karura: usingKaruraPlaygrounds, + shiden: usingShidenPlaygrounds, } as const; +type NetworkNames = keyof typeof NETWORKS; -export function mapToChainId(networkName: keyof typeof NETWORKS) { +type NativeRuntime = 'opal' | 'quartz' | 'unique'; + +export function mapToChainId(networkName: keyof typeof NETWORKS): number { switch (networkName) { case 'acala': return ACALA_CHAIN; @@ -64,10 +91,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 +110,364 @@ return moonbeamUrl; case 'polkadex': return polkadexUrl; + case 'moonriver': + return moonriverUrl; + case 'karura': + return karuraUrl; + case 'shiden': + return shidenUrl; } } -export function getDevPlayground(name: T) { +export function getDevPlayground(name: NetworkNames) { return NETWORKS[name]; +} + +export 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; + +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 10; + 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; + } + } + + 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('[Unique -> %s] transaction fees on Unique: %s UNQ', networkName, helper.util.bigIntToDecimals(this._unqFees)); + expect(this._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 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, 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); + + 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, 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 reserveTransferUNQfrom(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, + { + 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, 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); + + 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, 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); + }); + } + } \ No newline at end of file --- a/tests/src/xcm/xcmQuartz.test.ts +++ b/tests/src/xcm/xcmQuartz.test.ts @@ -15,29 +15,13 @@ // along with Unique Network. If not, see . import {IKeyringPair} from '@polkadot/types/types'; -import config from '../config'; import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util'; import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev'; +import {STATEMINE_CHAIN, QUARTZ_CHAIN, KARURA_CHAIN, MOONRIVER_CHAIN, SHIDEN_CHAIN, STATEMINE_DECIMALS, KARURA_DECIMALS, QTZ_DECIMALS, RELAY_DECIMALS, SHIDEN_DECIMALS, karuraUrl, moonriverUrl, relayUrl, shidenUrl, statemineUrl} from './xcm.types'; + -const QUARTZ_CHAIN = +(process.env.RELAY_QUARTZ_ID || 2095); -const STATEMINE_CHAIN = +(process.env.RELAY_STATEMINE_ID || 1000); -const KARURA_CHAIN = +(process.env.RELAY_KARURA_ID || 2000); -const MOONRIVER_CHAIN = +(process.env.RELAY_MOONRIVER_ID || 2023); -const SHIDEN_CHAIN = +(process.env.RELAY_SHIDEN_ID || 2007); const STATEMINE_PALLET_INSTANCE = 50; - -const relayUrl = config.relayUrl; -const statemineUrl = config.statemineUrl; -const karuraUrl = config.karuraUrl; -const moonriverUrl = config.moonriverUrl; -const shidenUrl = config.shidenUrl; - -const RELAY_DECIMALS = 12; -const STATEMINE_DECIMALS = 12; -const KARURA_DECIMALS = 12; -const SHIDEN_DECIMALS = 18n; -const QTZ_DECIMALS = 18n; const TRANSFER_AMOUNT = 2000000000000000000000000n;