Teleport Tokens from Chain A to Chain B
This tutorial shows how to move ERC-20 tokens from Astar Shibuya to Ethereum Sepolia. The full source code for the contract utilized in this demo is available here .
Overview
Token Teleportation
The contract allows tokens to be "teleported" (transferred) between chains through these functions:
teleport(address recipient, uint256 amount):- Burns tokens from the sender on the source chain
- Creates a cross-chain message with the transfer details
- Submits the message to the gateway contract
- Returns a unique
message ID
onGmpReceived(bytes32 id, uint128 network, bytes32 sender, bytes calldata data):- Receives the cross-chain message on the destination chain
- Validates the message source
- Mints tokens to the recipient on the destination chain
teleportCost(uint16 networkid, address recipient, uint256 amount):- Estimates the cost (in native currency) required to teleport tokens
Cross-Chain Workflow
On the Source Chain:
- User calls
teleport, burning tokens and initiating a cross-chain message. - The gateway relays the message to the destination chain.
On the Destination Chain:
- The gateway triggers
onGmpReceivedon the targetBasicERC20contract. - Tokens are minted to the recipient.
Step-by-Step Guide
Step 1: Instantiating the contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {ERC20} from "@solmate/tokens/ERC20.sol";
import {IGmpReceiver} from "@analog-gmp/interfaces/IGmpReceiver.sol";
import {IGateway} from "@analog-gmp/interfaces/IGateway.sol";
import {GmpSender, PrimitiveUtils} from "@analog-gmp/Primitives.sol";
contract BasicERC20 is ERC20, IGmpReceiver {
}- Import
ERC20.solfrom "@solmate/tokens. - Import
IGmpReceiver.solandIGateway.solfrom@analog-gmp/interfaces. - Import
GmpSenderandPrimitiveUtilsfrom@analog-gmp. - Inherit the
IGmpReceiver.solcontract into your main contract (i.e.,BasicERC20).
Step 2: Create state variables and constructor
IGateway private immutable _trustedGateway;
BasicERC20 private immutable _recipientErc20;
uint16 private immutable _recipientNetwork;
/**
* @dev Emitted when `amount` tokens are teleported from one account (`from`) in this chain to
* another (`to`) in another chain.
*
* Note Is not necessary to emit the destination network, because this is already emitted by the gateway in `GmpCreated` event.
*/
event OutboundTransfer(bytes32 indexed id, address indexed from, address indexed to, uint256 amount);
/**
* @dev @dev Emitted when `amount` tokens are teleported from one account (`from`) in another chain to
* an account (`to`) in this chain.
*
* Note Is not necessary to emit the source network, because this is already emitted by the gateway in `GmpExecuted` event.
*/
event InboundTransfer(bytes32 indexed id, address indexed from, address indexed to, uint256 amount);
/**
* @dev Gas limit used to execute the `onGmpReceived` method.
*/
uint256 private constant MSG_GAS_LIMIT = 100_000;
/**
* @dev Command that will be encoded in the `data` field on the `onGmpReceived` method.
*/
struct TeleportCommand {
address from;
address to;
uint256 amount;
}
constructor(
string memory name,
string memory symbol,
IGateway gatewayAddress,
BasicERC20 recipient,
uint16 recipientNetwork,
address holder,
uint256 initialSupply
) ERC20(name, symbol, 10) {
_trustedGateway = gatewayAddress;
_recipientErc20 = recipient;
_recipientNetwork = recipientNetwork;
if (initialSupply > 0) {
_mint(holder, initialSupply);
}
}
- Create an instance of the
_trustedGatewayof typeIGateway. This is the contract that will submit the message to the target chain. Next, create two instances:_recipientErc20of typeBasicERC20and_recipientNetworkof typeuint16. - Create two events:
OutboundTransferthat will be emitted when tokens are teleported from Shibuya to Sepolia, andInboundTransferwill be emitted when the tokens are teleported from one account (`from`) in another chain to an account (`to`) in this chain. - Create a
TeleportCommandof type struct that will be encoded in the “data” field of the “onGmpReceived” function. - Create the constructor with the parameters:
name,symbol,gatewayAddress,recipient,recipientNetwork, andholder, and set them appropriately. For example, this contract needs a reference to theIGatewayandERC20, so we’ll pass these details on to the constructor.
Step 3: Teleport the tokens
/**
* @dev Teleport tokens from `msg.sender` to `recipient` in `_recipientNetwork`
*/
function teleport(address recipient, uint256 amount) external returns (bytes32 messageID) {
_burn(msg.sender, amount);
bytes memory message = abi.encode(TeleportCommand({from: msg.sender, to: recipient, amount: amount}));
messageID = _trustedGateway.submitMessage(address(_recipientErc20), _recipientNetwork, MSG_GAS_LIMIT, message);
emit OutboundTransfer(messageID, msg.sender, recipient, amount);
}- Create a function named
teleportthat will send the tokens from Shibuya to Sepolia. This function has the following parameters:
recipient: the receiver of ERC-20 tokens on the destination chain.amount: the amount of ERC-20 tokens the recipient receives.
- Burn the ERC-20 tokens specified by the
amountvariable for a given sender on the source chain. - Create a GMP payload (i.e.,
message). For ourBasicERC20DApp, themessageshould contain the sender, recipient, and amount. We’ll need to abi-encode these parameters and set them as themessage. - Call the Gateway contract to generate a cross-chain request. Call the
submitMessagemethod of the Gateway contract with the required parameters to route the message to the destination chain. For more details about this function, read here.
Step 4: Handling the GMP call
After setting up the logic for teleporting tokens from the source chain, the next step is to implement the onGmpReceived method of the IGmpReceiver interface to manage the GMP request on the target chain. The onGmpReceived function will consist of the following signature:
function onGmpReceived(bytes32 id, uint128 network, bytes32 sender, bytes calldata data)
external
payable
returns (bytes32)
{
// Convert bytes32 to address
address senderAddr = GmpSender.wrap(sender).toAddress();
// Validate the message
require(msg.sender == address(_trustedGateway), "Unauthorized: only the gateway can call this method");
require(network == _recipientNetwork, "Unauthorized network");
require(senderAddr == address(_recipientErc20), "Unauthorized sender");
// Decode the command
TeleportCommand memory command = abi.decode(data, (TeleportCommand));
// Mint the tokens to the destination account
_mint(command.to, command.amount);
emit InboundTransfer(id, command.from, command.to, command.amount);
return id;
}
- It’s important that this function's name remains
onGmpReceivedand that its signature, including all the parameters, remains the same. This is crucial as the Gateway contract on the destination chain will call this function. Any modifications to the name or parameters could lead to a failed call. - Ensure that only the Gateway contract calls this function; no other addresses or contracts should have access to it.
- Ensure that the network and sender address are validated.
- Decode the packet using ABI encoding the data received and
TeleportCommandand store it in thecommandvariable. - Mint the tokens to the destination account and emit the
InboundTransferevent with theid,command.from,command.to, andcommand.amount.