Skip to Content
DocumentationAnalog GMPDevelopers' GuideTutorialsTeleport Tokens from Chain A to Chain B

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:

  1. 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
  2. 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
  3. 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 onGmpReceived on the target BasicERC20 contract.
  • 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 { }
  1. Import ERC20.sol from "@solmate/tokens.
  2. Import IGmpReceiver.sol and IGateway.sol from @analog-gmp/interfaces.
  3. Import GmpSender and PrimitiveUtils from @analog-gmp.
  4. Inherit the IGmpReceiver.sol contract 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); } }
  1. Create an instance of the _trustedGateway of type IGateway. This is the contract that will submit the message to the target chain. Next, create two instances: _recipientErc20 of type BasicERC20 and _recipientNetwork of type uint16.
  2. Create two events: OutboundTransfer that will be emitted when tokens are teleported from Shibuya to Sepolia, and InboundTransfer will be emitted when the tokens are teleported from one account (`from`) in another chain to an account (`to`) in this chain.
  3. Create a TeleportCommand of type struct that will be encoded in the “data” field of the “onGmpReceived” function.
  4. Create the constructor with the parameters: name, symbol, gatewayAddress, recipient, recipientNetwork, and holder, and set them appropriately. For example, this contract needs a reference to the IGateway and ERC20, 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); }
  1. Create a function named teleport that 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.
  1. Burn the ERC-20 tokens specified by the amount variable for a given sender on the source chain.
  2. Create a GMP payload (i.e., message). For our BasicERC20 DApp, the message should contain the sender, recipient, and amount. We’ll need to abi-encode these parameters and set them as the message.
  3. Call the Gateway contract to generate a cross-chain request. Call the submitMessage method 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; }
  1. It’s important that this function's name remains onGmpReceived and 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.
  2. Ensure that only the Gateway contract calls this function; no other addresses or contracts should have access to it.
  3. Ensure that the network and sender address are validated.
  4. Decode the packet using ABI encoding the data received and TeleportCommand and store it in the command variable.
  5. Mint the tokens to the destination account and emit the InboundTransfer event with the id, command.from, command.to, and command.amount.