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.
Option 1: Create a New Universal NFT
Create a new Universal NFT project:
npx zetachain@latest new --project nft
Install dependencies:
cd nft
yarn
forge soldeer update
Compile contracts:
forge build
Option 2: Upgrade an Existing ERC-721 Project
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.
Deploy on Testnet
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
Source Code
https://github.com/zeta-chain/example-contracts/tree/main/examples/nft (opens in a new tab)