Skip to content

How to Register Cross-Chain Assets

Introduction

For an asset to be transferred across chains via XCM, there needs to be an open channel between the two chains, and the asset needs to be registered on the destination chain. If a channel doesn't exist between the two chains, one will need to be opened. Please check out the XC Channel Registration guide for information on how to establish a channel between Moonbeam and another chain.

This guide will show you how to register external XC-20s on Moonbeam and provide the information you need to register Moonbeam assets, including Moonbeam native assets (GLMR, MOVR, and DEV) and local XC-20s (XCM-enabled ERC-20s), on another chain.

The examples in this guide use a CLI tool developed to ease the entire process, which you can find in the xcm-tools GitHub repository.

git clone https://github.com/Moonsong-Labs/xcm-tools && \
cd xcm-tools && \
yarn

Register External XC-20s on Moonbeam

Registering External XC-20s on Moonbeam is a multi-step process that, at a high level, involves proposing the asset registration on the Moonbeam Community Forum and creating an on-chain governance proposal.

If a channel between Moonbeam and the origin chain of the asset does not yet exist, one will need to be opened. You can batch the channel-related calls with the asset registration calls, so you only need to submit a single proposal. You'll need to start by creating a couple of forum posts: an XCM Disclosure post and an XCM Proposal post.

After you've collected feedback from community members, you can create a proposal to open a channel and register any assets. Please refer to the Establishing an XC Integration with Moonbeam guide for more information on opening a channel.

Asset registration if XC channel doesn't exist

If a channel between the chains already exists, you'll need to create a forum post to register the asset, collect feedback, and then submit the proposal to register the asset.

Asset registration if XC channel exists

Create a Forum Post

To create a forum post on the Moonbeam Community Forum, you'll need to make sure that you're adding the post to the correct category and adding relevant content. For general guidelines and a template to follow, please refer to the Moonbeam Community Forum Templates for XCM Integrations page.

Create a Proposal to Register an Asset

To register an asset native to another chain on Moonbeam, you'll need to submit a governance proposal that will call the assetManager.registerForeignAsset extrinsic. Additionally, you'll need to set the asset's units per second value through the assetManager.setAssetUnitsPerSecond extrinsic. The units per second value is the units of tokens to charge per second of execution time during XCM transfers. How to calculate the units per second will be covered in the following section.

To get started, you'll need to collect some information about the asset:

  • The ID of the parachain the asset lives on
  • The metadata of the asset. This includes:
  • The asset name
  • The asset symbol. You'll need to prepend "xc" to the asset symbol to indicate that the asset is an XCM-enabled asset
  • The number of decimals the asset has
  • The units per second
  • The multilocation of the asset as seen from Moonbeam

With this information in hand, you can get the encoded calldata for both calls and batch the calldata into a single transaction. From there, you can start the governance process, which includes using the calldata to submit a preimage and then using the preimage to create a proposal. If you're also opening a channel at the same time, you can add the channel-related calldata to the batch asset registration calldata and open a single proposal for everything. Asset and channel registration proposals on Moonbeam should be assigned to the General Admin Track.

Overview of the proposal process

Calculate the Asset's Units Per Second

Units per second is the number of tokens charged per second of execution of an XCM message. The target cost for an XCM transfer is $0.02 at the time of registration. The units per second might get updated through governance as the token price fluctuates.

The easiest way to calculate an asset's units per second is through the calculate-units-per-second.ts script in the xcm-tools repository. The script accepts the following arguments:

  • --decimals or --d - decimals of the tokens you are calculating the units per second for
  • --xcm-weight-cost or --xwc - total weight cost of the execution of the entire XCM message. The estimated weight per XCM operation on each Moonbeam chain is:

    800000000
    
    800000000
    
    638978000
    
  • --target or --t - (optional) target price for XCM execution, defaults to $0.02

  • --asset or --a - (optional) the token Coingecko API ID
  • --price or --p - (optional) if the Coingecko API does not support the token, you can specify the price manually

For example, to calculate the units per second of DOT (Polkadot token), which has 10 decimals, on Moonbeam, you can run:

yarn calculate-units-per-second --d 10 --a polkadot --xwc 800000000 

Which should result in the following output (at the time of writing):

Token Price is $7.33
The UnitsPerSecond needs to be set 34106412005

