Build
Universal Assets
Universal Token

Universal Tokens are fully interoperable ERC-20 tokens that can be minted and transferred across any connected chain without wrapping or bridging. Each token retains its supply and metadata across chains, enabling true chain-agnostic fungibility and seamless use in multichain DeFi, payments, and governance systems.

Universal Tokens on ZetaChain are built on the standard OpenZeppelin ERC-20 (opens in a new tab) implementation and use UUPS upgradeable (opens in a new tab) proxy patterns, allowing developers to extend and upgrade token logic safely over time.

Create a new Universal Token project:

npx zetachain@latest new --project token

Install dependencies:

cd token
yarn
forge soldeer update

Compile contracts:

forge build

You can upgrade your existing ERC-20 project to become a Universal Token by installing the official standard contracts package:

yarn add @zetachain/standard-contracts

Then, update your contract using the example implementation (opens in a new tab) as a reference—see the commented lines that include Universal Token-specific logic for ZetaChain integration.

This allows your token to support cross-chain minting, transfers, and persistent supply tracking across ZetaChain and connected EVM chains.

RPC_ETHEREUM=$(zetachain q chains show --chain-id 11155111 -f rpc)
RPC_BASE=$(zetachain q chains show --chain-id 84532 -f rpc)
RPC_ZETACHAIN=$(zetachain q chains show --chain-id 7001 -f rpc)

ZRC20_ETHEREUM=$(zetachain q tokens show -s ETH.ETHSEP -f zrc20)
ZRC20_BASE=$(zetachain q tokens show -s ETH.BASESEP	-f zrc20)

GATEWAY_ETHEREUM=0x0c487a766110c85d301d96e33579c5b317fa4995
GATEWAY_BASE=0x0c487a766110c85d301d96e33579c5b317fa4995
GATEWAY_ZETACHAIN=0x6c533f7fe93fae114d0954697069df33c9b74fd7
UNISWAP_ROUTER=0x2ca7d64A7EFE2D62A725E2B35Cf7230D6677FfEe

GAS_LIMIT=1000000
PRIVATE_KEY=...

Deploy contracts on ZetaChain, Base and Ethereum.

ZETACHAIN_TOKEN=$(npx tsx commands deploy \
  --rpc $RPC_ZETACHAIN \
  --private-key $PRIVATE_KEY \
  --name ZetaChainUniversalToken \
  --uniswap-router $UNISWAP_ROUTER \
  --gateway $GATEWAY_ZETACHAIN \
  --gas-limit $GAS_LIMIT | jq -r .contractAddress) && echo $ZETACHAIN_TOKEN
BASE_TOKEN=$(npx tsx commands deploy \
  --rpc $RPC_BASE \
  --private-key $PRIVATE_KEY \
  --name EVMUniversalToken \
  --gateway $GATEWAY_BASE \
  --gas-limit $GAS_LIMIT | jq -r .contractAddress) && echo $BASE_TOKEN
ETHEREUM_TOKEN=$(npx tsx commands deploy \
  --rpc $RPC_ETHEREUM \
  --private-key $PRIVATE_KEY \
  --name EVMUniversalToken \
  --gateway $GATEWAY_ETHEREUM \
  --gas-limit $GAS_LIMIT | jq -r .contractAddress) && echo $ETHEREUM_TOKEN

Connect Contracts

After deployment, link the contracts so they can trust each other for cross-chain communication. Use setConnected on ZetaChain to register Connected contracts by their ZRC-20 gas token (used to identify the chain):

cast send $ZETACHAIN_TOKEN 'setConnected(address,bytes)' $ZRC20_BASE $BASE_TOKEN --rpc-url $RPC_ZETACHAIN --private-key $PRIVATE_KEY
cast send $ZETACHAIN_TOKEN 'setConnected(address,bytes)' $ZRC20_ETHEREUM $ETHEREUM_TOKEN --rpc-url $RPC_ZETACHAIN --private-key $PRIVATE_KEY

Then, on each connected chain, use setUniversal to point back to the Universal contract on ZetaChain:

cast send $BASE_TOKEN 'setUniversal(address)' $ZETACHAIN_TOKEN --rpc-url $RPC_BASE --private-key $PRIVATE_KEY
cast send $ETHEREUM_TOKEN 'setUniversal(address)' $ZETACHAIN_TOKEN --rpc-url $RPC_ETHEREUM --private-key $PRIVATE_KEY

