diff --git a/test/helpers/assets.ts b/test/helpers/assets.ts index 6dcf116bb1..9cb3389cfc 100644 --- a/test/helpers/assets.ts +++ b/test/helpers/assets.ts @@ -12,6 +12,7 @@ import type { import type { AccountId20 } from "@polkadot/types/interfaces/runtime"; import { encodeFunctionData, parseAbi, keccak256 } from "viem"; import { ApiPromise, WsProvider } from "@polkadot/api"; +import { alith } from "@moonwall/util"; export const EVM_FOREIGN_ASSETS_PALLET_ACCOUNT = "0x6d6f646c666f7267617373740000000000000000"; export const ARBITRARY_ASSET_ID = 42259045809535163221576417993425387648n; @@ -20,19 +21,21 @@ export const DUMMY_REVERT_BYTECODE = "0x60006000fd"; export const RELAY_SOURCE_LOCATION = { Xcm: { parents: 1, interior: "Here" } }; export const RELAY_SOURCE_LOCATION2 = { Xcm: { parents: 2, interior: "Here" } }; export const RELAY_V3_SOURCE_LOCATION = { V3: { parents: 1, interior: "Here" } } as any; -export const PARA_1000_SOURCE_LOCATION_V3 = { +export const PARA_1000_SOURCE_LOCATION = { Xcm: { parents: 1, interior: { X1: { Parachain: 1000 } } }, }; -export const PARA_2000_SOURCE_LOCATION = { - Xcm: { parents: 1, interior: { X1: { Parachain: 2000 } } }, -}; export const PARA_1001_SOURCE_LOCATION = { Xcm: { parents: 1, interior: { X1: { Parachain: 1001 } } }, }; +export const PARA_2000_SOURCE_LOCATION = { + Xcm: { parents: 1, interior: { X1: { Parachain: 2000 } } }, +}; // XCM V4 Locations export const RELAY_SOURCE_LOCATION_V4 = { parents: 1, interior: { here: null } }; export const PARA_1000_SOURCE_LOCATION_V4 = { parents: 1, interior: { X1: [{ Parachain: 1000 }] } }; +export const PARA_1001_SOURCE_LOCATION_V4 = { parents: 1, interior: { X1: [{ Parachain: 1001 }] } }; + export interface AssetMetadata { name: string; symbol: string; @@ -47,12 +50,31 @@ export const relayAssetMetadata: AssetMetadata = { isFrozen: false, }; +export interface TestAsset { + // The asset id as required by pallet - moonbeam - foreign - assets + id: bigint | string; + // The asset's XCM location (preferably a v4) + location: any; + // The asset's metadata + metadata: AssetMetadata; + // The asset's relative price as required by pallet - xcm - weight - trader + relativePrice?: bigint; +} + export function assetContractAddress(assetId: bigint | string): `0x${string}` { - return `0xffffffff${BigInt(assetId).toString(16)}`; + // Prefix as defined in pallet - moonbeam - foreign - assets (4 bytes) + const contractBaseAddress = "0xffffffff"; + // Asset part (padded to 16 bytes) + const assetAddressBytes = BigInt(assetId).toString(16).padStart(32, "0"); + return `${contractBaseAddress}${assetAddressBytes}`; } export const patchLocationV4recursively = (value: any) => { // e.g. Convert this: { X1: { Parachain: 1000 } } to { X1: [ { Parachain: 1000 } ] } + // Also, will remove the Xcm key if it exists. + if (value && value.Xcm !== undefined) { + value = value.Xcm; + } if (value && typeof value == "object") { if (Array.isArray(value)) { return value.map(patchLocationV4recursively); @@ -191,7 +213,7 @@ export async function calculateRelativePrice( return relativePrice; } -function getSupportedAssedStorageKey(asset: any, context: any) { +function getSupportedAssetStorageKey(asset: any, context: any) { const assetV4 = patchLocationV4recursively(asset); const module = xxhashAsU8a(new TextEncoder().encode("XcmWeightTrader"), 128); @@ -207,14 +229,25 @@ function getSupportedAssedStorageKey(asset: any, context: any) { return new Uint8Array([...module, ...method, ...blake2concatStagingXcmV4Location]); } -export async function addAssetToWeightTrader(asset: any, relativePrice: number, context: any) { +/** + * Adds asset to the pallet - xcm - weight - trader, with the relative provided. + * If the relative price is 0, the asset will be added with a placeholder price. + * + * @param assetLocation XCM v4 location of asset + * @param relativePrice the pallet requires 18 decimals balance needed to equal 1 unit of native + * asset. For example, if the asset price is twice as low as the GLMR price, + * the relative price should be 500_000_000_000_000_000n. + * + * @param context + */ +export async function addAssetToWeightTrader(asset: any, relativePrice: bigint, context: any) { const assetV4 = patchLocationV4recursively(asset.Xcm); - if (relativePrice == 0) { + if (relativePrice == 0n) { const addAssetWithPlaceholderPrice = context .polkadotJs() .tx.sudo.sudo(context.polkadotJs().tx.xcmWeightTrader.addAsset(assetV4, 1n)); - const overallAssetKey = getSupportedAssedStorageKey(assetV4, context); + const overallAssetKey = getSupportedAssetStorageKey(assetV4, context); const overrideAssetPrice = context.polkadotJs().tx.sudo.sudo( context.polkadotJs().tx.system.setStorage([ @@ -288,7 +321,7 @@ export async function registerOldForeignAsset( .polkadotJs() .tx.sudo.sudo(context.polkadotJs().tx.xcmWeightTrader.addAsset(assetV4, relativePrice)), { - expectEvents: [context.polkadotJs().events.xcmWeightTrader.SupportedAssetAdded], + expectEvents: [context.polkadotJs().events.xcmWeightTrader.SupportedAssetAdded as any], allowFailures: false, } ); @@ -344,9 +377,16 @@ export async function registerOldForeignAsset( } /** - * This registers a foreign asset via the moonbeam-foreign-assets pallet. + * + * This registers a foreign asset via the moonbeam - foreign - assets pallet. * This call will deploy the contract and make the erc20 contract of the asset available - * in the following address: 0xffffffff + metadata.id + * in the following address: 0xffffffff + assetId + * + * @param context + * @param assetId a bigint representing the assetId (it can be arbitrary for tests) + * @param xcmLocation the XCM location of the asset in Polkadot + * @param metadata + * @returns */ export async function registerForeignAsset( context: DevModeContext, @@ -355,6 +395,10 @@ export async function registerForeignAsset( metadata: AssetMetadata ) { const { decimals, name, symbol } = metadata; + + // Sanitize Xcm Location + xcmLocation = patchLocationV4recursively(xcmLocation); + const { result } = await context.createBlock( context .polkadotJs() @@ -400,13 +444,10 @@ export async function mockAssetBalance( context: DevModeContext, assetBalance: bigint, assetId: bigint, - assetLocation: any, sudoAccount: KeyringPair, - account: string | AccountId20 + account: `0x${string}` ) { const api = context.polkadotJs(); - // Register the asset - await registerForeignAsset(context, assetId, RELAY_SOURCE_LOCATION, relayAssetMetadata); const xcmTransaction = { V2: { @@ -433,6 +474,27 @@ export async function mockAssetBalance( return; } +/* Registers foreign asset and calls mock asset balance */ +export async function registerAndFundAsset( + context: any, + asset: TestAsset, + amount: bigint, + address: `0x${string}` +) { + const { registeredAssetId } = await registerForeignAsset( + context, + BigInt(asset.id), + asset.location, + asset.metadata + ); + + await addAssetToWeightTrader(asset.location, asset.relativePrice || 0n, context); + + await mockAssetBalance(context, amount, BigInt(asset.id), alith, address); + + return registeredAssetId; +} + // Mock balance for old foreign assets // DEPRECATED: Please don't use for new tests export async function mockOldAssetBalance( diff --git a/test/helpers/xcm.ts b/test/helpers/xcm.ts index 12bedafe88..3fae235449 100644 --- a/test/helpers/xcm.ts +++ b/test/helpers/xcm.ts @@ -388,6 +388,36 @@ export class XcmFragment { return this; } + // Add a `DepositAsset` instruction for specific beneficiary and token + deposit_asset_definite( + location: any, + amount: bigint, + beneficiary: `0x${string}`, + network: XcmV3JunctionNetworkId["type"] | null = null + ): this { + this.instructions.push({ + DepositAsset: { + assets: { + Definite: [ + { + id: { + Concrete: location, + }, + fun: { + Fungible: amount, + }, + }, + ], + }, + beneficiary: { + parents: 0, + interior: { X1: { AccountKey20: { network, key: beneficiary } } }, + }, + }, + }); + return this; + } + // Add a `SetErrorHandler` instruction, appending all the nested instructions set_error_handler_with(callbacks: XcmCallback[]): this { const error_instructions: any[] = []; diff --git a/test/suites/dev/moonbase/test-assets/test-foreign-assets-balances.ts b/test/suites/dev/moonbase/test-assets/test-foreign-assets-balances.ts index 57bfdbedfd..ca3e647676 100644 --- a/test/suites/dev/moonbase/test-assets/test-foreign-assets-balances.ts +++ b/test/suites/dev/moonbase/test-assets/test-foreign-assets-balances.ts @@ -7,6 +7,8 @@ import { RELAY_SOURCE_LOCATION_V4, foreignAssetBalance, mockAssetBalance, + registerForeignAsset, + relayAssetMetadata, } from "../../../../helpers"; describeSuite({ @@ -22,7 +24,10 @@ describeSuite({ const assetLocation = RELAY_SOURCE_LOCATION_V4; const assetId = ARBITRARY_ASSET_ID; - await mockAssetBalance(context, someBalance, assetId, assetLocation, alith, ALITH_ADDRESS); + // Register the asset + await registerForeignAsset(context, assetId, assetLocation, relayAssetMetadata); + // Mock asset balance + await mockAssetBalance(context, someBalance, assetId, alith, ALITH_ADDRESS); const newBalance = await foreignAssetBalance(context, assetId, ALITH_ADDRESS); expect(newBalance).toBe(someBalance); diff --git a/test/suites/dev/moonbase/test-xcm-v4/test-xcm-evm-fee-with-origin-token.ts b/test/suites/dev/moonbase/test-xcm-v4/test-xcm-evm-fee-with-origin-token.ts new file mode 100644 index 0000000000..6c85da3d22 --- /dev/null +++ b/test/suites/dev/moonbase/test-xcm-v4/test-xcm-evm-fee-with-origin-token.ts @@ -0,0 +1,187 @@ +import "@moonbeam-network/api-augment"; +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; + +import { ethan } from "@moonwall/util"; +import { ApiPromise } from "@polkadot/api"; + +import { + AssetMetadata, + PARA_1000_SOURCE_LOCATION, + PARA_1001_SOURCE_LOCATION, + TestAsset, + foreignAssetBalance, + registerAndFundAsset, + verifyLatestBlockFees, +} from "../../../../helpers/index.js"; + +import { + RawXcmMessage, + XcmFragment, + descendOriginFromAddress20, + injectHrmpMessageAndSeal, +} from "../../../../helpers/xcm.js"; + +export const InterlayAssetMetadata: AssetMetadata = { + name: "INTR", + symbol: "INTR", + decimals: 12n, + isFrozen: false, +}; + +export const MaticAssetMetadata: AssetMetadata = { + name: "MATIC", + symbol: "MATIC", + decimals: 12n, + isFrozen: false, +}; + +const DEFAULT_ADDRESS = "0x0101010101010101010101010101010101010101"; + +/** + * We are going to test the following scenario: + * + * An account in the origin parachain, let's say Interlay, wants to send MATIC tokens from + * Interlay to Moonbeam through XCM, and wants to pay the transaction with in the origin + * chain's token, INTR, so they don't need to buy GLMR. + * + * From Moonbeam's point of view, this is an incoming transfer, and we need to test that + * the XCM transaction went through, and the assets in the origin account were + * succesfully deducted. + * + * For this we're going to create two foreign assets, xcINTR for paying fees and xcMATIC to be + * transferred. And we are going to fund an account in the origin's chain asset. + * + * Parachain 1000 would be the location of the sending parachain (INTR). + * Parachain 1001 would be the location of MATIC. + * + */ +describeSuite({ + id: "D014137", + title: "Mock XCM - Transfer some ERC20 token and pay with origin chain's token", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let sendingAddress: `0x${string}`; + let descendAddress: `0x${string}`; + let api: ApiPromise; + + const initialBalance: bigint = 500_000_000_000_000n; + const xcMaticToSend = 3_500_000_000n; + + const xcIntrAsset: TestAsset = { + id: 1000100010001000n, + location: PARA_1000_SOURCE_LOCATION, + metadata: InterlayAssetMetadata, + relativePrice: 1_000_000_000_000_000_000n, + }; + + const xcMaticAsset: TestAsset = { + id: 1001100110011001n, + location: PARA_1001_SOURCE_LOCATION, + metadata: MaticAssetMetadata, + relativePrice: 1_000_000_000_000_000_000n, + }; + + beforeAll(async () => { + api = context.polkadotJs(); + + const { originAddress, descendOriginAddress } = descendOriginFromAddress20( + context, + DEFAULT_ADDRESS, + 1000 + ); + + sendingAddress = originAddress; + descendAddress = descendOriginAddress; + + // Register foreign asset used to pay fees (i.e. xcINTR) + await registerAndFundAsset(context, xcIntrAsset, initialBalance, descendAddress); + + // Register foreign asset used to transfer(i.e.xcMatic) + await registerAndFundAsset(context, xcMaticAsset, initialBalance, descendAddress); + }); + + it({ + id: "T01", + title: "should receive foreign asset transfer, paying fees in origin chain's foreign asset", + test: async function () { + // 3. Build incoming XCM message + const xcmMessage = new XcmFragment({ + assets: [ + { + multilocation: PARA_1000_SOURCE_LOCATION.Xcm, + fungible: 200_000_000_000_000n, + }, + { + multilocation: PARA_1001_SOURCE_LOCATION.Xcm, + fungible: xcMaticToSend, + }, + ], + weight_limit: { + refTime: 50_000_000_000n, + proofSize: 150000n, + }, + descend_origin: sendingAddress, + }) + .descend_origin() + .withdraw_asset() + .buy_execution() + .deposit_asset_definite( + PARA_1001_SOURCE_LOCATION.Xcm, + 500_000n, + ethan.address as `0x${string}` + ) + .deposit_asset_definite( + PARA_1000_SOURCE_LOCATION.Xcm, + 500_000n, + descendAddress as `0x${string}` + ) + .as_v4(); + + await verifyLatestBlockFees(context); + + const xcIntrBalanceBefore = await foreignAssetBalance( + context, + BigInt(xcIntrAsset.id), + descendAddress + ); + + const xcMaticBalanceBefore = await foreignAssetBalance( + context, + BigInt(xcMaticAsset.id), + descendAddress + ); + + // Simulate reception of an incoming XCM message and create block to execute it + await injectHrmpMessageAndSeal(context, 1000, { + type: "XcmVersionedXcm", + payload: xcmMessage, + } as RawXcmMessage); + + const xcIntrBalanceAfter = await foreignAssetBalance( + context, + BigInt(xcIntrAsset.id), + descendAddress + ); + + const xcMaticBalanceAfter = await foreignAssetBalance( + context, + BigInt(xcMaticAsset.id), + descendAddress + ); + + const xcMaticBalanceEthan = await foreignAssetBalance( + context, + BigInt(xcMaticAsset.id), + ethan.address as `0x${string}` + ); + + // Check that xcIntr where debited from Alith's descend address + // to pay the fees of the XCM execution + expect(xcIntrBalanceBefore - xcIntrBalanceAfter).to.be.eq(199_999_999_500_000n); + // Check that xcMatic where transferred to Ethan + expect(xcMaticBalanceBefore - xcMaticBalanceAfter).to.be.eq(xcMaticToSend); + expect(xcMaticBalanceEthan).to.be.eq(500_000n); + }, + }); + }, +});