Generate the Encoded Calldata for the Asset Registration

If you're not familiar with the governance system on Moonbeam, you can find out more information on the Governance on Moonbeam page. With any governance proposal on Moonbeam, you'll need to submit a preimage, which defines the actions to be executed, and then use the preimage to submit a proposal.

To submit a preimage, you'll need to get the encoded calldata for each extrinsic that you want to execute. As previously mentioned, you'll use the assetManager.registerForeignAsset, and optionally, the assetManager.setAssetUnitsPerSecond, and system.setStorage extrinsics.

You can use the xcm-asset-registrator.ts script to calculate the encoded calldata and even submit the preimage and proposal if you desire. Proposals must be submitted via the General Admin Track. If you're registering an asset and opening a channel, you'll want to wait to submit the preimage and proposal until you have the calldata for the channel-related calls.

To get the encoded calldata for the assetManager.registerForeignAsset extrinsic, you can use the following arguments:

  • --ws-provider or --w - the WebSocket provider to be used for the requests. The WSS network endpoints for each Moonbeam-based network are as follows:

    wss://wss.api.moonbeam.network
    
    wss://wss.api.moonriver.moonbeam.network
    
    wss://wss.api.moonbase.moonbeam.network
    
  • --asset or --a - the multilocation of the asset

  • --name or --n - the name of the asset
  • --symbol or --sym - the symbol of the asset. Remember that "xc" should be prepended to the symbol to indicate the asset is an XCM-enabled asset
  • --decimals or --d - the number of decimals of the asset
  • --existential-deposit or --ed - (optional) - the existential deposit of the registered asset. This should always be set to 1
  • --sufficient or --suf - (optional) - the sufficiency, which dictates whether an asset can be sent to an account without a native token balance. This should always be set to true

To create a batch transaction that also sets the units per second or revert code of the asset's precompile in addition to the asset registration, you can choose to add these arguments:

  • --units-per-second or --u - (optional) - the units per second, which specifies the amount to charge per second of execution in the registered asset. You should have calculated this value in the previous section. If this is provided, the script will create a batch transaction for the governance proposal that, at a minimum, will register the asset and set the units per second on-chain
  • --revert-code or --revert - (optional) - registers the revert code for the asset's precompile in the EVM. If this is provided, the script will create a batch transaction for the governance proposal that, at a minimum, will register the asset and set the revert code.

    Note

    This flag is not necessary for proposals on Moonbeam as it includes a system.setStorage call that the OpenGov General Admin Origin can't execute. The dummy EVM bytecode can be set later with a call to the Precompile Registry precompile, which means that you don't need to worry about going through governance to set the revert code! Please check out the Set XC-20 Precompile Bytecode section to learn how to set the dummy bytecode.

As a practical example, the following command would generate the encoded calldata to register an asset from parachain 888 that has a general key of 1:

yarn register-asset -w wss://wss.api.moonbase.moonbeam.network \
--asset '{ "parents": 1, "interior": { "X2": [{ "Parachain": 888 }, {"GeneralKey": "0x000000000000000001"}]}}' \
--symbol "xcEXTN" --decimals 18 \
--name "Example Token" \
--units-per-second 20070165297881393351 \ 
--ed 1 --sufficient true

Its output would look like the following:

Encoded proposal for registerAsset is 0x1f0000010200e10d0624000000000000000001344578616d706c6520546f6b656e1878634558544e12000000000000000000000000000000000000
Encoded proposal for setAssetUnitsPerSecond is 0x1f0100010200e10d0624000000000000000001c7a8978b008d8716010000000000000026000000
Encoded calldata for tx is 0x0102081f0000010200e10d0624000000000000000001344578616d706c6520546f6b656e1878634558544e120000000000000000000000000000000000001f0100010200e10d0624000000000000000001c7a8978b008d8716010000000000000026000000

Programmatically Submit the Preimage and Proposal for Asset Registration

