Build
Tutorials
Message Passing

In this tutorial you will create a simple universal app deployed on ZetaChain, which emits when called from a connected chain.

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

  • Build a simple universal app
  • 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);
    }
}

The constructor accepts the ZetaChain's Gateway address and saves it into a state variable. Gateway is used to make outgoing contract calls and token withdrawals.

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.

Before proceeding, you might want to check out the Testnet Setup guide to learn how to set up an account and request testnet tokens.

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)