This ensures only authorized contracts can send and receive token transfers across chains.

Mint on ZetaChain

npx tsx commands mint \
  --rpc $RPC_ZETACHAIN \
  --private-key $PRIVATE_KEY \
  --contract $ZETACHAIN_TOKEN \
  --amount 10 | jq -r .mintTransactionHash

https://zetachain-testnet.blockscout.com/tx/0xbe550cb123e34c4bddf3b273e854837ea6f727e527bf80a752870c3f8bff0ef1 (opens in a new tab)

Transfer from ZetaChain to Base

Transfer the token from ZetaChain to Base. Gas amount (specified in ZETA) is an estimate. Unused tokens are refunded to the user.

Use ZRC-20 Base ETH as the destination address to specify the chain to which the tokens will be transferred.

npx tsx commands transfer \
  --rpc $RPC_ZETACHAIN \
  --private-key $PRIVATE_KEY \
  --from $ZETACHAIN_TOKEN \
  --destination $ZRC20_BASE \
  --amount 10 \
  --gas-amount 5 | jq -r .transferTransactionHash
zetachain q cctx --hash 0x2ced374831b7612f4f2df98f2d1f30b2fa797ddcc62df0a5883b402b9310fe7a
7001 → 84532 ✅ OutboundMined
CCTX:     0x88ac99d5ce593af62f5e56e2cdeb14797e23fb890933f50d74f1c8944b91b991
Tx Hash:  0x2ced374831b7612f4f2df98f2d1f30b2fa797ddcc62df0a5883b402b9310fe7a (on chain 7001)
Tx Hash:  0x4c0dbfda09e0364a54be7f812a49c677739f0c3a9bc6d2e6bf0c8d5ea4a7903d (on chain 84532)
Sender:   0xE3CA615E4Bd2b106ff51e88A04Ec39A2Afc75212
Receiver: 0x449777033Ff53aD3B4F70C17c31110476E61D2A8
Message:  0000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba32000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba3200000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000

Transfer from Base to Ethereum

Let’s move the token again — this time from Base to Ethereum.

npx tsx commands transfer \
  --rpc $RPC_BASE \
  --private-key $PRIVATE_KEY \
  --from $BASE_TOKEN \
  --destination $ZRC20_ETHEREUM \
  --amount 10 \
  --gas-amount 0.005 | jq -r .transferTransactionHash
zetachain q cctx --hash 0x8db12522169485a44ba8490d53f734ac6b7f5da72a6decbe3ae462198a960cee
84532 → 7001 ✅ OutboundMined
CCTX:     0x39d970fc742519c87e67733ffd13315dce907c07e65cf97ba4ffba2e2bf2ceed
Tx Hash:  0x8db12522169485a44ba8490d53f734ac6b7f5da72a6decbe3ae462198a960cee (on chain 84532)
Tx Hash:  0x180f7fecd37f9e097ae5372ec729502933aa15cd6ec078af1efb911370fb33ea (on chain 7001)
Sender:   0x449777033Ff53aD3B4F70C17c31110476E61D2A8
Receiver: 0xE3CA615E4Bd2b106ff51e88A04Ec39A2Afc75212
Message:  00000000000000000000000005ba149a7bd6dc1f937fa9046a9e05c05f3b18b00000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba32000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba3200000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000
Amount:   5000000000000000 Gas tokens

7001 → 11155111 ✅ PendingOutbound (transaction broadcasted to target chain)
CCTX:     0xbbc4e169decc986556b079445dc5055725dff65e9ee66a48613374c1b4e64eda
Tx Hash:  0x39d970fc742519c87e67733ffd13315dce907c07e65cf97ba4ffba2e2bf2ceed (on chain 7001)
Tx Hash:  0x5f973954ab6c6745e770e02f4697309b3e18be60fc53764c7b6deec07cd5fe4b (on chain 11155111)
Sender:   0xE3CA615E4Bd2b106ff51e88A04Ec39A2Afc75212
Receiver: 0xBc1eE0E9452eC2E809FC2dBD0000A7D6095fDfC2
Message:  0000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba32000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000006ba661563e1c0000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba3200000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000
Amount:   118362341785116 Gas tokens
Status:   PendingOutbound, initiating outbound

https://github.com/zeta-chain/example-contracts/tree/main/examples/token (opens in a new tab)