Ready to Build Multi-Chain NFTs?
Get expert guidance on implementing cross-chain NFT collections.
Complete Guide
Quick Answer
Multi-chain NFT collections use either lock-and-mint (NFT locked on source chain, bridged version minted on destination) or burn-and-mint/ONFT (NFT burned on source, minted on destination). LayerZero ONFT provides a turnkey solution for cross-chain NFTs. With lock-and-mint, the source chain holds the canonical NFT; with ONFT, the NFT exists on exactly one chain at any time — both approaches maintain 1 NFT = 1 token across all chains.
Approach 1: Lock-and-Mint Bridge
// Source chain: lock NFT
// Destination chain: mint a "bridged" version
contract NFTBridge {
mapping(uint256 => address) public lockedTokenOwner; // tokenId => original owner
// Lock NFT to bridge to another chain
function lockAndBridge(uint256 tokenId, uint256 destinationChainId) external {
require(nft.ownerOf(tokenId) == msg.sender, "Not owner");
nft.transferFrom(msg.sender, address(this), tokenId);
lockedTokenOwner[tokenId] = msg.sender;
// Send message to destination chain via LayerZero
bytes memory payload = abi.encode(msg.sender, tokenId);
endpoint.send(
destinationChainId,
remoteAddress,
payload,
msg.sender, // Refund address
address(0), // ZRO payment
bytes("") // Adapter params
);
emit NFTBridged(tokenId, msg.sender, destinationChainId);
}
// Release when bridged back
function release(uint256 tokenId, address recipient) external onlyEndpoint {
require(lockedTokenOwner[tokenId] != address(0), "Not locked");
delete lockedTokenOwner[tokenId];
nft.transferFrom(address(this), recipient, tokenId);
emit NFTReleased(tokenId, recipient);
}
event NFTBridged(uint256 tokenId, address owner, uint256 destinationChain);
event NFTReleased(uint256 tokenId, address recipient);
}Approach 2: Cross-Chain NFT (LayerZero ONFT)
// ONFT: same NFT exists on multiple chains
// When transferred cross-chain: burned on source, minted on destination
import {ONFT721} from "@layerzerolabs/solidity-examples/contracts/token/onft/ONFT721.sol";
contract MyCrossChainNFT is ONFT721 {
uint256 public nextTokenId = 1;
constructor(address _lzEndpoint)
ONFT721("MyCrossChainNFT", "MCCNFT", 200, _lzEndpoint)
{}
function mint(address to) external onlyOwner {
_safeMint(to, nextTokenId++);
}
// Inherited from ONFT721: sendFrom() handles cross-chain transfer
// When sendFrom() is called: NFT burned on this chain, minted on destination
}
// Frontend: send NFT from Ethereum to Polygon
async function bridgeNFT(tokenId, destinationChainId) {
const nft = new ethers.Contract(NFT_ADDRESS, ONFT_ABI, signer);
// Estimate fee for cross-chain transfer
const [nativeFee] = await nft.estimateSendFee(
destinationChainId,
ethers.zeroPadValue(recipientAddress, 32),
tokenId,
false,
"0x"
);
await nft.sendFrom(
ownerAddress,
destinationChainId,
ethers.zeroPadValue(recipientAddress, 32),
tokenId,
ownerAddress, // Refund address
ethers.ZeroAddress,
"0x",
{ value: nativeFee }
);
// NFT now exists on Polygon, not Ethereum
}