Skip to content

Native Cross-Chain Token Transfers

Introduction

As presented in the Native Cross-Chain Communication article from the Learn section, Tanssi appchains benefit from an inherent capability to communicate and interoperate with any other appchain in the ecosystem. This native cross-chain communication allows safe and fast token transfers leveraging the Cross-Consensus Message format (XCM for short), which facilitates communication between different consensus systems.

The communication protocol enabling token transfers is built on Substrate and runs on a lower level than the EVM, making it harder for EVM developers to access.

Nevertheless, EVM appchains have an XCM precompile that fills the gap between execution layers, exposing a smart contract interface that abstracts away the underlying complexities, making the execution of cross-chain token transfers as easy as any other smart contract call.

This guide will show you how to interact with the XCM Interface precompile to execute cross-chain token transfers through the Ethereum API.

The XCM precompile is located at the following address:

0x0000000000000000000000000000000000000804

Note

There can be some unintended consequences when using precompiles. Tanssi's precompiles are derived from Moonbeam's, and as such, please familiarize yourself with Moonbeam's Precompile Security Considerations.

The XCM Solidity Interface

The XCMInterface.sol interface on Tanssi EVM appchains is a Solidity interface that allows developers to interact with the precompile's functions.

XCMInterface.sol
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;

/// @dev The XCM contract's address.
address constant XCM_CONTRACT_ADDRESS = 0x0000000000000000000000000000000000000804;

/// @dev The XCM contract's instance.
XCM constant XCM_CONTRACT = XCM(XCM_CONTRACT_ADDRESS);

/// @author The Moonbeam Team
/// @title XCM precompile Interface
/// @dev The interface that Solidity contracts use to interact with the substrate pallet-xcm.
interface XCM {
    // A location is defined by its number of parents and the encoded junctions (interior)
    struct Location {
        uint8 parents;
        bytes[] interior;
    }

    // Support for Weights V2
    struct Weight {
        uint64 refTime;
        uint64 proofSize;
    }

    // A way to represent fungible assets in XCM using Location format
    struct AssetLocationInfo {
        Location location;
        uint256 amount;
    }

    // A way to represent fungible assets in XCM using address format
    struct AssetAddressInfo {
        address asset;
        uint256 amount;
    }

    /// @dev Function to send assets via XCM using transfer_assets() pallet-xcm extrinsic.
    /// @custom:selector 59df8416
    /// @param dest The destination chain.
    /// @param beneficiary The actual account that will receive the tokens on dest.
    /// @param assets The combination (array) of assets to send.
    /// @param feeAssetItem The index of the asset that will be used to pay for fees.
    /// @param weight The weight to be used for the whole XCM operation.
    /// (uint64::MAX in refTime means Unlimited weight) 
    function transferAssetsLocation(
        Location memory dest,
        Location memory beneficiary,
        AssetLocationInfo[] memory assets,
        uint32 feeAssetItem,
        Weight memory weight
    ) external;

    /// @dev Function to send assets via XCM to a 20 byte-like parachain 
    /// using transfer_assets() pallet-xcm extrinsic.
    /// @custom:selector b489262e
    /// @param paraId The para-id of the destination chain.
    /// @param beneficiary The actual account that will receive the tokens on paraId destination.
    /// @param assets The combination (array) of assets to send.
    /// @param feeAssetItem The index of the asset that will be used to pay for fees.
    /// @param weight The weight to be used for the whole XCM operation.
    /// (uint64::MAX in refTime means Unlimited weight)
    function transferAssetsToPara20(
        uint32 paraId,
        address beneficiary,
        AssetAddressInfo[] memory assets,
        uint32 feeAssetItem,
        Weight memory weight
    ) external;

    /// @dev Function to send assets via XCM to a 32 byte-like parachain 
    /// using transfer_assets() pallet-xcm extrinsic.
    /// @custom:selector 4461e6f5
    /// @param paraId The para-id of the destination chain.
    /// @param beneficiary The actual account that will receive the tokens on paraId destination.
    /// @param assets The combination (array) of assets to send.
    /// @param feeAssetItem The index of the asset that will be used to pay for fees.
    /// @param weight The weight to be used for the whole XCM operation.
    /// (uint64::MAX in refTime means Unlimited weight)
    function transferAssetsToPara32(
        uint32 paraId,
        bytes32 beneficiary,
        AssetAddressInfo[] memory assets,
        uint32 feeAssetItem,
        Weight memory weight
    ) external;

