Skip to content

Overview of XC-20s

Introduction

The Cross-Consensus Message (XCM) format defines how messages can be sent between interoperable blockchains. This format opens the door to transferring messages and assets (Substrate assets) between Moonbeam/Moonriver and the relay chain or other parachains in the Polkadot/Kusama ecosystems.

Substrate assets are natively interoperable. However, developers need to tap into the Substrate API to interact with them, with no real visibility into the EVM. Consquently, interoperable Substrate assets are not that attractive for developers building on the EVM. To fix that and to help developers tap into the native interoperability that Polkadot/Kusama offers, Moonbeam introduced the concept of XC-20s.

XC-20s are a unique asset class on Moonbeam. It combines the power of Substrate assets (native interoperability) but allows users and developers to interact with them through a familiar ERC-20 interface. On the EVM side, XC-20s follow the standard ERC-20 interface, so smart contracts and users can easily interact with them, and no knowledge of Substrate is required. This ultimately provides greater flexibility for developers when working with these types of assets and allows seamless integrations with EVM-based smart contracts such as DEXs and lending platforms, among others. Moreover, developers can integrate XC-20s with regular Ethereum development frameworks, or dApps, and create connected contract strategies with such assets. Moreover, with the introduction of RT2301, all ERC-20s are XCM-ready, meaning they can also be referred to as XC-20s.

Moonbeam XC-20 XCM Integration With Polkadot

This page aims to cover the basics on XC-20s, if you want to learn how to interact with or transfer XC-20s, please refer to the Interact with XC-20s or the Using the X-Tokens Pallet To Send XC-20s guides.

Types of XC-20s

There are two types of XC-20s: local and external.

What are Local XC-20s?

Local XC-20s are all ERC-20s that exist on the EVM, and that can be transferred cross-chain through XCM. In order for local XC-20s to be transferred to another parachain, the asset needs to be registered on that chain. When transferring local XC-20s, the actual tokens reside in the destination chain's Sovereign account on Moonbeam. Local XC-20s must follow the ERC-20 interface outlined in this guide, they cannot be customized ERC-20s. More specifically, the function selector of 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.

What are External XC-20s?

External XC-20s are native cross-chain assets that are transferred from another parachain or the relay chain to Moonbeam. These assets are Substrate assets at their core. When transferring external XC-20s, the actual tokens reside in Moonbeam's Sovereign account in each of these chains. External XC-20s will all have xc prepended to their names to distinguish them as native cross-chain assets.

Local XC-20s vs External XC-20s

Both types of XC-20s can be easily sent to other parachains in the ecosystem as if they were Substrate assets, through both the Ethereum and Substrate API. However, using the Substrate API for XCM transfer will emit EVM logs for local XC-20s, but not for external XC-20s. Using the Ethereum API is recommended to provide more visibility into the XCM actions through EVM-based explorers, such as Moonscan.

Within Moonbeam, local XC-20s can only be transferred through their regular ERC-20 interface. On the contrary, external XC-20s can be transferred through both interfaces (Substrate and ERC-20). If external XC-20s are transferred through the Substrate API, the transaction won't be visible from EVM-based block explorers. Only transactions done via the Ethereum API are visible through such explorers.

The main difference between these two types of assets is that local XC-20s are EVM ERC-20s that have XCM capabilities, while external XC-20s are Substrate assets with an ERC-20 interface on top.

Cross-chain transfers of XC-20s are done using the X-Tokens Pallet. To learn how to use the X-Tokens Pallet to transfer XC-20s, you can refer to the Using the X-Tokens Pallet To Send XC-20s guide.

Current List of External XC-20s

The current list of available external XC-20 assets per network is as follows:

