Skip to content

Using the X-Tokens Pallet To Send XC-20s

X-Tokens Precompile Contracts Banner

Introduction

Building an XCM message for fungible asset transfers is not an easy task. Consequently, there are wrapper functions/pallets that developers can leverage to use XCM features on Polkadot/Kusama.

One example of such wrappers is the x-tokens pallet, which provides different methods to transfer fungible assets via XCM.

This guide will show you how to leverage the x-tokens pallet to send XC-20s from a Moonbeam-based network to other chains in the ecosystem (relay chain/parachains). Moreover, you'll also learn how to use the x-tokens precompile to perform the same actions via the Ethereum API.

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.

Relevant XCM Definitions

  • XCM — stands for cross-consensus message. It is a general way for consensus systems to communicate with each other
  • VMP — stands for vertical message passing, one of the transport methods for XCMs. It allows parachains to exchange messages with the relay chain. UMP (upward message passing) enables parachains to send messages to their relay chain, while DMP (downward message passing) enables the relay chain to pass messages down to one of their parachains
  • XCMP — stands for cross-consensus message passing, one of the transport methods for XCMs. It allows parachains to exchange messages with other parachains on the same relay chain
  • HRMP — stands for horizontal relay-routed message passing, a stop-gap protocol while a full XCMP implementation is launched. It has the same interface as XCMP, but messages are stored on the relay chain
  • 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, and blake2(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 democracy (technical committee or 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 an interior. Parents refers to how many "hops" into a parent blockchain you need to take from a given origin. The interior 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 }]}}

X-Tokens Pallet Interface

Extrinsics

The x-tokens pallet provides the following extrinsics (functions):

  • transfer(currencyId, amount, dest, destWeight) — transfer a currency, defined as either the native token (self reserved), or with the asset ID
  • transferMultiasset(asset, dest, destWeight) — transfer a fungible asset, defined by its multilocation
  • transferMultiassetWithFee(asset, fee, dest, destWeight) — transfer a fungible asset, but it allows the sender to pay the fee with a different asset. Both are defined by their multilocation
  • transferMultiassets(assets, feeItem, dest, destWeight) — transfer several fungible assets, specifying which is used as the fee. Each asset is defined by its multilocation
  • transferMulticurrencies(currencies, feeItem, dest, destWeight) — transfer different curriencies, specifying which is used as the fee. Each currency is defined as either the native token (self reserved) or with the asset ID
  • transferWithFee(currencyId, amount, fee, dest, destWeight) — transfer a currency, but it allows the sender to pay the fee with a different asset. Both are defined by their multilocation

Where the inputs that need to be provided can be defined as:

  • currencyId/currencies — the ID/IDs of the currency/currencies being sent via XCM. Different runtimes have different ways to define the IDs. In the case of Moonbeam-based networks, SelfReserve refers to the native token, and ForeignAsset refers to the asset ID of the XC-20 (not to be confused with the XC-20 address)
  • amount — the number of tokens that are going to be sent via XCM
  • dest — a multilocation to define the destination address for the tokens being sent via XCM. It supports different address formats such as 20 or 32 bytes addresses (Ethereum or Substrate)
  • destWeight — the maximum amount of execution time you want to provide in the destination chain to execute the XCM message being sent. 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. It is important to correctly set the destination weight to avoid failed XCM executions
  • asset/assets — a multilocation to define the asset/assets being sent via XCM. Each parachain has a different way to reference assets. For example, Moonbeam-based networks reference their native tokens with the pallet balances index
  • fee — a multilocation to define the asset used to pay for the XCM execution in the target chain
  • feeItem — an index to define the asset position of an array of assets being sent, used to pay for the XCM execution in the target chain. For example, if only one asset is being sent, the feeItem would be 0

Storage Methods

The x-tokens pallet includes the following read-only storage method:

  • palletVersion() - provides the version of the x-tokens pallet being used

Pallet Constants

