Skip to main content
Cross-Chain Messaging
Value Transfer

Multichain Value Transfer

In this tutorial you will learn how to send ZETA tokens between connected blockchains using ZetaChain.

In this example you will only be sending ZETA tokens without any associated message.

Set up your environment

git clone
cd template

Create the Contract

To create a new cross-chain messaging contract you will use the messaging Hardhat task available by default in the template.

npx hardhat messaging Value --fees zetaRevert

Use the --fees flag to specify that you want your contract to accept ZETA tokens as fees. Since the purpose of this contract is to send ZETA tokens, it makes sense to also use ZETA tokens as fees.

Modify the Contract

// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@openzeppelin/contracts/interfaces/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@zetachain/protocol-contracts/contracts/evm/tools/ZetaInteractor.sol";
import "@zetachain/protocol-contracts/contracts/evm/interfaces/ZetaInterfaces.sol";
import "@zetachain/protocol-contracts/contracts/evm/Zeta.eth.sol";

contract Value is ZetaInteractor {
error ErrorTransferringZeta();
error InvalidMessageType();

event ValueEvent();
event ValueRevertedEvent();

bytes32 public constant VALUE_MESSAGE_TYPE = keccak256("CROSS_CHAIN_VALUE");
IERC20 internal immutable _zetaToken;

address connectorAddress,
address zetaTokenAddress
) ZetaInteractor(connectorAddress) {
_zetaToken = IERC20(zetaTokenAddress);

function sendMessage(
uint256 destinationChainId,
uint256 zetaValueAndGas
) external payable {
if (!_isValidChainId(destinationChainId))
revert InvalidDestinationChainId();

bool success1 = _zetaToken.approve(address(connector), zetaValueAndGas);
bool success2 = _zetaToken.transferFrom(
if (!(success1 && success2)) revert ErrorTransferringZeta();

destinationChainId: destinationChainId,
destinationAddress: interactorsByChainId[destinationChainId],
destinationGasLimit: 300000,
message: abi.encode(),
zetaValueAndGas: zetaValueAndGas,
zetaParams: abi.encode("")

function onZetaMessage(
ZetaInterfaces.ZetaMessage calldata zetaMessage
) external override isValidMessageCall(zetaMessage) {
bytes32 messageType = abi.decode(zetaMessage.message, (bytes32));

if (messageType != VALUE_MESSAGE_TYPE) revert InvalidMessageType();

emit ValueEvent();

function onZetaRevert(
ZetaInterfaces.ZetaRevert calldata zetaRevert
) external override isValidRevertCall(zetaRevert) {
bytes32 messageType = abi.decode(zetaRevert.message, (bytes32));

if (messageType != VALUE_MESSAGE_TYPE) revert InvalidMessageType();

emit ValueRevertedEvent();

Modify the contract so that it only inherits from ZetaInteractor. Since the purpose of the contract is to only send ZETA tokens (and not messages), it doesn't need to inherit from ZetaMessageReceiver and implement the onZetaMessage and onZetaRevert functions.

You can also remove the message type from the connector.send call.

Deploy the Contract

Clear the cache and artifacts, then compile the contract:

npx hardhat compile --force

Run the following command to deploy the contract to two networks:

npx hardhat deploy --networks sepolia_testnet,mumbai_testnet

Send a message

Run the following command to send ZETA tokens from Sepolia to Mumbai. Please, note that since the contract expect ZETA tokens as fees, the value of the --amount param is denomited in ZETA tokens. A fraction of the amount will be deducted as a cross-chain fee, the rest will be sent to the recipient on the destination chain.

npx hardhat interact --contract 0xe6DE62328677C80084b07eF25637EC83A53d69E1 --network sepolia_testnet  --destination mumbai_testnet --amount 3

🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1

✅ The transaction has been broadcasted to sepolia_testnet
📝 Transaction hash: 0x4996283c199fafe4c15f33a8ef6d4a41d00545b0736bac0e5a74d72fb342b4c7

Source Code

You can find the source code for the example in this tutorial here: