Using the XCM Transactor Pallet for Remote Executions¶
Introduction¶
XCM messages are comprised of a series of instructions that are executed by the Cross-Consensus Virtual Machine (XCVM). Combinations of these instructions result in predetermined actions such as cross-chain token transfers and, more interestingly, remote cross-chain execution.
Nevertheless, building an XCM message from scratch is somewhat tricky. Moreover, XCM messages are sent to other participants in the ecosystem from the root account (that is, SUDO or through a democratic vote), which is not ideal for projects that want to leverage remote cross-chain calls via a simple transaction.
To overcome these issues, developers can leverage wrapper functions/pallets to use XCM features on Polkadot/Kusama, such as the XCM Transactor Pallet. In that aspect, the XCM Transactor Pallet allows users to perform remote cross-chain calls through different methods that dispatch the call from different derivated accounts so that they can be easily executed with a simple transaction.
The two main extrinsics of the pallet are transacting through a sovereign derivative account or a derivative account calculated from a given multilocation. Each extrinsic is named accordingly.
This guide will show you how to use the XCM Transactor Pallet to send XCM messages from a Moonbeam-based network to other chains in the ecosystem (relay chain/parachains). In addition, you'll also learn how to use the XCM Transactor Precompile to perform the same actions via the Ethereum API.
Note that there are still limitations in what you can remotely execute through XCM messages.
Developers must understand that sending incorrect XCM messages can result in the loss of funds. Consequently, it is essential to test XCM features on a TestNet before moving to a production environment.
XCM Instructions For Remote Execution¶
The relevant XCM instructions to perform remote execution through XCM are, but not limited to:
DescendOrigin
- gets executed in the target chain. Mutates the origin that will be used for executing the subsequent XCM instructionsWithdrawAsset
- gets executed in the target chain. Removes assets and places them into the holding registerBuyExecution
- gets executed in the target chain. Takes the assets from holding to pay for execution fees. The fees to pay are determined by the target chainTransact
- gets executed in the target chain. Dispatches the encoded call data from a given origin
When the XCM message built by the XCM Transactor Pallet is executed, fees must be paid. All the relevant information can be found in the XCM Transactor Fees section of the XCM Fees page.
Relevant XCM Definitions¶
- Sovereign account — an account each chain in the ecosystem has, one for the relay chain and the other for other parachains. It is calculated as the
blake2
hash of a specific word and parachain ID concatenated (blake2(para+ParachainID)
for the Sovereign account in the relay chain, andblake2(sibl+ParachainID)
for the Sovereign account in other parachains), truncating the hash to the correct length. The account is owned by root and can only be used through SUDO (if available) or governance (referenda). The Sovereign account typically signs XCM messages in other chains in the ecosystem -
Multilocation — a way to specify a point in the entire relay chain/parachain ecosystem relative to a given origin. For example, it can be used to specify a specific parachain, asset, account, or even a pallet inside a parachain. In general terms, a multilocation is defined with a
parents
and aninterior
:parents
- refers to how many "hops" into a parent blockchain you need to take from a given origininterior
- refers to how many fields you need to define the target point.
For example, to target a parachain with ID
1000
from another parachain, the multilocation would be{ "parents": 1, "interior": { "X1": [{ "Parachain": 1000 }]}}
- Multilocation-derivative account — an account derivated from the new origin set by the Descend Origin XCM instruction and the provided multilocation, which is typically the sovereign account from which the XCM originated. Derivative accounts are keyless (the private key is unknown). Consequently, derivative accounts related to XCM-specific use cases can only be accessed through XCM extrinsics. For Moonbeam-based networks, the derivation method is calculating theblake2
hash of the multilocation, which includes the origin parachain ID, and truncating the hash to the correct length (20 bytes for an Ethereum-styled account). The XCM call origin conversion happens when theTransact
instruction gets executed. Consequently, each parachain can convert the origin with its own desired procedure, so the user who initiated the transaction might have a different derivative account per parachain. This derivative account pays for transaction fees, and it is set as the dispatcher of the call - Transact information — relates to extra weight and fee information for the XCM remote execution part of the XCM Transactor extrinsic. This is needed because the XCM transaction fee is paid by the sovereign account. Therefore, XCM Transactor calculates what this fee is and charges the sender of the XCM Transactor extrinsic the estimated amount in the corresponding XC-20 token to repay the sovereign account
XCM Transactor Pallet Interface¶
Extrinsics¶
The XCM Transactor Pallet provides the following extrinsics (functions):
- hrmpManage(action, fee, weightInfo) - manages HRMP operations related to opening, accepting, and closing an HRMP channel. The given action can be any of these four actions:
InitOpen
,Accept
,Close
, andCancel
- removeFeePerSecond(assetLocation) — remove the fee per second information for a given asset in its reserve chain. The asset is defined as a multilocation
- removeTransactInfo(location) — remove the transact information for a given chain, defined as a multilocation
- setFeePerSecond(assetLocation, feePerSecond) — sets the fee per second information for a given asset on its reserve chain. The asset is defined as a multilocation. The
feePerSecond
is the token units per second of XCM execution that will be charged to the sender of the XCM Transactor extrinsic - setTransactInfo(location, transactExtraWeight, maxWeight) — sets the transact information for a given chain, defined as a multilocation. The transact information includes:
- transactExtraWeight — weight to cover execution fees of the XCM instructions (
WithdrawAsset
,BuyExecution
, andTransact
), which is estimated to be at least 10% over what the remote XCM instructions execution uses - maxWeight — maximum weight units allowed for the remote XCM execution
- transactExtraWeightSigned — (optional) weight to cover execution fees of the XCM instructions (
DescendOrigin
,WithdrawAsset
,BuyExecution
, andTransact
), which is estimated to be at least 10% over what the remote XCM instructions execution uses
- transactExtraWeight — weight to cover execution fees of the XCM instructions (
- transactThroughSigned(destination, fee, call, weightInfo) — sends an XCM message with instructions to remotely execute a given call in the given destination. The remote call will be signed and executed by a new account that the destination parachain must derivate. For Moonbeam-based networks, this account is the
blake2
hash of the descended multilocation, truncated to the correct length. The XCM Transactor Pallet calculates the fees for the remote execution and charges the sender of the extrinsic the estimated amount in the corresponding XC-20 token given by the asset ID - transactThroughSovereign(destination, feePayer, fee, call, originKind, weightInfo) — sends an XCM message with instructions to remotely execute a given call in the given destination. The remote call will be signed by the origin parachain sovereign account (who pays the fees), but the transaction is dispatched from a given origin. The XCM Transactor Pallet calculates the fees for the remote execution and charges the given account the estimated amount in the corresponding XC-20 token given by the asset multilocation
Where the inputs that need to be provided can be defined as:
- assetLocation — a multilocation representing an asset on its reserve chain. The value is used to set or retrieve the fee per second information
- location — a multilocation representing a chain in the ecosystem. The value is used to set or retrieve the transact information
- destination — a multilocation representing a chain in the ecosystem where the XCM message is being sent to
- fee — an enum that provides developers two options on how to define the XCM execution fee item. Both options rely on the
feeAmount
, which is the units of the asset per second of XCM execution you provide to execute the XCM message you are sending. The two different ways to set the fee item are:- AsCurrencyID — is the ID of the currency being used to pay for the remote call execution. Different runtimes have different ways of defining the IDs. In the case of Moonbeam-based networks,
SelfReserve
refers to the native token,ForeignAsset
refers to the asset ID of an external XC-20 (not to be confused with the XC-20 address), andErc20
refers to the contract address of a local XC-20 - AsMultiLocation — is the multilocation that represents the asset to be used for fee payment when executing the XCM
- AsCurrencyID — is the ID of the currency being used to pay for the remote call execution. Different runtimes have different ways of defining the IDs. In the case of Moonbeam-based networks,
- innerCall — encoded call data of the call that will be executed in the destination chain. This is wrapped with the
asDerivative
option if transacting through the sovereign derivative account - weightInfo — a structure that contains all the weight related information. If not enough weight is provided, the execution of the XCM will fail, and funds might get locked in either the sovereign account or a special pallet. Consequently, it is essential to correctly set the destination weight to avoid failed XCM executions. The structure contains two fields:
- transactRequiredWeightAtMost — weight related to the execution of the
Transact
call itself. For transacts through sovereign-derivative, you have to take into account the weight of theasDerivative
extrinsic as well. However, this does not include the cost (in weight) of all the XCM instructions - overallWeight — the total weight the XCM Transactor extrinsic can use. This includes all the XCM instructions plus the weight of the call itself (transactRequiredWeightAtMost)
- transactRequiredWeightAtMost — weight related to the execution of the
- call — similar to
innerCall
, but it is not wrapped with theasDerivative
extrinsic - feePayer — the address that will pay for the remote XCM execution in the transact through sovereign extrinsic. The fee is charged in the corresponding XC-20 token
- originKind — dispatcher of the remote call in the destination chain. There are four types of dispatchers available
Storage Methods¶
The XCM Transactor Pallet includes the following read-only storage method:
- destinationAssetFeePerSecond() - returns the fee per second for an asset given a multilocation. This enables the conversion from weight to fee. The storage element is read by the pallet extrinsics if
feeAmount
is set toNone
- palletVersion() — returns current pallet version from storage
- transactInfoWithWeightLimit(location) — returns the transact information for a given multilocation. The storage element is read by the pallet extrinsics if
feeAmount
is set toNone
Pallet Constants¶
The XCM Transactor Pallet includes the following read-only functions to obtain pallet constants:
- baseXcmWeight() - returns the base XCM weight required for execution, per XCM instruction
- selfLocation() - returns the multilocation of the chain
XCM Transactor Transact Through Signed¶
This section covers building an XCM message for remote executions using the XCM Transactor Pallet, specifically with the transactThroughSigned
function. However, you'll not be able to follow along as the destination parachain is not publicly available.
Note
You need to ensure that the call you are going to execute remotely is allowed in the destination chain!
Checking Prerequisites¶
To be able to send the extrinsics in this section, you need to have:
- An account in the origin chain with funds
- Funds in the multilocation-derivative account on the target chain. You can calculate this address by using the
calculate-multilocation-derivative-account.ts
script
For this example, the following accounts will be used:
- Alice's account in the origin parachain with address
0x44236223aB4291b93EEd10E4B511B37a398DEE55
- Its multilocation-derivative address in the target parachain is
0x5c27c4bb7047083420eddff9cddac4a0a120b45c
Building the XCM¶
Since you'll be interacting with the transactThroughSigned
function of the XCM Transactor Pallet, you'll need to assemble the dest
, fee
, call
, and weightInfo
parameters. To do so, you can take the following steps:
-
Define the destination multilocation, which will target parachain 888:
const dest = { V3: { parents: 1, interior: { X1: { Parachain: 888 } }, }, };
-
Define the
fee
information, which will require you to:- Define the currency ID and provide the asset details
- Set the fee amount
const fee = { currency: { AsCurrencyId: { ForeignAsset: 35487752324713722007834302681851459189n }, }, feeAmount: 50000000000000000n, };
const fee = { currency: { AsCurrencyId: { Erc20: { contractAddress: ERC_20_ADDRESS} }, }, feeAmount: 50000000000000000n, };
-
Define the
call
that will be executed in the destination chain. This is the encoded call data of the pallet, method, and input values to be called. It can be constructed in Polkadot.js Apps (must be connected to the destination chain) or using the Polkadot.js API. For this example, the inner call is a simple balance transfer of 1 token of the destination chain to Alice's account there:const call = '0x030044236223ab4291b93eed10e4b511b37a398dee5513000064a7b3b6e00d';
-
Set the
weightInfo
, which includes the requiredtransactRequiredWeightAtMost
weight and the optionaloverallWeight
parameters. Both weight parameters require you to specifyrefTime
andproofSize
, whererefTime
is the amount of computational time that can be used for execution andproofSize
is the amount of storage in bytes that can be used. For each parameter, you can follow these guidelines:- For
transactRequiredAtMost
, the value must include theasDerivative
extrinsic as well. However, this does not include the weight of the XCM instructions. For this example, setrefTime
to1000000000
weight units andproofSize
to0
- For
overallWeight
, the value must be the total of transactRequiredWeightAtMost plus the weight needed to cover the XCM instructions execution costs in the destination chain. If you do not provide this value, the pallet will use the element in storage (if exists), and add it to transactRequiredWeightAtMost. For this example, setrefTime
to2000000000
weight units andproofSize
to0
const weightInfo = { transactRequiredWeightAtMost: { refTime: 1000000000n, proofSize: 0 }, overallWeight: { refTime: 2000000000n, proofSize: 0 }, };
- For
Now that you have the values for each of the parameters, you can write the script for the transaction. You'll take the following steps:
- Provide the input data for the call. This includes:
- The Moonbase Alpha endpoint URL to create the provider
- The values for each of the parameters of the
transactThroughSigned
function
- Create a Keyring instance that will be used to send the transaction
- Create the Polkadot.js API provider
- Craft the
xcmTransactor.transactThroughSigned
extrinsic with thedest
,fee
,call
andweightInfo
values - Send the transaction using the
signAndSend
extrinsic and the Keyring instance you created in the second step
Remember
This is for demo purposes only. Never store your private key in a JavaScript file.
import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; // Version 9.13.6
// 1. Provide input data
const providerWsURL = 'wss://wss.api.moonbase.moonbeam.network';
const privateKey = 'INSERT_PRIVATE_KEY';
const dest = {
V3: {
parents: 1,
interior: { X1: { Parachain: 888 } },
},
};
const fee = {
currency: {
AsCurrencyId: { ForeignAsset: 35487752324713722007834302681851459189n },
},
feeAmount: 50000000000000000n,
};
const call = '0x030044236223ab4291b93eed10e4b511b37a398dee5513000064a7b3b6e00d';
const weightInfo = {
transactRequiredWeightAtMost: { refTime: 1000000000n, proofSize: 0 },
overallWeight: { refTime: 2000000000n, proofSize: 0 },
};
// 2. Create Keyring instance
const keyring = new Keyring({ type: 'ethereum' });
const alice = keyring.addFromUri(privateKey);
const transactThroughSigned = async () => {
// 3. Create Substrate API provider
const substrateProvider = new WsProvider(providerWsURL);
const api = await ApiPromise.create({ provider: substrateProvider });
// 4. Craft the extrinsic
const tx = api.tx.xcmTransactor.transactThroughSigned(
dest,
fee,
call,
weightInfo
);
// 5. Send the transaction
const txHash = await tx.signAndSend(alice);
console.log(`Submitted with hash ${txHash}`);
api.disconnect();
};
transactThroughSigned();
Note
You can view an example of the above script, which sends 1 xcUNIT to Alice's account on the relay chain, on Polkadot.js Apps using the following encoded calldata: 0x210603010100e10d00017576e5e612ff054915d426c546b1b21a010000c52ebca2b10000000000000000007c030044236223ab4291b93eed10e4b511b37a398dee5513000064a7b3b6e00d02286bee0001030094357700
.
Once the transaction is processed, Alice should've received one token in her address on the destination chain.
XCM Transactor Precompile¶
The XCM Transactor Precompile contract allows developers to access the XCM Transactor Pallet features through the Ethereum API of Moonbeam-based networks. Similar to other precompile contracts, the XCM Transactor Precompile is located at the following addresses:
0x000000000000000000000000000000000000080d
0x000000000000000000000000000000000000080d
0x000000000000000000000000000000000000080d
The XCM Transactor Legacy Precompile is still available on all Moonbeam-based networks. However, the legacy version will be deprecated in the near future, so all implementations must migrate to the newer interface. The XCM Transactor Legacy Precompile is located at the following addresses:
0x0000000000000000000000000000000000000806
0x0000000000000000000000000000000000000806
0x0000000000000000000000000000000000000806
Note
There can be some unintended consequences when using the precompiled contracts on Moonbeam. Please refer to the Security Considerations page for more information.
The XCM Transactor Solidity Interface¶
XcmTransactor.sol is an interface through which developers can interact with the XCM Transactor Pallet using the Ethereum API.
Note
The legacy version of the XCM Transactor Precompile will be deprecated in the near future, so all implementations must migrate to the newer interface.
The interface includes the following functions:
- indexToAccount(uint16 index) — read-only function that returns the registered address authorized to operate using a derivative account of the Moonbeam-based network sovereign account for the given index
- transactInfoWithSigned(Multilocation memory multilocation) — read-only function that, for a given chain defined as a multilocation, returns the transact information considering the three XCM instructions associated with the external call execution (
transactExtraWeight
). It also returns extra weight information associated with theDescendOrigin
XCM instruction for the transact through signed extrinsic (transactExtraWeightSigned
) - feePerSecond(Multilocation memory multilocation) — read-only function that, for a given asset as a multilocation, returns units of token per second of the XCM execution that is charged as the XCM execution fee. This is useful when, for a given chain, there are multiple assets that can be used for fee payment
- transactThroughSignedMultilocation(Multilocation memory dest, Multilocation memory feeLocation, uint64 transactRequiredWeightAtMost, bytes memory call, uint256 feeAmount, uint64 overallWeight) — function that represents the
transactThroughSigned
method described in the previous example, setting the fee type to AsMultiLocation. You need to provide the asset multilocation of the token that is used for fee payment instead of the XC-20 tokenaddress
- transactThroughSigned(Multilocation memory dest, address feeLocationAddress, uint64 transactRequiredWeightAtMost, bytes memory call, uint256 feeAmount, uint64 overallWeight) — function that represents the
transactThroughSigned
method described in the previous example, setting the fee type to AsCurrencyId. Instead of the asset ID, you'll need to provide the asset XC-20 address of the token that is used for fee payment - encodeUtilityAsDerivative(uint8 transactor, uint16 index, bytes memory innerCall) - encodes an
asDerivative
wrapped call given the transactor to be used, the index of the derivative account, and the inner call to be executed from the derivated address
Building the Precompile Multilocation¶
In the XCM Transactor Precompile interface, the Multilocation
structure is defined as follows:
struct Multilocation {
uint8 parents;
bytes[] interior;
}
Note that each multilocation has a parents
element, defined in this case by a uint8
, and an array of bytes. Parents refer to how many "hops" in the upwards direction you have to do if you are going through the relay chain. Being a uint8
, the normal values you would see are:
Origin | Destination | Parents Value |
---|---|---|
Parachain A | Parachain A | 0 |
Parachain A | Relay Chain | 1 |
Parachain A | Parachain B | 1 |
The bytes array (bytes[]
) defines the interior and its content within the multilocation. The size of the array defines the interior
value as follows:
Array | Size | Interior Value |
---|---|---|
[] | 0 | Here |
[XYZ] | 1 | X1 |
[XYZ, ABC] | 2 | X2 |
[XYZ, ... N] | N | XN |
Note
Interior value Here
is often used for the relay chain (either as a destination or to target the relay chain asset).
Suppose the bytes array contains data. Each element's first byte (2 hexadecimal numbers) corresponds to the selector of that XN
field. For example:
Byte Value | Selector | Data Type |
---|---|---|
0x00 | Parachain | bytes4 |
0x01 | AccountId32 | bytes32 |
0x02 | AccountIndex64 | u64 |
0x03 | AccountKey20 | bytes20 |
0x04 | PalletInstance | byte |
0x05 | GeneralIndex | u128 |
0x06 | GeneralKey | bytes[] |
Next, depending on the selector and its data type, the following bytes correspond to the actual data being provided. Note that for AccountId32
, AccountIndex64
, and AccountKey20
, the network
field seen in the Polkadot.js Apps example is appended at the end. For example:
Selector | Data Value | Represents |
---|---|---|
Parachain | "0x00+000007E7" | Parachain ID 2023 |
AccountId32 | "0x01+AccountId32+00" | AccountId32, Network(Option) Null |
AccountId32 | "0x01+AccountId32+03" | AccountId32, Network Polkadot |
AccountKey20 | "0x03+AccountKey20+00" | AccountKey20, Network(Option) Null |
PalletInstance | "0x04+03" | Pallet Instance 3 |
Note
The interior
data usually needs to be wrapped around quotes. On the contrary, you might get an invalid tuple value
error.
The following code snippet goes through some examples of Multilocation
structures, as they would need to be fed into the XCM Transactor Precompile functions:
// Multilocation targeting the relay chain asset from a parachain
{
1, // parents = 1
[] // interior = here
}
// Multilocation targeting Moonbase Alpha DEV token from another parachain
{
1, // parents = 1
// Size of array is 2, meaning is an X2 interior
[
"0x00000003E8", // Selector Parachain, ID = 1000 (Moonbase Alpha)
"0x0403" // Pallet Instance = 3
]
}
// Multilocation targeting aUSD asset on Acala
{
1, // parents = 1
// Size of array is 1, meaning is an X1 interior
[
"0x00000007D0", // Selector Parachain, ID = 2000 (Acala)
"0x060001" // General Key Selector + Asset Key
]
}
| Created: May 25, 2022