The x-tokens pallet includes the following read-only functions to obtain pallet constants:

  • baseXcmWeight() - returns the base XCM weight required for execution
  • selfLocation() - returns the multilocation of the chain

Building an XCM Message with the X-Tokens Pallet

This guide covers the process of building an XCM message using the x-tokens pallet, more specifically, with the transfer and transferMultiasset functions. Nevertheless, these two cases can be extrapolated to the other functions, especially once familiar with multilocations.

Note

Each parachain can allow/forbid specific methods from a pallet. Consequently, developers must ensure to use methods that are allowed. On the contrary, the transaction will fail with an error similar to system.CallFiltered.

You'll be transfering xcUNIT tokens, which is the XC-20 representation of the Alphanet relay chain token UNIT. An xcUNIT is an external XC-20. You can adapt this guide for another external XC-20 or a mintable XC-20.

Checking Prerequisites

To be able to send the extrinsics in Polkadot.js Apps, you need to have the following:

Moonbeam Swap xcUNITs

To check your xcUNIT balance, you can add the XC-20 to MetaMask with the following address:

0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080

If you're interested in how the precompile address is calculated, you can check out the following guides:

X-Tokens Transfer Function

In this example, you'll build an XCM message to transfer xcUNIT from Moonbase Alpha back to its relay chain through the transfer function of the x-tokens pallet.

Head to the extrinsic page of Polkadot.js Apps and set the following options (you can adapt for mintable XC-20s):

  1. Select the account from which you want to send the XCM
  2. Choose the xTokens pallet
  3. Choose the transfer extrinsic
  4. Set the currency ID to ForeignAsset for external XC-20s or LocalAssetReserve for mintable XC-20s. Since xcUNIT is an external XC-20, you can choose ForeignAsset
  5. Enter the asset ID. For this example, xcUNIT has an asset id of 42259045809535163221576417993425387648
  6. Set the number of tokens to send. For this example, you are sending 1 xcUNIT, but you have to account for the 12 decimals of xcUNIT
  7. To define the XCM destination multilocation, you have to target an account in the relay chain from Moonbase Alpha as the origin. Therefore, set the following parameters:

    Parameter Value
    Version V1
    Parents 1
    Interior X1
    X1 AccountId32
    Network Any
    Id Target Account
  8. Set the destination weight to 1000000000. Note that on Moonbase Alpha, each XCM instruction costs around 100000000 weight units. A transfer consists of 4 XCM instructions, so a destination weight of 400000000 should be enough

  9. Click the Submit Transaction button and sign the transaction

Note

The encoded call data for the extrinsict configured above is 0x1e00018080778c30c20fa2ebc0ed18d2cbca1f0010a5d4e800000000000000000000000101010100c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300ca9a3b00000000. It also includes a specific recipient that you'll need to change.

XCM X-Tokens Transfer Extrinsic

Once the transaction is processed, the TargetAccount should have received the transferred amount minus a small fee that is deducted to execute the XCM on the destination chain. On Polkadot.js Apps, can check the relevant extrinsics and events in Moonbase Alpha and the relay chain.

X-Tokens Transfer MultiAsset Function

In this example, you'll build an XCM message to transfer xcUNIT from Moonbase Alpha back to its relay chain using the transferMultiasset function of the x-tokens pallet.

Head to the extrinsic page of Polkadot.js Apps and set the following options:

  1. Select the account from which you want to send the XCM
  2. Choose the xTokens pallet
  3. Choose the transferMultiasset extrinsic
  4. To define the XCM asset multilocation, you have to target UNIT in the relay chain from Moonbase Alpha as the origin. Each chain sees its own asset differently. Therefore, you will have to set a different asset multilocation for each destination. For this example, the relay chain token can be defined as:

    Parameter Value
    Version V1
    Parents 1
    Interior Here

    If you're adapting this guide for a mintable XC-20, you have to to specify the pallet where the asset was created and the asset ID. Therefore, you would set the following parameters:

    Parameter Value
    Version V1
    Parents 1
    Interior X2
    PalletInstance 36
    GeneralIndex Asset ID
  5. Set the asset type as Fungible

  6. Set the number of tokens to send. For this example, you are sending 1 xcUNIT, but you have to account for the 12 decimals
  7. To define the XCM destination multilocation, you have to target an account in the relay chain from Moonbase Alpha as the origin. Therefore, set the following parameters:

    Parameter Value
    Version V1
    Parents 1
    Interior X1
    X1 AccountId32
    Network Any
    Id Target Account
  8. Set the destination weight to 1000000000. Note that on Moonbase Alpha, each XCM instruction costs around 100000000 weight units. A transferMultiasset consists of 4 XCM instructions, so a destination weight of 400000000 should be enough

  9. Click the Submit Transaction button and sign the transaction

