Cross-Chain Message
In this tutorial you will learn how to create a contract capable of sending a message with arbitrary data between contracts on connected chains using cross-chain messaging.
Set up your environment
git clone https://github.com/zeta-chain/template
cd template
yarn
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 CrossChainMessage message:string
The messaging
task accepts one or more arguments: the name of the contract and
a list of arguments (optionally with types). The arguments define the contents
of the message that will be sent across chains.
In the example above the message will have only one field: message
of type
string
. If the type is not specified it is assumed to be string
.
The optional --fees
flag is missing, which means this contract will accept
native gas tokens as input and swap it for ZETA.
Cross-Chain Messaging Contract
Let's review 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";
contract CrossChainMessage is ZetaInteractor, ZetaReceiver {
event CrossChainMessageEvent(string);
event CrossChainMessageRevertedEvent(string);
ZetaTokenConsumer private immutable _zetaConsumer;
IERC20 internal immutable _zetaToken;
constructor(address connectorAddress, address zetaTokenAddress, address zetaConsumerAddress) ZetaInteractor(connectorAddress) {
_zetaToken = IERC20(zetaTokenAddress);
_zetaConsumer = ZetaTokenConsumer(zetaConsumerAddress);
}
function sendMessage(uint256 destinationChainId, string memory message) external payable {
if (!_isValidChainId(destinationChainId))
revert InvalidDestinationChainId();
uint256 crossChainGas = 2 * (10 ** 18);
uint256 zetaValueAndGas = _zetaConsumer.getZetaFromEth{
value: msg.value
}(address(this), crossChainGas);
_zetaToken.approve(address(connector), zetaValueAndGas);
connector.send(
ZetaInterfaces.SendInput({
destinationChainId: destinationChainId,
destinationAddress: interactorsByChainId[destinationChainId],
destinationGasLimit: 300000,
message: abi.encode(message),
zetaValueAndGas: zetaValueAndGas,
zetaParams: abi.encode("")
})
);
}
function onZetaMessage(
ZetaInterfaces.ZetaMessage calldata zetaMessage
) external override isValidMessageCall(zetaMessage) {
(string memory message ) = abi.decode(
zetaMessage.message, (string)
);
emit CrossChainMessageEvent(message);
}
function onZetaRevert(
ZetaInterfaces.ZetaRevert calldata zetaRevert
) external override isValidRevertCall(zetaRevert) {
(string memory message) = abi.decode(
zetaRevert.message,
(string)
);
emit CrossChainMessageRevertedEvent(message);
}
}
The contract:
- inherits from
ZetaInteractor
(opens in a new tab), which provides two modifiers that are used to validate the message and revert calls:isValidMessageCall
andisValidRevertCall
. - implements
ZetaReceiver
(opens in a new tab), which defines two functions:onZetaMessage
andonZetaRevert
.
State Variables:
_zetaConsumer
: a private immutable state variable that stores the address ofZetaTokenConsumer
, which is used amond other things for getting ZETA tokens from native tokens to pay for gas when sending a message._zetaToken
: an internal immutable state variable that stores the address of the ZETA token contract.
The contract defines two events: CrossChainMessageEvent
emitted when a message
is processed on the destination chain and CrossChainMessageRevertedEvent
emitted when a message is reverted on the destination chain.
The constructor passes connectorAddress
to the ZetaInteractor
constructor
and initializes both _zetaToken
and _zetaConsumer
state variables.
The sendMessage
function is used to send a message to a recipient contract on
the destination chain. It first checks that the destination chain ID is valid.
Then it uses ZETA consumer to get the needed amount of ZETA tokens from the
provided msg.value
(amount of native gas assets sent with the function call),
and approves the ZetaConnector
to spend the zetaValueAndGas
amount of ZETA
tokens.
The sendMessage
function uses connector.send
to send a crosss-chain message
with the following arguments wrapped in a struct:
destinationChainId
: chain ID of the destination chaindestinationAddress
: address of the contract receiving the message on the destination chain (expressed in bytes since it can be non-EVM)destinationGasLimit
: gas limit for the destination chain's transactionmessage
: arbitrary message to be parsed by the receiving contract on the destination chainzetaValueAndGas
: amount of ZETA tokens to be sent to the destination chain, ZetaChain gas fees, and destination chain gas fees (expressed in ZETA tokens)zetaParams
: optional ZetaChain parameters.
The onZetaMessage
function processes incoming cross-chain messages. The
function uses abi.decode
to decode the contents of the message
. After
decoding the functions emits CrossChainMessageEvent
event with the message
content.
The onZetaRevert
function handles the reverts of cross-chain messages. This
function is triggered on the source chain if the message passing failed.
Both onZetaMessage
and onZetaRevert
use the isValidRevertCall
modifier to
ensure that the revert message is genuine and originates from the trusted
source.
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,bsc_testnet
🚀 Successfully deployed contract on bsc_testnet
📜 Contract address: 0x4036009aa206a5c4d3bDABaC7242b18ACc5655D5
🚀 Successfully deployed contract on sepolia_testnet
📜 Contract address: 0x5d5c88B669337686af75f97C817365164786C88a
🔗 Setting interactors for a contract on bsc_testnet
✅ Interactor address for 11155111 (sepolia_testnet) is set to 0x5d5c88b669337686af75f97c817365164786c88a
🔗 Setting interactors for a contract on sepolia_testnet
✅ Interactor address for 97 (bsc_testnet) is set to 0x4036009aa206a5c4d3bdabac7242b18acc5655d5
Send a Message
Send a message from Sepolia to BSC testnet using the contract address (see the
output of the deploy
task). Make sure to submit enough native gas tokens with
--amount
to pay for the transaction fees.
npx hardhat interact --message hello --contract 0x5d5c88B669337686af75f97C817365164786C88a --network sepolia_testnet --amount 0.01 --destination bsc_testnet
🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
✅ The transaction has been broadcasted to sepolia_testnet
📝 Transaction hash: 0x798f15cc214e8d5d595ba1944099134330439a4a30b76cc791e1ca60cd85d696
You can check the broadcasted transaction on Sepolia's Etherscan:
Next, you can track the progress of the cross-chain transaction:
npx hardhat cctx 0x798f15cc214e8d5d595ba1944099134330439a4a30b76cc791e1ca60cd85d696
✓ CCTXs on ZetaChain found.
✓ 0x9f3dfff6b1373a6717ce6a0e20d3e6e1591cf13e75634f385550d3e1a226c604: 11155111 → 97: OutboundMined
Source Code
You can find the source code for the example in this tutorial here:
https://github.com/zeta-chain/example-contracts/tree/main/messaging/message (opens in a new tab)