LayerZero V2 ONFT Quickstart
The Omnichain Non-Fungible Token (ONFT) Standard allows non-fungible tokens (NFTs) to be transferred across multiple blockchains without asset wrapping or middlechains.
-
ONFT Contract: Uses a burn-and-mint mechanism. For a fluid NFT that can move directly between chains (e.g. Chain A and Chain B), you must deploy an ONFT contract on every chain. This creates a "mesh" of interconnected contracts.
-
ONFT Adapter: Uses a lock-and-mint mechanism. If you already have an NFT collection on one chain and want to extend it omnichain, you deploy a single ONFT Adapter on the source chain. Then, you deploy ONFT contracts on any new chains where the collection will be transferred. Note that only one ONFT Adapter is allowed in the entire mesh.
This mesh concept is central to all LayerZero implementations: it represents the network of contracts that work together to enable omnichain NFT functionality.
ONFT (Burn & Mint)
When using ONFT, tokens are burned on the source chain whenever an omnichain transfer is initiated. LayerZero sends a message to the destination contract instructing it to mint the same number of tokens that were burned, ensuring the overall token supply remains consistent.
function _debit(address _from, uint256 _tokenId, uint32 /*_dstEid*/) internal virtual override {
if (_from != ERC721.ownerOf(_tokenId)) revert OnlyNFTOwner(_from, ERC721.ownerOf(_tokenId));
_burn(_tokenId);
}
function _credit(address _to, uint256 _tokenId, uint32 /*_srcEid*/) internal virtual override {
_mint(_to, _tokenId);
}
Key Points
- Default pattern for new NFT collections.
ONFT721
extendsERC721
(OpenZeppelin) and adds cross-chain logic.- Unified supply across chains is maintained by burning on source, minting on destination.
ONFT Adapter (Lock & Mint)
When using ONFT Adapter, tokens are locked in a contract on the source chain, while the destination contract mints or unlocks the token after receiving a message from LayerZero. When bridging back, the minted token is burned on the remote side, and the original is unlocked on the source side.
function _debit(address _from, uint256 _tokenId, uint32 /*_dstEid*/) internal virtual override {
// Lock the token by transferring it to this adapter contract
innerToken.transferFrom(_from, address(this), _tokenId);
}
function _credit(address _toAddress, uint256 _tokenId, uint32 /*_srcEid*/) internal virtual override {
// Unlock the token by transferring it back to the user
innerToken.transferFrom(address(this), _toAddress, _tokenId);
}
Key Points
- Suitable for existing NFT collections.
- The adapter contract is effectively a “lockbox” for your existing ERC721 tokens.
- No changes to your original NFT contract are required. Instead, the adapter implements the cross-chain logic.
- ONFT
- ONFT Adapter
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { ONFT721Core } from "./ONFT721Core.sol";
/**
* @title ONFT721 Contract
* @dev ONFT721 is an ERC-721 token that extends the functionality of the ONFT721Core contract.
*/
abstract contract ONFT721 is ONFT721Core, ERC721 {
string internal baseTokenURI;
event BaseURISet(string baseURI);
/**
* @dev Constructor for the ONFT721 contract.
* @param _name The name of the ONFT.
* @param _symbol The symbol of the ONFT.
* @param _lzEndpoint The LayerZero endpoint address.
* @param _delegate The delegate capable of making OApp configurations inside of the endpoint.
*/
constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) ERC721(_name, _symbol) ONFT721Core(_lzEndpoint, _delegate) {}
// @notice Retrieves the address of the underlying ERC721 implementation (ie. this contract).
function token() external view returns (address) {
return address(this);
}
function setBaseURI(string calldata _baseTokenURI) external onlyOwner {
baseTokenURI = _baseTokenURI;
emit BaseURISet(baseTokenURI);
}
function _baseURI() internal view override returns (string memory) {
return baseTokenURI;
}
/**
* @notice Indicates whether the ONFT721 contract requires approval of the 'token()' to send.
* @dev In the case of ONFT where the contract IS the token, approval is NOT required.
* @return requiresApproval Needs approval of the underlying token implementation.
*/
function approvalRequired() external pure virtual returns (bool) {
return false;
}
// @dev Key cross-chain overrides
function _debit(address _from, uint256 _tokenId, uint32 /*_dstEid*/) internal virtual override {
if (_from != ERC721.ownerOf(_tokenId)) revert OnlyNFTOwner(_from, ERC721.ownerOf(_tokenId));
_burn(_tokenId);
}
function _credit(address _to, uint256 _tokenId, uint32 /*_srcEid*/) internal virtual override {
_mint(_to, _tokenId);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { ONFT721Core } from "./ONFT721Core.sol";
// @dev ONFT721Adapter is an adapter contract used to enable cross-chain transferring of an existing ERC721 token.
abstract contract ONFT721Adapter is ONFT721Core {
IERC721 internal immutable innerToken;
/**
* @dev Constructor for the ONFT721 contract.
* @param _token The underlying ERC721 token address this adapts
* @param _lzEndpoint The LayerZero endpoint address.
* @param _delegate The delegate capable of making OApp configurations inside of the endpoint.
*/
constructor(address _token, address _lzEndpoint, address _delegate) ONFT721Core(_lzEndpoint, _delegate) {
innerToken = IERC721(_token);
}
// @notice Retrieves the address of the underlying ERC721 implementation (ie. external contract).
function token() external view returns (address) {
return address(innerToken);
}
/**
* @notice Indicates whether the ONFT721 contract requires approval of the 'token()' to send.
* @dev In the case of ONFT where the contract IS the token, approval is NOT required.
* @return requiresApproval Needs approval of the underlying token implementation.
*/
function approvalRequired() external pure virtual returns (bool) {
return true;
}
// @dev Key cross-chain overrides
function _debit(address _from, uint256 _tokenId, uint32 /*_dstEid*/) internal virtual override {
// @dev Dont need to check onERC721Received() when moving into this contract, ie. no 'safeTransferFrom' required
innerToken.transferFrom(_from, address(this), _tokenId);
}
function _credit(address _toAddress, uint256 _tokenId, uint32 /*_srcEid*/) internal virtual override {
// @dev Do not need to check onERC721Received() when moving out of this contract, ie. no 'safeTransferFrom'
// required
// @dev The default implementation does not implement IERC721Receiver as 'safeTransferFrom' is not used.
// @dev If IERC721Receiver is required, ensure proper re-entrancy protection is implemented.
innerToken.transferFrom(address(this), _toAddress, _tokenId);
}
}
Installation
To start using the ONFT721
and ONFT721Adapter
contracts, you can either create a new project via the LayerZero CLI or add the contract package to an existing project: