Skip to main content
Version: Endpoint V2 Docs

LayerZero V2 ONFT Quickstart

The Omnichain Non-Fungible Token (ONFT) Standard allows non-fungible tokens to be transferred across multiple blockchains without asset wrapping or middlechains.

This standard works by burning tokens on the source chain whenever an omnichain transfer is initiated, sending a message via the protocol and delivering a function call to the destination contract to mint the same number of tokens burned, creating a unified supply across all networks LayerZero supports.

ONFT Standard

ONFT Example ONFT Example

ONFT Adapter Standard

ONFT Adapter Example ONFT Adapter Example

Using this design pattern, LayerZero can extend any non-fungible token to interoperate with other chains. The most widely used of these standards is ONFT721.sol, an extension of the OApp Contract Standard and the ERC721 Standard.

// 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;
}

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);
}
}

Installation

To start using the ONFT721 and ONFT721Adapter contracts, you can install the ONFT package to an existing project:

npm install @layerzerolabs/onft-evm
info

LayerZero contracts work with both OpenZeppelin V5 and V4 contracts. Specify your desired version in your project's package.json:

"resolutions": {
"@openzeppelin/contracts": "^5.0.1",
}
tip

LayerZero also provides create-lz-oapp, an npx package that allows developers to create any omnichain application in <4 minutes! Get started by running the following from your command line:

npx create-lz-oapp@latest

Deployment Workflow

  1. Deploy the ONFT to all the chains you want to connect.

  2. Since ONFT extends OApp, call ONFT.setPeer to whitelist each destination contract on every destination chain.

    // The real endpoint ids will vary per chain, and can be found under "Supported Chains"
    uint32 aEid = 1;
    uint32 bEid = 2;

    MyONFT aONFT;
    MyONFT bONFT;

    function addressToBytes32(address _addr) public pure returns (bytes32) {
    return bytes32(uint256(uint160(_addr)));
    }

    // Call on both sides per pathway
    aONFT.setPeer(bEid, addressToBytes32(address(bONFT)));
    bONFT.setPeer(aEid, addressToBytes32(address(aONFT)));
  3. Set the DVN configuration, including optional settings such as block confirmations, security threshold, the Executor, max message size, and send/receive libraries.

    EndpointV2.setSendLibrary(aONFT, bEid, newLib)
    EndpointV2.setReceiveLibrary(aONFT, bEid, newLib, gracePeriod)
    EndpointV2.setReceiveLibraryTimeout(aONFT, bEid, lib, gracePeriod)
    EndpointV2.setConfig(aONFT, sendLibrary, sendConfig)
    EndpointV2.setConfig(aONFT, receiveLibrary, receiveConfig)
    EndpointV2.setDelegate(delegate)

    These custom configurations will be stored on-chain as part of EndpointV2, along with your respective SendLibrary and ReceiveLibrary:

    // LayerZero V2 MessageLibManager.sol (part of EndpointV2.sol)
    mapping(address sender => mapping(uint32 dstEid => address lib)) internal sendLibrary;
    mapping(address receiver => mapping(uint32 srcEid => address lib)) internal receiveLibrary;
    mapping(address receiver => mapping(uint32 srcEid => Timeout)) public receiveLibraryTimeout;
    // LayerZero V2 SendLibBase.sol (part of SendUln302.sol)
    mapping(address oapp => mapping(uint32 eid => ExecutorConfig)) public executorConfigs;
    // LayerZero V2 UlnBase.sol (both in SendUln302.sol and ReceiveUln302.sol)
    mapping(address oapp => mapping(uint32 eid => UlnConfig)) internal ulnConfigs;
    // LayerZero V2 EndpointV2.sol
    mapping(address oapp => address delegate) public delegates;

    You can find example scripts to make these calls under Security and Executor Configuration.

    danger

    These configurations control the verification mechanisms of messages sent between your OApps. You should review the above settings carefully.

    If no configuration is set, the configuration will fallback to the default configurations set by LayerZero Labs. For example:

    /// @notice The Send Library is the Oapp specified library that will be used to send the message to the destination
    /// endpoint. If the Oapp does not specify a Send Library, the default Send Library will be used.
    /// @dev If the Oapp does not have a selected Send Library, this function will resolve to the default library
    /// configured by LayerZero
    /// @return lib address of the Send Library
    /// @param _sender The address of the Oapp that is sending the message
    /// @param _dstEid The destination endpoint id
    function getSendLibrary(address _sender, uint32 _dstEid) public view returns (address lib) {
    lib = sendLibrary[_sender][_dstEid];
    if (lib == DEFAULT_LIB) {
    lib = defaultSendLibrary[_dstEid];
    if (lib == address(0x0)) revert Errors.LZ_DefaultSendLibUnavailable();
    }
    }

  4. (Recommended) The ONFT inherits OAppOptionsType3, meaning you can enforce specific gas settings when users call aONFT.send.

    EnforcedOptionParam[] memory aEnforcedOptions = new EnforcedOptionParam[](1);
    // Send gas for lzReceive (A -> B).
    aEnforcedOptions[0] = EnforcedOptionParam({eid: bEid, msgType: SEND, options: OptionsBuilder.newOptions().addExecutorLzReceiveOption(50000, 0)}); // gas limit, msg.value
    aONFT.setEnforcedOptions(aEnforcedOptions);
  5. Required only for ONFTAdapter: Approve your ONFTAdapter as a spender of your ERC721 token for the token amount you want to transfer by calling ERC20.approve. This comes standard in the ERC721 interface, and is required when using an intermediary contract to spend token amounts on behalf of the caller. See more details about each setting below.