Build
Tutorials
Message Passing

By the end of this tutorial, you will have learned how to:

  • Build a universal app that emits an event when called from a connected chain
  • Deploy the universal app on localnet
  • Use the Gateway on a connected chain to call a universal app

Start by cloning the example contracts repository and installing the necessary dependencies:

git clone https://github.com/zeta-chain/example-contracts
cd example-contracts/examples/hello
yarn

A universal app is a contract that inherits from UniversalContract interface.

contracts/Universal.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
 
import "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol";
 
contract Universal is UniversalContract {
    GatewayZEVM public immutable gateway;
 
    event HelloEvent(string, string);
 
    modifier onlyGateway() {
        require(msg.sender == address(gateway), "Caller is not the gateway");
        _;
    }
 
    constructor(address payable gatewayAddress) {
        gateway = GatewayZEVM(gatewayAddress);
    }
 
    function onCall(
        MessageContext calldata context,
        address zrc20,
        uint256 amount,
        bytes calldata message
    ) external override onlyGateway {
        string memory name = abi.decode(message, (string));
        emit HelloEvent("Hello: ", name);
    }
}

A universal contract must implement the onCall function. onCall is a function that gets triggered when the contract receives a call from a connected chain through the Gateway. This function processes the incoming data, which includes:

  • context: A MessageContext struct containing:
    • chainID: The integer ID of the connected chain from which the cross-chain call originated.
    • sender: The address (EOA or contract) that initiated the gateway call on the connected chain.
    • origin: deprecated.
  • zrc20: The address of the ZRC-20 token representing the asset from the source chain.
  • amount: The number of tokens transferred.
  • message: The encoded data payload.

In this example, onCall simply decodes the message into a variable and emits an event.

onCall should only be called by the Gateway to ensure that it is only called as a response to a call on a connected chain and that you can trust the values of the function parameters.

Localnet is a local development environment, which simulates the behavior of Gateways deployed on multiple EVM blockchains. Start localnet:

npx hardhat localnet
npx hardhat compile --force

Deploy the universal contract and pass ZetaChain's Gateway address to the constructor. You can find the Gateway address in the output of Localnet.

npx hardhat deploy --network localhost --gateway 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707
🚀 Successfully deployed "Universal" contract on localhost.
📜 Contract address: 0xc351628EB244ec633d5f21fBD6621e1a683B1181

To call the universal app deployed on ZetaChain from a connected chain, make a call to the Gateway contract on a connected EVM chain using the evm-call task:

npx hardhat evm-call \
  --network localhost \
  --gateway-evm 0x610178dA211FEF7D417bC0e6FeD39F05609AD788 \
  --receiver 0xc351628EB244ec633d5f21fBD6621e1a683B1181 \
  --types '["string"]' alice

Pass the EVM Gateway Address from the Localnet's table of addresses and the universal contract address as the receiver. A universal app expects to receive a single string in the message, so pass the appropriate type and value to the command.

After the transaction is processed you will see an [ZetaChain]: Event from onCall message in the terminal where Localnet is running.

Deploy the contract to ZetaChain's testnet using the Gateway address from Contract Addresses page.

npx hardhat deploy --network zeta_testnet --gateway 0x6c533f7fe93fae114d0954697069df33c9b74fd7
🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32

🚀 Successfully deployed "Universal" contract on zeta_testnet.
📜 Contract address: 0x11998e1A5D2e770753263376ceE78B14c9617f16

Make a transaction to the Gateway on the Base Sepolia testnet to make a cross-chain call to the universal app on ZetaChain. Make sure to use Gateway address on Base Sepolia.

npx hardhat evm-call \
  --network base_sepolia \
  --gateway-evm 0x0c487a766110c85d301d96e33579c5b317fa4995 \
  --receiver 0x11998e1A5D2e770753263376ceE78B14c9617f16 \
  --types '["string"]' alice
Transaction hash: 0x3fbed0a3dc48eda00212aff6e930940d84b142f7da316b9186f0c68edd0793b2

https://sepolia.basescan.org/tx/0x3fbed0a3dc48eda00212aff6e930940d84b142f7da316b9186f0c68edd0793b2 (opens in a new tab)

You can track the progress of a cross-chain transaction using ZetaChain's API:

https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/crosschain/inboundHashToCctxData/0x3fbed0a3dc48eda00212aff6e930940d84b142f7da316b9186f0c68edd0793b2 (opens in a new tab)