    /// @dev Function to send assets via XCM to the relay chain 
    /// using transfer_assets() pallet-xcm extrinsic.
    /// @custom:selector d7c89659
    /// @param beneficiary The actual account that will receive the tokens on the relay chain.
    /// @param assets The combination (array) of assets to send.
    /// @param feeAssetItem The index of the asset that will be used to pay for fees.
    /// @param weight The weight to be used for the whole XCM operation.
    /// (uint64::MAX in refTime means Unlimited weight)
    function transferAssetsToRelay(
        bytes32 beneficiary,
        AssetAddressInfo[] memory assets,
        uint32 feeAssetItem,
        Weight memory weight
    ) external;
}

The interface includes the necessary data structures along with the following functions:

transferAssetsToPara20(paraId, beneficiary, assets, feeAssetItem, weight) — sends assets to another EVM-compatible appchain using the underlying transfer_assets() transaction included in the XCM pallet module
  • paraId uint32 - the destination's appchain ID
  • beneficiary address - the ECDSA-type account in the destination chain that will receive the tokens
  • assets AssetAddressInfo[] memory - an array of assets to send
  • feeAssetItem uint32 - the index of the asset that will be used to pay fees
  • weight Weight memory- the maximum gas to use in the whole operation. Setting uint64::MAX to refTime acts in practice as unlimited weight
  • paraId - 888
  • beneficiary - 0x3f0Aef9Bd799F1291b80376aD57530D353ab0217
  • assets - [["0x0000000000000000000000000000000000000800", 1000000000000000000]]
  • feeAssetItem - 0
  • weight - [9223372036854775807, 9223372036854775807]
transferAssetsToPara32(paraId, beneficiary, assets,feeAssetItem, weight) — sends assets to a Substrate appchain using the underlying transfer_assets() transaction included in the XCM pallet module
  • paraId uint32 - the destination's appchain ID
  • beneficiary bytes32 - the Substrate's SR25519-type account in the destination chain that will receive the tokens
  • assets AssetAddressInfo[] memory - an array of assets to send
  • feeAssetItem uint32 - the index of the asset that will be used to pay fees
  • weight Weight memory - the maximum gas to use in the whole operation. Setting uint64::MAX to refTime acts in practice as unlimited weight
  • paraId - 888
  • beneficiary - 0xf831d83025f527daeed39a644d64d335a4e627b5f4becc78fb67f05976889a06
  • assets - [["0x0000000000000000000000000000000000000800", 1000000000000000000]]
  • feeAssetItem - 0
  • weight - [9223372036854775807, 9223372036854775807]
transferAssetsToRelay(beneficiary, assets, feeAssetItem, weight) — sends assets to the relay chain using the underlying transfer_assets() transaction included in the XCM pallet module
  • beneficiary bytes32 - the Substrate's sr25519-type account in the relay chain that will receive the tokens
  • assets AssetAddressInfo[] memory - an array of assets to send
  • feeAssetItem uint32 - the index of the asset that will be used to pay fees
  • weight Weight memory - the maximum gas to use in the whole operation. Setting uint64::MAX to refTime acts in practice as unlimited weight
  • beneficiary - 0xf831d83025f527daeed39a644d64d335a4e627b5f4becc78fb67f05976889a06
  • assets - [["0x0000000000000000000000000000000000000800", 1000000000000000000]]
  • feeAssetItem - 0
  • weight - [9223372036854775807, 9223372036854775807]
transferAssetsLocation(dest, beneficiary, assets, feeAssetItem, weight) — sends assets using the underlying transfer_assets() transaction included in the XCM pallet module
  • dest Location memory - the destination chain
  • beneficiary Location memory - the account in the destination chain that will receive the tokens
  • assets AssetLocationInfo[] memory - an array of assets to send
  • feeAssetItem uint32 - the index of the asset that will be used to pay fees
  • weight Weight memory - the maximum gas to use in the whole operation. Setting uint64::MAX to refTime acts in practice as unlimited weight
  • dest - ["1",[]]
  • beneficiary - [0, ["0x01f831d83025f527daeed39a644d64d335a4e627b5f4becc78fb67f05976889a0600"]]
  • assets - [[[1, ["0x010000000000000000000000000000000000000800"]], 1000000000000000000]]
  • feeAssetItem - 0
  • weight - [9223372036854775807, 9223372036854775807]

Interact with the Solidity Interface

Checking Prerequisites

To follow along with this tutorial, you will need to have your wallet configured to work with your EVM appchain and an account funded with native tokens. You can add your EVM appchain to MetaMask with one click on the Tanssi dApp. Or, you can configure MetaMask for Tanssi with the demo EVM appchain.

Note

It is necessary to have previously established communication channels with the destination chain before using this precompile's functionality. To do so, refer to the Manage Cross-Chain Communication Channels guide. Also, if the token being transferred is native to your appchain, the destination chain must have registered the foreign asset.

Remix Set Up

You can interact with the XCM Interface precompile using Remix. To add the precompile to Remix, you will need to:

  1. Get a copy of XCMInterface.sol
  2. Paste the file contents into a Remix file named XcmInterface.sol

Compile the Contract

Next, you will need to compile the interface in Remix:

  1. Click on the Compile tab, second from top
  2. Compile the interface by clicking on Compile XCMInterface.sol

Compiling XcmInterface.sol

When the compilation is completed, you will see a green checkmark next to the Compile tab.

Access the Contract

Instead of deploying the precompile, you will access the interface given the address of the precompiled contract:

  1. Click on the Deploy and Run tab directly below the Compile tab in Remix. Please note that the precompiled contracts are already accessible at their respective addresses. Therefore, there is no deployment step
  2. Make sure Injected Provider - Metamask is selected in the ENVIRONMENT dropdown. Once you select Injected Provider - Metamask, you may be prompted by MetaMask to connect your account to Remix if it's not already connected
  3. Make sure the correct account is displayed under ACCOUNT
  4. Ensure XCM - XCMInterface.sol is selected in the CONTRACT dropdown. Given that it is a precompiled contract, there is no deployment step. Instead, you are going to provide the address of the precompile in the At Address field
  5. Provide the address of the precompile: 0x0000000000000000000000000000000000000804 and click At Address

Access the address

The XCM Interface precompile will appear in the list of Deployed Contracts.

Send Tokens Over to Another EVM-Compatible Appchain

To send tokens over to an account in another EVM-compatible appchain, please follow these steps:

  1. Expand the transferAssetsToPara20 function
  2. Enter the appchain ID (paraId)
  3. Enter the 20-bytes (Ethereum-like) destination account (beneficiary)
  4. Specify the tokens to be transferred. Note that this parameter is an array that contains at least one asset. Each asset is specified by its address and the total amount to transfer

    Note

    Tokens are specified by their ERC-20 address. If the token you want to transfer is the appchain's native one, the Native Token ERC-20 Precompile will help you reference it through an ERC-20 interface.

  5. Enter the index of the asset that will be used to pay the fees. This index is zero-based, so the first element is 0, the second is 1, and so on

  6. Enter the maximum gas to pay for the transaction. This gas is derived from two parameters, the processing time (refTime) and the proof size (proofSize). In practice, setting refTime to uint64::MAX is equal to unlimited weight
  7. Click transact
  8. MetaMask will pop up, and you will be prompted to review the transaction details. Click Confirm to send the transaction

Confirm Approve Transaction

After the transaction is confirmed, wait for a few blocks for the transfer to reach the destination chain and reflect the new balance.

Send Tokens Over to a Substrate Appchain

To send tokens over to an account in a Substrate appchain, please follow these steps:

  1. Expand the transferAssetsToPara32 function
  2. Enter the appchain ID (paraId)
  3. Enter the sr25519-type destination account (beneficiary)
  4. Specify the tokens to be transferred. Note that this parameter is an array that contains at least one asset. Each asset is specified by its address and the total amount to transfer

    Note

    Tokens are specified by their ERC-20 address. If the token you want to transfer is the appchain's native one, the Native Token ERC-20 Precompile will help you reference it through an ERC-20 interface.

  5. Enter the index of the asset that will be used to pay the fees. This index is zero-based, so the first element is 0, the second is 1, and so on

  6. Enter the maximum gas to pay for the transaction. This gas is derived from two parameters, the processing time (refTime) and the proof size (proofSize). In practice, setting refTime to uint64::MAX is equal to unlimited weight
  7. Click transact
  8. MetaMask will pop up, and you will be prompted to review the transaction details. Click Confirm to send the transaction

