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 IDbeneficiary
address - the ECDSA-type account in the destination chain that will receive the tokensassets
AssetAddressInfo[] memory - an array of assets to sendfeeAssetItem
uint32 - the index of the asset that will be used to pay feesweight
Weight memory- the maximum gas to use in the whole operation. Setting uint64::MAX torefTime
acts in practice as unlimited weight
paraId
- 888beneficiary
- 0x3f0Aef9Bd799F1291b80376aD57530D353ab0217assets
- [["0x0000000000000000000000000000000000000800", 1000000000000000000]]feeAssetItem
- 0weight
- [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 IDbeneficiary
bytes32 - the Substrate's SR25519-type account in the destination chain that will receive the tokensassets
AssetAddressInfo[] memory - an array of assets to sendfeeAssetItem
uint32 - the index of the asset that will be used to pay feesweight
Weight memory - the maximum gas to use in the whole operation. Setting uint64::MAX torefTime
acts in practice as unlimited weight
paraId
- 888beneficiary
- 0xf831d83025f527daeed39a644d64d335a4e627b5f4becc78fb67f05976889a06assets
- [["0x0000000000000000000000000000000000000800", 1000000000000000000]]feeAssetItem
- 0weight
- [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 tokensassets
AssetAddressInfo[] memory - an array of assets to sendfeeAssetItem
uint32 - the index of the asset that will be used to pay feesweight
Weight memory - the maximum gas to use in the whole operation. Setting uint64::MAX torefTime
acts in practice as unlimited weight
beneficiary
- 0xf831d83025f527daeed39a644d64d335a4e627b5f4becc78fb67f05976889a06assets
- [["0x0000000000000000000000000000000000000800", 1000000000000000000]]feeAssetItem
- 0weight
- [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 chainbeneficiary
Location memory - the account in the destination chain that will receive the tokensassets
AssetLocationInfo[] memory - an array of assets to sendfeeAssetItem
uint32 - the index of the asset that will be used to pay feesweight
Weight memory - the maximum gas to use in the whole operation. Setting uint64::MAX torefTime
acts in practice as unlimited weight
dest
- ["1",[]]beneficiary
- [0, ["0x01f831d83025f527daeed39a644d64d335a4e627b5f4becc78fb67f05976889a0600"]]assets
- [[[1, ["0x010000000000000000000000000000000000000800"]], 1000000000000000000]]feeAssetItem
- 0weight
- [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:
- Get a copy of
XCMInterface.sol
- Paste the file contents into a Remix file named
XcmInterface.sol
Compile the Contract¶
Next, you will need to compile the interface in Remix:
- Click on the Compile tab, second from top
- Compile the interface by clicking on Compile 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:
- 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
- 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
- Make sure the correct account is displayed under ACCOUNT
- 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
- Provide the address of the precompile:
0x0000000000000000000000000000000000000804
and click At 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:
- Expand the transferAssetsToPara20 function
- Enter the appchain ID (paraId)
- Enter the 20-bytes (Ethereum-like) destination account (beneficiary)
-
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.
-
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 is1
, and so on - 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 touint64::MAX
is equal to unlimited weight - Click transact
- MetaMask will pop up, and you will be prompted to review the transaction details. Click Confirm to send the 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:
- Expand the transferAssetsToPara32 function
- Enter the appchain ID (
paraId
) - Enter the sr25519-type destination account (beneficiary)
-
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.
-
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 is1
, and so on - 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 - Click transact
- MetaMask will pop up, and you will be prompted to review the transaction details. Click Confirm to send the 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:
- Expand the transferAssetsToRelay function
- Enter the sr25519-type destination account (beneficiary)
-
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.
-
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 is1
, and so on - 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 - Click transact
- MetaMask will pop up, and you will be prompted to review the transaction details. Click Confirm to send the 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:
- Expand the transferAssetsLocation function
- Enter the multilocation that specifies the destination chain. Note that any chain can be specified, regardless of its configuration or type
- Enter the Multilocation that specifies the destination account. Note that any account can be specified, regardless of its type (ECDSA, sr25519, or any other)
-
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.
-
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 is1
, and so on - 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 - Click transact
- MetaMask will pop up, and you will be prompted to review the transaction details. Click Confirm to send the transaction
After the transaction is confirmed, wait for a few blocks for the transfer to reach the destination chain and reflect the new balance.
| Created: February 2, 2024