Note

The encoded call data for the extrinsict configured above is 0x1e010100010000070010a5d4e80101010100c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300ca9a3b00000000. It also includes a specific recipient that you'll need to change.

XCM X-Tokens Transfer Extrinsic

Once the transaction is processed, the TargetAccount should have received the transferred amount minus a small fee that is deducted to execute the XCM on the destination chain. On Polkadot.js Apps, you can check the relevant extrinsics and events in Moonbase Alpha and the relay chain.

X-Tokens Precompile

The x-tokens precompile contract allows developers to access XCM token transfer features through the Ethereum API of Moonbeam-based networks. As with other precompile contracts, the x-tokens precompile is located at the following addresses:

0x0000000000000000000000000000000000000804
0x0000000000000000000000000000000000000804
0x0000000000000000000000000000000000000804

The X-Tokens Solidity Interface

Xtokens.sol is an interface through which developers can interact with the x-tokens pallet using the Ethereum API.

The interface includes the following functions:

  • transfer(address currencyAddress, uint256 amount, Multilocation memory destination, uint64 weight) — function that represents the transfer method described in the previous example. Instead of using the currency ID, you'll need to provide the assets precompile address for the currencyAddress:

    The destination multilocation is built in a particular way that is described in the following section

  • transferMultiasset(Multilocation memory asset, uint256 amount, Multilocation memory destination, uint64 weight) — function that represents the transferMultiasset method described in the previous example. Both multilocations are built in a particular way that is described in the following section

Building the Precompile Multilocation

In the x-tokens 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 Any
AccountKey20 "0x03+AccountKey20+00" AccountKey20, Network Any
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 x-tokens precompile functions:

// Multilocation targeting the relay chain or its 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 Alice's account on the Relay Chain from Moonbase Alpha
{
    1, // parents = 0
    // Size of array is 1, meaning is an X1 interior
    [
        "0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300" 
        // AccountKey32 Selector + Address in hex + Network = Any
    ]
}

Using Libraries to Interact with X-Tokens

The Multilocation structs can be formatted like any other struct when using libraries to interact with the Ethereum API. The following code snippet include the previous x-tokens transfer function, the x-tokens multiasset transfer function, and sample Multilocation struct examples. You can find the x-tokens ABI on Github.

import abi from './xtokensABI.js'; // Import the x-tokens ABI
import { ethers } from 'ethers'; // Import Ethers library
const PRIVATE_KEY = 'YOUR_PRIVATE_KEY_HERE';

// Create Ethers wallet & contract instance
const provider = new ethers.providers.JsonRpcProvider('https://rpc.api.moonbase.moonbeam.network');
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const xTokens = new ethers.Contract(
    '0x0000000000000000000000000000000000000804', 
    abi, 
    signer
);

// xcUNIT address in Moonbase Alpha
const xcUNIT_ADDRESS = '0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080';

// Multilocation targeting Alice's account on the relay chain from Moonbase Alpha
const ALICE_RELAY_ACC = [1, ['0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300']];

// Sends 1 xcUNIT to the relay chain using the transfer function
async function transferToAlice() {
    // Creates, signs, and sends the transfer transaction
    const transaction = await xTokens.transfer(
        xcUNIT_ADDRESS,         // Asset
        '1000000000000',        // Amount
        ALICE_RELAY_ACC,        // Destination
        '1000000000'            // Weight
    );

    // Waits for the transaction to be included in a block
    await transaction.wait();
    console.log(transaction);
}

