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
 
Set Up Your Environment
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
yarnUniversal Contract
A universal app is a contract that inherits from UniversalContract interface.
// 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: AMessageContextstruct 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.
Options 1: Deploy on Localnet
Localnet is a local development environment, which simulates the behavior of Gateways deployed on multiple EVM blockchains. Start localnet:
npx hardhat localnetnpx hardhat compile --forceDeploy 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: 0xc351628EB244ec633d5f21fBD6621e1a683B1181Make a Call to the Universal App
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"]' alicePass 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.
Option 2: Deploy on Testnet
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: 0x11998e1A5D2e770753263376ceE78B14c9617f16Make 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"]' aliceTransaction hash: 0x3fbed0a3dc48eda00212aff6e930940d84b142f7da316b9186f0c68edd0793b2You can track the progress of a cross-chain transaction using ZetaChain's API: