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 Adapter Standard
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.
- ONFT721
- ONFT721Adapter
// 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);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { ONFT721Core } from "./ONFT721Core.sol";
/**
* @title ONFT721Adapter Contract
* @dev ONFT721Adapter is a wrapper used to enable cross-chain transferring of an existing ERC721 token.
* @dev ERC721 NFTs from extensions which revert certain transactions, such as ones from blocked wallets or soulbound
* @dev tokens, may still be bridgeable.
*/
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;
}
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 install the ONFT package to an existing project:
- npm
- yarn
- pnpm
- forge
npm install @layerzerolabs/onft-evm
yarn add @layerzerolabs/onft-evm
pnpm add @layerzerolabs/onft-evm
forge install https://github.com/LayerZero-Labs/devtools
forge install https://github.com/LayerZero-Labs/layerzero-v2
Then add to your foundry.toml
under [profile.default]
:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
'@layerzerolabs/onft-evm/=lib/devtools/packages/onft-evm/',
'@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/',
'@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol',
]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
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",
}
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
Deploy the
ONFT
to all the chains you want to connect.Since
ONFT
extendsOApp
, callONFT.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)));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 respectiveSendLibrary
andReceiveLibrary
:// 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.
dangerThese 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();
}
}(Recommended) The ONFT inherits
OAppOptionsType3
, meaning you can enforce specific gas settings when users callaONFT.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);Required only for
ONFTAdapter
: Approve yourONFTAdapter
as a spender of yourERC721
token for the token amount you want to transfer by callingERC20.approve
. This comes standard in theERC721
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.