Build
Universal Assets
Universal NFT

Universal NFTs are fully interoperable ERC-721 tokens that can be minted and transferred across any connected chain without wrapping or bridging. Each NFT has a persistent token ID that remains the same on every chain, and metadata is preserved during cross-chain transfers. This enables true chain-agnostic ownership and interaction for use cases like cross-chain games, marketplaces, and identity.

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

Create a new Universal NFT project:

npx zetachain@latest new --project nft

Install dependencies:

cd nft
yarn
forge soldeer update

Compile contracts:

forge build

You can upgrade your existing ERC-721 project to become a Universal NFT 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 NFT-specific logic for ZetaChain integration.

This allows your NFT to support cross-chain minting, transfers, and persistent token IDs 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.

NFT_ZETACHAIN=$(npx tsx commands deploy \
  --rpc $RPC_ZETACHAIN \
  --private-key $PRIVATE_KEY \
  --name ZetaChainUniversalNFT \
  --uniswap-router $UNISWAP_ROUTER \
  --gateway $GATEWAY_ZETACHAIN \
  --gas-limit $GAS_LIMIT | jq -r .contractAddress) && echo $NFT_ZETACHAIN
0x6335bAB2eF31B79eE01dCFDB656a1eEf5ACd0840
NFT_BASE=$(npx tsx commands deploy \
  --rpc $RPC_BASE \
  --private-key $PRIVATE_KEY \
  --name EVMUniversalNFT \
  --gateway $GATEWAY_BASE \
  --gas-limit $GAS_LIMIT | jq -r .contractAddress) && echo $NFT_BASE
0xB7c73Ee9B4E65458C972d64bbfAe653d0E6F389A
NFT_ETHEREUM=$(npx tsx commands deploy \
  --rpc $RPC_ETHEREUM \
  --private-key $PRIVATE_KEY \
  --name EVMUniversalNFT \
  --gateway $GATEWAY_ETHEREUM \
  --gas-limit $GAS_LIMIT | jq -r .contractAddress) && echo $NFT_ETHEREUM
0x166d406a3049C04bF884a4C8cfe99c5bdCebC928

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 $NFT_ZETACHAIN 'setConnected(address,bytes)' $ZRC20_BASE $NFT_BASE --rpc-url $RPC_ZETACHAIN --private-key $PRIVATE_KEY
cast send $NFT_ZETACHAIN 'setConnected(address,bytes)' $ZRC20_ETHEREUM $NFT_ETHEREUM --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 $NFT_BASE 'setUniversal(address)' $NFT_ZETACHAIN --rpc-url $RPC_BASE --private-key $PRIVATE_KEY
cast send $NFT_ETHEREUM 'setUniversal(address)' $NFT_ZETACHAIN --rpc-url $RPC_ETHEREUM --private-key $PRIVATE_KEY

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

Mint on ZetaChain

TOKEN_ID=$(npx tsx commands mint \
  --rpc $RPC_ZETACHAIN \
  --private-key $PRIVATE_KEY \
  --contract $NFT_ZETACHAIN \
  --token-uri https://example.com/nft/metadata/1 | jq -r .tokenId) && echo $TOKEN_ID

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 NFT will be transferred.

npx tsx commands transfer \
  --rpc $RPC_ZETACHAIN \
  --private-key $PRIVATE_KEY \
  --contract $NFT_ZETACHAIN \
  --token-id $TOKEN_ID \
  --destination $ZRC20_BASE \
  --gas-amount 5 | jq -r .transferTransactionHash
0xc1d363c8fddd21add48580c52550248f969a9a3b65d1377440cefe7fa8631db8

Outgoing cross-chain transaction from ZetaChain to Base:

zetachain q cctx --hash 0xc1d363c8fddd21add48580c52550248f969a9a3b65d1377440cefe7fa8631db8
7001 → 84532 ✅ OutboundMined
CCTX:     0x8066aaf467afb79d5c969b3389f958d566637ad690410b7538706d20d6eeceac
Tx Hash:  0xc1d363c8fddd21add48580c52550248f969a9a3b65d1377440cefe7fa8631db8 (on chain 7001)
Tx Hash:  0xe2c966b67fdc6759669cebc021466c408449f5c2c19954ffdb6e73d714ec29bc (on chain 84532)
Sender:   0x6335bAB2eF31B79eE01dCFDB656a1eEf5ACd0840
Receiver: 0xB7c73Ee9B4E65458C972d64bbfAe653d0E6F389A
Message:  0000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba32000000000000000000000000a6ca386bc8dc2baad9c67208f0129de2244bda1b00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba320000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000002268747470733a2f2f6578616d706c652e636f6d2f6e66742f6d657461646174612f310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Transfer from Base to Ethereum

Let’s move the NFT again — this time from Base to Ethereum. You’ll reference the same token ID, which remains unchanged.

npx tsx commands transfer \
  --rpc $RPC_BASE \
  --private-key $PRIVATE_KEY \
  --contract $NFT_BASE \
  --token-id $TOKEN_ID \
  --destination $ZRC20_ETHEREUM \
  --gas-amount 0.05 | jq -r .transferTransactionHash
0x4aa957b2678232fb3f09557e8fcee22d5503272b6d6b8001677f32cbcdfa30dc
zetachain q cctx --hash 0x4aa957b2678232fb3f09557e8fcee22d5503272b6d6b8001677f32cbcdfa30dc
84532 → 7001 ✅ OutboundMined
CCTX:     0xa54aea97073e6f76bf28c46290b0544b80aa1e85702284aacbedf03d5f59c322
Tx Hash:  0x4aa957b2678232fb3f09557e8fcee22d5503272b6d6b8001677f32cbcdfa30dc (on chain 84532)
Tx Hash:  0xe8d8432917cc0cb98f60285c711608003859924f3dc962c648db23232a36dcbb (on chain 7001)
Sender:   0xB7c73Ee9B4E65458C972d64bbfAe653d0E6F389A
Receiver: 0x6335bAB2eF31B79eE01dCFDB656a1eEf5ACd0840
Message:  00000000000000000000000005ba149a7bd6dc1f937fa9046a9e05c05f3b18b00000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba32000000000000000000000000a6ca386bc8dc2baad9c67208f0129de2244bda1b00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba320000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000002268747470733a2f2f6578616d706c652e636f6d2f6e66742f6d657461646174612f310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Amount:   50000000000000000 Gas tokens

7001 → 11155111 ✅ OutboundMined
CCTX:     0x34656cbd3210f3e33edb6d261bfdbc800476baa85f40fc15b75f934efbbd30c9
Tx Hash:  0xa54aea97073e6f76bf28c46290b0544b80aa1e85702284aacbedf03d5f59c322 (on chain 7001)
Tx Hash:  0xe7c0d400bd94e5c84cb44752d1e9e05e05f73f0433f6e915e77cc7fdde47d8fd (on chain 11155111)
Sender:   0x6335bAB2eF31B79eE01dCFDB656a1eEf5ACd0840
Receiver: 0x166d406a3049C04bF884a4C8cfe99c5bdCebC928
Message:  0000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba32000000000000000000000000a6ca386bc8dc2baad9c67208f0129de2244bda1b00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000e22dfad4b38420000000000000000000000004955a3f38ff86ae92a914445099caa8ea2b9ba320000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000002268747470733a2f2f6578616d706c652e636f6d2f6e66742f6d657461646174612f310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Amount:   3978993754388546 Gas tokens

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