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
yarn
Universal 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);
error Unauthorized();
modifier onlyGateway() {
if (msg.sender != address(gateway)) revert Unauthorized();
_;
}
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
: AMessageContext
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.
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 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
Make 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"]' 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.
Option 2: Deploy on Testnet
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
You can track the progress of a cross-chain transaction using ZetaChain's API: