Skip to content

Commit

Permalink
Test XCM transfer and pay fees with foreign asset (#3047)
Browse files Browse the repository at this point in the history
* Add helper to deposit specific asset from holding register to a beneficiary

* Add xcm test - incoming message can be payed in foreign asset

* make assets helper more readable

---------

Co-authored-by: Agustín Rodriguez <[email protected]>
  • Loading branch information
gonzamontiel and Agusrodri authored Nov 25, 2024
1 parent 7bd296e commit c6d07d7
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 17 deletions.
94 changes: 78 additions & 16 deletions test/helpers/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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([
Expand Down Expand Up @@ -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,
}
);
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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: {
Expand All @@ -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(
Expand Down
30 changes: 30 additions & 0 deletions test/helpers/xcm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
RELAY_SOURCE_LOCATION_V4,
foreignAssetBalance,
mockAssetBalance,
registerForeignAsset,
relayAssetMetadata,
} from "../../../../helpers";

describeSuite({
Expand All @@ -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);
Expand Down
Loading

0 comments on commit c6d07d7

Please sign in to comment.