// Multilocation targeting the relay chain or its asset from a parachain
const RELAY_CHAIN_ASSET = [1, []];

// Sends 1 xcUNIT to the relay chain using the transferMultiasset function
async function transferMultiassetToAlice() {
    const transaction = await xTokens.transferMultiasset(
        RELAY_CHAIN_ASSET,      // Asset
        '1000000000000',        // Amount
        ALICE_RELAY_ACC,        // Destination
        '1000000000'            // Weight
    );
    await transaction.wait();
    console.log(transaction);}

transferToAlice();
transferMultiassetToAlice();

// Here are some additional multilocations for the Asset multilocation:
const LOCAL_ASSET = [0, ["0x0424", "0x05FD9D0BF45A2947A519A741C4B9E99EB6"]]; // Note that 0x0424 indicates the x-tokens pallet
const DEV_FROM_OTHER_PARACHAIN = [1, ["0x00000003E8", "0x0403"]]; // Use if you were targeting DEV from a non-Moonbeam network

// Here are some additional multilocations for the Destination multilocation:
const ADDRESS32_OF_PARACHAIN = [1, ["0x00000007EF", "0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300"]];
const ADDRESS20_FROM_PARACHAIN_TO_MOONBASE = [1, ["0x00000003E8", "0x03f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac00"]];
import abi from './xtokensABI.js'; // Import the x-tokens ABI
import Web3 from 'web3'; // Import Web3 library
const PRIVATE_KEY = 'YOUR_PRIVATE_KEY_HERE';

// Create Web3 wallet & contract instance
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network'); // Change to network of choice
const xTokens = new web3.eth.Contract(
  abi,
  '0x0000000000000000000000000000000000000804',
  { from: web3.eth.accounts.privateKeyToAccount(PRIVATE_KEY).address } // 'from' is necessary for gas estimation
);

// xcUNIT address in Moonbase Alpha
const xcUNIT_ADDRESS = '0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080';

// Multilocation targeting Alice's account on the relay chain from Moonbase Alpha
const ALICE_RELAY_ACC = [1, ['0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300']];

// Sends 1 xcUNIT to the relay chain using the transfer function
async function transferToAlice() {
  // Create transaction
  const transferTx = xTokens.methods.transfer(
    xcUNIT_ADDRESS,         // Asset
    '1000000000000',        // Amount
    ALICE_RELAY_ACC,        // Destination
    '1000000000'            // Weight
  );

  // Sign transaction
  const signedTx = await web3.eth.accounts.signTransaction(
    {
      to: '0x0000000000000000000000000000000000000804',
      data: transferTx.encodeABI(),
      gas: await transferTx.estimateGas()
    },
    PRIVATE_KEY
  );

  // Send signed transaction
  const sendTx = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  console.log(sendTx);
}

// Multilocation targeting the relay chain or its asset from a parachain
const RELAY_CHAIN_ASSET = [1, []];

// Sends 1 xcUNIT to the relay chain using the transferMultiasset function
async function transferMultiassetToAlice() {
  const transferTx = xTokens.methods.transferMultiasset(
    RELAY_CHAIN_ASSET,      // Asset
    '1000000000000',        // Amount
    ALICE_RELAY_ACC,        // Destination
    '1000000000'            // Weight
  );
  const signedTx = await web3.eth.accounts.signTransaction(
    {
      to: '0x0000000000000000000000000000000000000804',
      data: transferTx.encodeABI(),
      gas: await transferTx.estimateGas()
    },
    PRIVATE_KEY
  );
  const sendTx = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  console.log(sendTx);
}

transferToAlice();
transferMultiassetToAlice();