The script provides the option to programmatically submit a preimage and democracy proposal for the asset registration if you pass in the following optional arguments:

  • --account-priv-key or --account - (optional) - the private key of the account that will submit the preimage and proposal
  • --sudo or --x - (optional) - wraps the transaction with sudo.sudo. This can be used for Moonbase Alpha, if you want to provide the SCALE encoded calldata to the team so that it is submitted via SUDO
  • --send-preimage-hash or --h - (optional) - submits the preimage
  • --send-proposal-as or --s - (optional) - specifies how the proposal should be sent. The following options are accepted:
    • democracy - sends the proposal through regular democracy using Governance v1
    • council-external - sends the proposal as an external proposal that will be voted on by the council using Governance v1
    • v2 - sends the proposal through OpenGov (Governance v2). This option should be used for Moonbeam. If you choose this option, you'll also need to use the --track argument to specify which Track the proposal will go through and the --delay argument to specify the delay period (in blocks) after the proposal has passed and before the proposal is executed
  • --collectiveThreshold or --c - (optional) - the number of council votes that are needed to approve the proposal. Defaults to 1
  • --at-block - (optional) - the block number at which the call should get executed
  • --track - (optional) - the Track the proposal should go through for OpenGov proposals. For Moonbeam, the General Admin Origin should be used
  • --delay - (optional) - the delay period (in blocks) after a proposal has passed and before it can be executed. Defaults to 100 blocks

Altogether, you can use the following command to submit a preimage and proposal using OpenGov, which batches the asset registration and sets the asset's units per second.

yarn register-asset -w wss://wss.api.moonbeam.network  \
--asset 'INSERT_ASSET_MULTILOCATION' \
--symbol "INSERT_TOKEN_SYMBOL" \
--decimals INSERT_TOKEN_DECIMALS \
--name "INSERT_TOKEN_NAME" \
--units-per-second INSERT_UNITS_PER_SECOND \
--existential-deposit 1 \
--sufficient true \
--account-priv-key "0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133" \
--send-preimage-hash true \
--send-proposal-as v2
--track '{ "Origins": "GeneralAdmin" }'
yarn register-asset -w wss://wss.api.moonriver.moonbeam.network  \
--asset 'INSERT_ASSET_MULTILOCATION' \
--symbol "INSERT_TOKEN_SYMBOL" \
--decimals INSERT_TOKEN_DECIMALS \
--name "INSERT_TOKEN_NAME" \
--units-per-second INSERT_UNITS_PER_SECOND \
--existential-deposit 1 \
--sufficient true \
--account-priv-key "0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133" \
--send-preimage-hash true \
--send-proposal-as v2
--track '{ "Origins": "GeneralAdmin" }'

For Moonbase Alpha, you will not need to go through governance. Instead, you can use the --sudo flag and provide the output to the Moonbeam team so that the asset and channel can be added quickly through sudo.

You can see additional examples in the README.md of the xcm-tools repository.

Test the Asset Registration on Moonbeam

After your asset is registered, the team will provide the asset ID and the XC-20 precompile address.

Your XC-20 precompile address is calculated by converting the asset ID decimal number to hex and prepending it with F's until you get a 40 hex character (plus the “0x”) address. For more information on how it is calculated, please refer to the Calculate External XC-20 Precompile Addresses section of the External XC-20 guide.

After the asset is successfully registered, you can try transferring tokens from your parachain to the Moonbeam-based network you are integrating with.

Note

Remember that Moonbeam-based networks use AccountKey20 (Ethereum-style addresses).

For testing, please also provide your parachain WSS endpoint so that the Moonbeam dApp can connect to it. Lastly, please fund the corresponding account:

AccountId: 5E6kHM4zFdH5KEJE3YEzX5QuqoETVKUQadeY8LVmeh2HyHGt
Hex:       0x5a071f642798f89d68b050384132eea7b65db483b00dbb05548d3ce472cfef48
AccountId: 5E6kHM4zFdH5KEJE3YEzX5QuqoETVKUQadeY8LVmeh2HyHGt
Hex:       0x5a071f642798f89d68b050384132eea7b65db483b00dbb05548d3ce472cfef48
AccountId: 5GWpSdqkkKGZmdKQ9nkSF7TmHp6JWt28BMGQNuG4MXtSvq3e
Hex:       0xc4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a063

Note

For Moonbeam and Moonriver testing, please send $50 worth of tokens to the aforementioned account. In addition, provide an Ethereum-style account to send $50 worth of GLMR/MOVR for testing purposes.