Origin Symbol XC-20 Address
Polkadot xcDOT 0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080
Acala xcaSEED 0xfFfFFFFF52C56A9257bB97f4B2b6F7B2D624ecda
Acala xcACA 0xffffFFffa922Fef94566104a6e5A35a4fCDDAA9f
Acala xcLDOT 0xFFfFfFffA9cfFfa9834235Fe53f4733F1b8B28d4
Apillon xcNCTR 0xFfFFfFfF8A9736B44EbF188972725bED67BF694E
Astar xcASTR 0xFfFFFfffA893AD19e540E172C10d78D4d479B5Cf
Bifrost xcBNC 0xFFffffFf7cC06abdF7201b350A1265c62C8601d2
Bifrost xcBNCS 0xfFfffffF6aF229AE7f0F4e0188157e189a487D59
Bifrost xcFIL 0xfFFfFFFF6C57e17D210DF507c82807149fFd70B2
Bifrost xcvASTR 0xFffFffff55C732C47639231a4C4373245763d26E
Bifrost xcvDOT 0xFFFfffFf15e1b7E3dF971DD813Bc394deB899aBf
Bifrost xcvFIL 0xFffffFffCd0aD0EA6576B7b285295c85E94cf4c1
Bifrost xcvGLMR 0xFfFfFFff99dABE1a8De0EA22bAa6FD48fdE96F6c
Bifrost xcvMANTA 0xFFfFFfFfdA2a05FB50e7ae99275F4341AEd43379
Centrifuge xcCFG 0xFFfFfFff44bD9D2FFEE20B25D1Cf9E78Edb6Eae3
Composable xcIBCMOVR 0xFfFfffFF3AFcd2cAd6174387df17180a0362E592
Composable xcIBCPICA 0xfFFFFfFFABe9934e61db3b11be4251E6e869cf59
Composable xcIBCIST 0xfFfFffff6A3977d5B65D1044FD744B14D9Cef932
Composable xcIBCBLD 0xFffFffff9664be0234ea4dc64558F695C4f2A9EE
Composable xcIBCTIA 0xFFFfFfff644a12F6F01b754987D175F5A780A75B
Composable xcIBCATOM 0xffFFFffF6807D5082ff2f6F86BdE409245e2D953
Darwinia xcRING 0xFfffFfff5e90e365eDcA87fB4c8306Df1E91464f
DED xcDED 0xfFffFFFf5da2d7214D268375cf8fb1715705FdC6
Equilibrium xcEQ 0xFffFFfFf8f6267e040D8a0638C576dfBa4F0F6D6
Equilibrium xcEQD 0xFFffFfFF8cdA1707bAF23834d211B08726B1E499
HydraDX xcHDX 0xFFFfFfff345Dc44DDAE98Df024Eb494321E73FcC
Interlay xcIBTC 0xFFFFFfFf5AC1f9A51A93F5C527385edF7Fe98A52
Interlay xcINTR 0xFffFFFFF4C1cbCd97597339702436d4F18a375Ab
Manta xcMANTA 0xfFFffFFf7D3875460d4509eb8d0362c611B4E841
Nodle xcNODL 0xfffffffFe896ba7Cb118b9Fa571c6dC0a99dEfF1
OriginTrail Parachain xcNEURO 0xFfffffFfB3229c8E7657eABEA704d5e75246e544
Parallel xcPARA 0xFfFffFFF18898CB5Fe1E88E668152B4f4052A947
Peaq xcPEAQ 0xFffFFFFFEC4908b74688a01374f789B48E9a3eab
Pendulum xcPEN 0xffFFfFFf2257622F345E1ACDe0D4f46D7d1D77D0
Phala xcPHA 0xFFFfFfFf63d24eCc8eB8a7b5D0803e900F7b6cED
Polkadex xcPDEX 0xfFffFFFF43e0d9b84010b1b67bA501bc81e33C7A
Polkadot Asset Hub xcPINK 0xfFfFFfFf30478fAFBE935e466da114E14fB3563d
Polkadot Asset Hub xcSTINK 0xFffFffFf54c556bD1d0F64ec6c78f1B477525E56
Polkadot Asset Hub xcUSDC 0xFFfffffF7D2B0B761Af01Ca8e25242976ac0aD7D
Polkadot Asset Hub xcUSDT 0xFFFFFFfFea09FB06d082fd1275CD48b191cbCD1d
Polkadot Asset Hub xcWIFD 0xfffffffF2e1D1ac9eA1686255bEfe995B31abc96
Snowbridge WBTC.e 0xfFffFFFf1B4Bb1ac5749F73D866FfC91a3432c47
Snowbridge wstETH.e 0xFfFFFfFF5D5DEB44BF7278DEE5381BEB24CB6573
Snowbridge WETH.e 0xfFffFFFF86829AFE1521AD2296719DF3ACE8DED7
Subsocial xcSUB 0xfFfFffFf43B4560Bc0C451a3386E082bff50aC90
Unique xcUNQ 0xFffffFFFD58f77E6693CFB99EbE273d73C678DC2
Zeitgeist xcZTG 0xFFFFfffF71815ab6142E0E20c7259126C6B40612

*You can check each Asset ID on Polkadot.js Apps

Origin Symbol XC-20 Address
Relay Chain Alphanet xcUNIT 0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080

*You can check each Asset ID on Polkadot.js Apps

Retrieve List of External XC-20s and Their Metadata

To fetch a list of the currently available external XC-20s along with their associated metadata, you can query the chain state using the Polkadot.js API. You'll take the following steps:

  1. Create an API provider for the network you'd like to get the list of assets for. You can use the following WSS endpoints for each network:

    wss://wss.api.moonbeam.network
    
    wss://wss.api.moonriver.moonbeam.network
    
    wss://wss.api.moonbase.moonbeam.network
    
  2. Query the assets pallet for all assets

  3. Iterate over the list of assets to get all of the asset IDs along with their associated metadata
import { ApiPromise, WsProvider } from '@polkadot/api';

const getXc20s = async () => {
  try {
    const substrateProvider = new WsProvider('INSERT_WSS_ENDPOINT');
    const api = await ApiPromise.create({ provider: substrateProvider });

    const assets = await api.query.assets.asset.entries();

    await Promise.all(
      assets.map(async ([{ args: [id] }]) => {
        try {
          const metadata = await api.query.assets.metadata(id);
          const humanMetadata = metadata.toHuman();

          console.log(`\nAsset ID: ${id}`);
          console.log('Metadata:');
          console.log('  Name:', humanMetadata.name);
          console.log('  Symbol:', humanMetadata.symbol);
          console.log('  Decimals:', humanMetadata.decimals);
          console.log('  Deposit:', humanMetadata.deposit);
          console.log('  IsFrozen:', humanMetadata.isFrozen);
          console.log('-----');
        } catch (error) {
          console.error(`Error fetching metadata for asset ${id}:`, error);
        }
      })
    );

    await api.disconnect();
  } catch (error) {
    console.error('Error in getXc20s:', error);
  }
};