Confirm Approve Transaction

After the transaction is confirmed, wait for a few blocks for the transfer to reach the destination chain and reflect the new balance.

Send Tokens Over to the Relay Chain

To send tokens over to an account in the relay chain, please follow these steps:

  1. Expand the transferAssetsToRelay function
  2. Enter the sr25519-type destination account (beneficiary)
  3. Specify the tokens to be transferred. Note that this parameter is an array that contains at least one asset. Each asset is specified by its address and the total amount to transfer

    Note

    Tokens are specified by their ERC-20 address. If the token you want to transfer is the appchain's native one, the Native Token ERC-20 Precompile will help you reference it through an ERC-20 interface.

  4. Enter the index of the asset that will be used to pay the fees. This index is zero-based, so the first element is 0, the second is 1, and so on

  5. Enter the maximum gas to pay for the transaction. This gas is derived from two parameters, the processing time (refTime) and the proof size (proofSize). In practice, setting refTime to uint64::MAX is equal to unlimited weight
  6. Click transact
  7. MetaMask will pop up, and you will be prompted to review the transaction details. Click Confirm to send the transaction

Confirm Approve Transaction

After the transaction is confirmed, wait for a few blocks for the transfer to reach the destination chain and reflect the new balance.

Send Tokens Over Specific Locations

This function is more generic than the others, allowing the destination chain, destination account, and assets to be specified using XCM Multilocations. To send tokens to specific locations, please follow these steps:

  1. Expand the transferAssetsLocation function
  2. Enter the multilocation that specifies the destination chain. Note that any chain can be specified, regardless of its configuration or type
  3. Enter the Multilocation that specifies the destination account. Note that any account can be specified, regardless of its type (ECDSA, sr25519, or any other)
  4. Specify the tokens to be transferred. Note that this parameter is an array that contains at least one asset and each asset is specified by its Multilocation and the total amount to transfer

    Note

    Tokens are specified by their ERC-20 address. If the token you want to transfer is the appchain's native one, the Native Token ERC-20 Precompile will help you reference it through an ERC-20 interface.

  5. Enter the index of the asset that will be used to pay the fees. This index is zero-based, so the first element is 0, the second is 1, and so on

  6. Enter the maximum gas to pay for the transaction. This gas is derived from two parameters, the processing time (refTime) and the proof size (proofSize). In practice, setting refTime to uint64::MAX is equal to unlimited weight
  7. Click transact
  8. MetaMask will pop up, and you will be prompted to review the transaction details. Click Confirm to send the transaction

Confirm Approve Transaction

After the transaction is confirmed, wait for a few blocks for the transfer to reach the destination chain and reflect the new balance.

The information presented herein has been provided by third parties and is made available solely for general information purposes. Tanssi does not endorse any project listed and described on the Tanssi Doc Website (https://docs.tanssi.network/). Tanssi Foundation does not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. Tanssi Foundation disclaims all liability and responsibility arising from any reliance placed on this information by you or by anyone who may be informed of any of its contents. All statements and/or opinions expressed in these materials are solely the responsibility of the person or entity providing those materials and do not necessarily represent the opinion of Tanssi Foundation. The information should not be construed as professional or financial advice of any kind. Advice from a suitably qualified professional should always be sought in relation to any particular matter or circumstance. The information herein may link to or integrate with other websites operated or content provided by third parties, and such other websites may link to this website. Tanssi Foundation has no control over any such other websites or their content and will have no liability arising out of or related to such websites or their content. The existence of any such link does not constitute an endorsement of such websites, the content of the websites, or the operators of the websites. These links are being provided to you only as a convenience and you release and hold Tanssi Foundation harmless from any and all liability arising from your use of this information or the information provided by any third-party website or service.
Last update: August 9, 2024
| Created: February 2, 2024