// Here are some additional multilocations for the Asset multilocation:
const LOCAL_ASSET = [0, ["0x0424", "0x05FD9D0BF45A2947A519A741C4B9E99EB6"]]; // Note that 0x0424 indicates the x-tokens pallet
const DEV_FROM_OTHER_PARACHAIN = [1, ["0x00000003E8", "0x0403"]]; // Use if you were targeting DEV from a non-Moonbeam network

// Here are some additional multilocations for the Destination multilocation:
const ADDRESS32_OF_PARACHAIN = [1, ["0x00000007EF", "0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300"]];
const ADDRESS20_FROM_PARACHAIN_TO_MOONBASE = [1, ["0x00000003E8", "0x03f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac00"]];
from web3 import Web3

abi = 'XTOKENS_ABI_HERE' # Paste or import the x-tokens ABI
privateKey = 'YOUR_PRIVATE_KEY_HERE' # This is for demo purposes, never store your private key in plain text
address = 'YOUR_ADDRESS_HERE' # The wallet address that corresponds to your private key

# Create Web3 wallet & contract instance
web3 = Web3(Web3.HTTPProvider('https://rpc.api.moonbase.moonbeam.network'))
xTokens = web3.eth.contract(
    address='0x0000000000000000000000000000000000000804',
    abi=abi
)

# xcUNIT address in Moonbase Alpha
xcUNIT_ADDRESS = '0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080'

# Multilocation targeting Alice's account on the relay chain from Moonbase Alpha
ALICE_RELAY_ACC = [1, ['0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300']]

# Sends 1 xcUNIT to the relay chain using the transfer function
def transferToAlice():
    # Create transaction
    transferTx = xTokens.functions.transfer(
        xcUNIT_ADDRESS,         # Asset
        1000000000000,          # Amount
        ALICE_RELAY_ACC,        # Destination
        1000000000              # Weight
    ).buildTransaction(
        {
            'from': address,
            'nonce': web3.eth.get_transaction_count(address),
        }
    )

    # Sign transaction
    signedTx = web3.eth.account.sign_transaction(transferTx, privateKey)

    # 7. Send tx and wait for receipt
    hash = web3.eth.send_raw_transaction(signedTx.rawTransaction)
    receipt = web3.eth.wait_for_transaction_receipt(hash)
    print(f'Tx successful with hash: { receipt.transactionHash.hex() }')

# Multilocation targeting the relay chain or its asset from a parachain
RELAY_CHAIN_ASSET = [1, []];

# Sends 1 xcUNIT to the relay chain using the transferMultiasset function
def transferMultiassetToAlice():
    transferTx = xTokens.functions.transferMultiasset(
        RELAY_CHAIN_ASSET,      # Asset
        1000000000000,          # Amount
        ALICE_RELAY_ACC,        # Destination
        1000000000              # Weight
    ).buildTransaction(
        {
            'from': address,
            'nonce': web3.eth.get_transaction_count(address),
        }
    )
    signedTx = web3.eth.account.sign_transaction(transferTx, privateKey)
    hash = web3.eth.send_raw_transaction(signedTx.rawTransaction)
    receipt = web3.eth.wait_for_transaction_receipt(hash)
    print(f'Tx successful with hash: { receipt.transactionHash.hex() }')

transferToAlice()
transferMultiassetToAlice()

# Here are some additional multilocations for the Asset multilocation:
LOCAL_ASSET = [0, ["0x0424", "0x05FD9D0BF45A2947A519A741C4B9E99EB6"]] # Note that 0x0424 indicates the x-tokens pallet
DEV_FROM_OTHER_PARACHAIN = [1, ["0x00000003E8", "0x0403"]] # Use if you were targeting DEV from a non-Moonbeam network

# Here are some additional multilocations for the Destination multilocation:
ADDRESS32_OF_PARACHAIN = [1, ["0x00000007EF", "0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300"]]
ADDRESS20_FROM_PARACHAIN_TO_MOONBASE = [1, ["0x00000003E8", "0x03f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac00"]]

Note

To test out the above examples on Moonbeam or Moonriver, you can replace the RPC URL with your own endpoint and API key which you can get from one of the supported Endpoint Providers.