getXc20s().catch(console.error);

The result will display the asset ID along with some additional information for all of the registered external XC-20s.

Retrieve Local XC-20 Metadata

Since local XC-20s are ERC-20s on Moonbeam that can be transferred via XCM to another parachain, you can interact with local XC-20s like you would an ERC-20. As long as you have the address and the ABI of the ERC-20, you can retrieve its metadata by interacting with its ERC-20 interface to retrieve the name, symbol, and decimals for the asset.

The following is an example that retrieves the asset metadata for the Jupiter token on Moonbase Alpha:

import { ethers } from 'ethers';

const providerRPC = {
  moonbase: {
    name: 'moonbase',
    rpc: 'https://rpc.api.moonbase.moonbeam.network', // Insert your RPC URL here
    chainId: 1287, // 0x507 in hex,
  },
};

const provider = new ethers.JsonRpcProvider(providerRPC.moonbase.rpc, {
  chainId: providerRPC.moonbase.chainId,
  name: providerRPC.moonbase.name,
});

// Replace with the address of the ERC-20 token
const tokenAddress = '0x9Aac6FB41773af877a2Be73c99897F3DdFACf576';
const tokenABI = [
  'function name() view returns (string)',
  'function symbol() view returns (string)',
  'function decimals() view returns (uint8)',
];

const tokenContract = new ethers.Contract(tokenAddress, tokenABI, provider);
async function getTokenMetadata() {
  try {
    const [name, symbol, decimals] = await Promise.all([
      tokenContract.name(),
      tokenContract.symbol(),
      tokenContract.decimals(),
    ]);
    console.log(`Name: ${name}`);
    console.log(`Symbol: ${symbol}`);
    console.log(`Decimals: ${decimals}`);
  } catch (error) {
    console.error('Error fetching token metadata:', error);
  }
}
getTokenMetadata();
import { Web3 } from 'web3';

// Insert your RPC URL here
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');

// Replace with the address of the ERC-20 token
const tokenAddress = '0x9Aac6FB41773af877a2Be73c99897F3DdFACf576';
const tokenABI = [
  // ERC-20 ABI
  {
    constant: true,
    inputs: [],
    name: 'name',
    outputs: [{ name: '', type: 'string' }],
    payable: false,
    stateMutability: 'view',
    type: 'function',
  },
  {
    constant: true,
    inputs: [],
    name: 'symbol',
    outputs: [{ name: '', type: 'string' }],
    payable: false,
    stateMutability: 'view',
    type: 'function',
  },
  {
    constant: true,
    inputs: [],
    name: 'decimals',
    outputs: [{ name: '', type: 'uint8' }],
    payable: false,
    stateMutability: 'view',
    type: 'function',
  },
];
const tokenContract = new web3.eth.Contract(tokenABI, tokenAddress);
async function getTokenMetadata() {
  try {
    const [name, symbol, decimals] = await Promise.all([
      tokenContract.methods.name().call(),
      tokenContract.methods.symbol().call(),
      tokenContract.methods.decimals().call(),
    ]);
    console.log(`Name: ${name}`);
    console.log(`Symbol: ${symbol}`);
    console.log(`Decimals: ${decimals}`);
  } catch (error) {
    console.error('Error fetching token metadata:', error);
  }
}
getTokenMetadata();
from web3 import Web3

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

# Replace with the address of the ERC-20 token
token_address = "0x9Aac6FB41773af877a2Be73c99897F3DdFACf576"
token_abi = [  # ERC-20 ABI
    {
        "constant": True,
        "inputs": [],
        "name": "name",
        "outputs": [{"name": "", "type": "string"}],
        "payable": False,
        "stateMutability": "view",
        "type": "function",
    },
    {
        "constant": True,
        "inputs": [],
        "name": "symbol",
        "outputs": [{"name": "", "type": "string"}],
        "payable": False,
        "stateMutability": "view",
        "type": "function",
    },
    {
        "constant": True,
        "inputs": [],
        "name": "decimals",
        "outputs": [{"name": "", "type": "uint8"}],
        "payable": False,
        "stateMutability": "view",
        "type": "function",
    },
]
token_contract = web3.eth.contract(address=token_address, abi=token_abi)


def get_token_metadata():
    try:
        name = token_contract.functions.name().call()
        symbol = token_contract.functions.symbol().call()
        decimals = token_contract.functions.decimals().call()
        print(f"Name: {name}")
        print(f"Symbol: {symbol}")
        print(f"Decimals: {decimals}")
    except Exception as e:
        print(f"Error fetching token metadata: {e}")


get_token_metadata()
Last update: November 5, 2024
| Created: December 22, 2021