XC-20s are Substrate-based assets with an ERC-20 interface. This means they can be added to MetaMask and composed with any EVM DApp that exists in the ecosystem. The team can connect you with any DApp you find relevant for an XC-20 integration.

If you need DEV tokens (the native token for Moonbase Alpha) to use your XC-20 asset, you can get some from the Moonbase Alpha Faucet, which dispenses 1.1 DEV tokens every 24 hours. If you need more, feel free to reach out to the team on Telegram or Discord.

Set XC-20 Precompile Bytecode

Once your XC-20 has been registered on Moonbeam, you can set the XC-20's precompile bytecode. This is necessary because precompiles are implemented inside the Moonbeam runtime and, by default, do not have bytecode. In Solidity, when a contract is called, there are checks that require the contract bytecode to be non-empty. So, setting the bytecode as a placeholder bypasses these checks and allows the precompile to be called.

You can use the Precompile Registry, which is a Solidity interface, to update the XC-20 precompile's bytecode to avoid any issues and ensure that the precompile is callable from Solidity. To do so, you'll use the Precompile Registry's updateAccountCode function.

To get started, you'll need to calculate your XC-20's precompile address and have the Precompile Registry's ABI.

Precompile Registry ABI
[
    {
        "inputs": [
            {
                "internalType": "address",
                "name": "a",
                "type": "address"
            }
        ],
        "name": "isActivePrecompile",
        "outputs": [
            {
                "internalType": "bool",
                "name": "",
                "type": "bool"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "address",
                "name": "a",
                "type": "address"
            }
        ],
        "name": "isPrecompile",
        "outputs": [
            {
                "internalType": "bool",
                "name": "",
                "type": "bool"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "address",
                "name": "a",
                "type": "address"
            }
        ],
        "name": "updateAccountCode",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
]

Then, you can use the following scripts to set the dummy code for your XC-20's precompile.

Remember

The following snippets are for demo purposes only. Never store your private keys in a JavaScript or Python file.

import { ethers } from 'ethers'; // Import Ethers library

const privateKey = 'INSERT_PRIVATE_KEY';
const abi = 'INSERT_PRECOMPILE_REGISTRY_ABI';
const xc20Address = 'INSERT_XC_20_PRECOMPILE_ADDRESS';
const registryAddress = '0x0000000000000000000000000000000000000815';

// Create Ethers provider and signer
const provider = new ethers.JsonRpcProvider(
  'https://rpc.api.moonbase.moonbeam.network'
);
const signer = new ethers.Wallet(privateKey, provider);

// Create interface for the Precompile Registry
const precompileRegistry = new ethers.Contract(registryAddress, abi, signer);

const updateAccountCode = async () => {
  // Update the precompile bytecode
  await precompileRegistry.updateAccountCode(xc20Address);

  // Check the precompile bytecode
  const bytecode = await provider.getCode(xc20Address);
  console.log(`The XC-20 precompile's bytecode is: ${bytecode}`);
};

updateAccountCode();
import { Web3 } from 'web3';

const privateKey = 'INSERT_PRIVATE_KEY';
const abi = 'INSERT_PRECOMPILE_REGISTRY_ABI';
const xc20Address = 'INSERT_XC_20_PRECOMPILE_ADDRESS';
const registryAddress = '0x0000000000000000000000000000000000000815';

// Create provider
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');

// Create interface for the Precompile Registry
const precompileRegistry = new web3.eth.Contract(abi, registryAddress, {
  from: web3.eth.accounts.privateKeyToAccount(privateKey).address,
});

const updateAccountCode = async () => {
  // Update the precompile bytecode
  await precompileRegistry.methods.updateAccountCode(xc20Address).call();

  // Check the precompile bytecode
  const bytecode = await web3.eth.getCode(xc20Address);
  console.log(`The XC-20 precompile's bytecode is: ${bytecode}`);
};

updateAccountCode();
from web3 import Web3

private_key = "INSERT_PRIVATE_KEY"
abi = "INSERT_PRECOMPILE_REGISTRY_ABI"  # Paste or import the Precompile Registry ABI
xc20_address = "INSERT_XC_20_PRECOMPILE_ADDRESS"
registry_address = "0x0000000000000000000000000000000000000815"

# Create provider
web3 = Web3(Web3.HTTPProvider("https://rpc.api.moonbase.moonbeam.network"))

# Create interface for the Precompile Registry
precompile_registry = web3.eth.contract(address=registry_address, abi=abi)


def update_account_code():
    # Update the precompile bytecode
    precompile_registry.functions.updateAccountCode(xc20_address).call()

    # Check the precompile bytecode
    bytecode = web3.eth.get_code(xc20_address)
    print("The XC-20 precompile's bytecode is: ", web3.to_hex(bytecode))


update_account_code()

After running the script to set the bytecode, you should see The XC-20 precompile's bytecode is: 0x60006000fd printed to your terminal.

Register Moonbeam Assets on Another Chain

In order to enable cross-chain transfers of Moonbeam assets, including Moonbeam native assets (GLMR, MOVR, DEV) and local XC-20s (XCM-enabled ERC-20s) deployed on Moonbeam, between Moonbeam and another chain, you'll need to register the assets on the other chain. Since each chain stores cross-chain assets differently, the exact steps to register Moonbeam assets on another chain will vary depending on the chain. At the very least, you'll need to know the metadata and the multilocation of the assets on Moonbeam.

There are additional steps aside from asset registration that will need to be taken to enable cross-chain integration with Moonbeam. For more information, please refer to the Establishing an XC Integration with Moonbeam guide.

Register Moonbeam Native Assets on Another Chain

The metadata for each network is as follows:

Variable Value
Name Glimmer
Symbol GLMR
Decimals 18
Existential deposit 1 (1 * 10^-18 GLMR)
Variable Value
Name Moonriver
Symbol MOVR
Decimals 18
Existential deposit 1 (1 * 10^-18 MOVR)
Variable Value
Name DEV
Symbol DEV
Decimals 18
Existential deposit 1 (1 * 10^-18 DEV)

The multilocation of Moonbeam native assets includes the parachain ID of the Moonbeam network and the pallet instance where Moonbeam assets live, which corresponds to the index of the Balances Pallet. The multilocation for each network is as follows:

{
  V4: {
    parents: 1,
    interior: {
      X2: [
        { 
          Parachain: 2004
        },
        {
          PalletInstance: 10
        }
      ]
    }
  }
}
{
  V4: {
    parents: 1,
    interior: {
      X2: [
        { 
          Parachain: 2023
        },
        {
          PalletInstance: 10
        }
      ]
    }
  }
}
{
  V4: {
    parents: 1,
    interior: {
      X2: [
        { 
          Parachain: 1000
        },
        {
          PalletInstance: 3
        }
      ]
    }
  }
}

Register Local XC-20s on Another Chain

The multilocation for local XC-20s include the parachain ID of Moonbeam, the pallet instance, and the address of the ERC-20. The pallet instance corresponds to the index of the ERC-20 XCM Bridge Pallet, as this is the pallet that enables any ERC-20 to be transferred via XCM.

To be registered on other chains, local XC-20s must strictly comply with the standard ERC-20 interface as described in EIP-20. In particular, the transfer function must be as described in EIP-20:

function transfer(address _to, uint256 _value) public returns (bool success)

If the function selector of the transfer function deviates from the standard, the cross-chain transfer will fail.

You can use the following multilocation to register a local XC-20:

{
  parents: 1,
  interior: {
    X3: [
      { 
        Parachain: 2004
      },
      {
        PalletInstance: 110
      },
      {
        AccountKey20: {
          key: 'INSERT_ERC20_ADDRESS'
        }
      }
    ]
  }
}
{
  parents: 1,
  interior: {
    X3: [
      { 
        Parachain: 2023
      },
      {
        PalletInstance: 110
      },
      {
        AccountKey20: {
          key: 'INSERT_ERC20_ADDRESS'
        }
      }
    ]
  }
}
{
  parents: 1,
  interior: {
    X3: [
      { 
        Parachain: 1000
      },
      {
        PalletInstance: 48
      },
      {
        AccountKey20: {
          key: 'INSERT_ERC20_ADDRESS'
        }
      }
    ]
  }
}

Since local XC-20s are ERC-20s on Moonbeam, there are no deposits required to create an ERC-20 on Moonbeam. There may, however, be deposits required to register the asset on another parachain. Please consult with the parachain team you wish to register the asset with for more information.

Last update: October 2, 2024
| Created: July 27, 2023