Omnichain NFT (ONFT) development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Omnichain NFT (ONFT) development
Medium
~3-5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Omnichain NFT (ONFT) Development

An NFT tied to a single chain is an asset with limited liquidity. A collection on Ethereum has access to OpenSea and Blur, but is cut off from the Polygon, Arbitrum, Solana ecosystems. An owner who wants to use the NFT in a game on Immutable X or as collateral in a DeFi protocol on Arbitrum — simply can't.

ONFT (Omnichain Non-Fungible Token) — LayerZero standard for NFTs with native cross-chain transfer. Not a bridge with lock-and-mint risks, but a single contract deployed on multiple chains that atomically moves NFT between them without losing metadata and ownership history.

How ONFT Works at Protocol Level

LayerZero: Endpoints and Ultra Light Node

LayerZero is not a separate blockchain. It's a messaging protocol with Endpoint contracts on each supported chain (~50+: Ethereum, Polygon, Arbitrum, Optimism, BSC, Solana, Aptos and others).

When NFT is sent from Ethereum to Arbitrum:

  1. sendFrom() on Ethereum calls Endpoint.send() with encoded payload (tokenId, recipient)
  2. LayerZero Oracle (Chainlink, Sequencer, or Google Cloud) captures block header on Arbitrum
  3. LayerZero Relayer transmits proof of transaction
  4. Endpoint on Arbitrum verifies proof through Ultra Light Node (ULN) — not full block verification, only needed storage proof
  5. lzReceive() on ONFT contract on Arbitrum is called with payload, mints NFT to recipient

On source chain NFT is burned (or locked depending on implementation). On destination — minted. Overall supply doesn't change.

ONFT721 vs. Custom Implementation

LayerZero provides ONFT721 base contract in @layerzerolabs/solidity-examples. It's ERC-721 with added sendFrom and lzReceive functions. Simplest ONFT implementation — inherit from ONFT721 with custom logic.

Key parameters at deployment:

constructor(
    string memory name,
    string memory symbol,
    uint256 _minGasToTransfer, // minimum gas for lzReceive on destination
    address _lzEndpoint        // LayerZero Endpoint address for this chain
) ONFT721(name, symbol, _minGasToTransfer, _lzEndpoint) {}

_minGasToTransfer is critical: if set too low — lzReceive on destination reverts due to out-of-gas, NFT "gets stuck" between chains. LayerZero recommendation: 200,000 gas for basic ONFT721, more if lzReceive has additional logic.

Problems to Solve During Development

Synchronizing Metadata During Cross-Chain Transfer

NFT metadata is stored on IPFS or Arweave — not a problem, URI is same on all chains. Problem with dynamic metadata: if NFT has on-chain attributes (character level in game, accumulated points), this data is stored in contract storage. When moving to another chain, on-chain state doesn't transfer automatically.

Solution: include state in LayerZero payload. Custom _debitFrom on source packs state, custom _creditTo on destination restores it. This increases gas cost of transfer, but preserves full state.

function _debitFrom(address _from, uint16, bytes memory, uint _tokenId)
    internal override returns(bytes memory) {
    // Collect token state
    TokenState memory state = tokenStates[_tokenId];
    _burn(_tokenId); // or lock
    return abi.encode(_tokenId, state); // include in payload
}

function _creditTo(uint16, address _toAddress, bytes memory _payload)
    internal override returns(uint) {
    (uint tokenId, TokenState memory state) = abi.decode(_payload, (uint, TokenState));
    _mint(_toAddress, tokenId);
    tokenStates[tokenId] = state; // restore state
    return tokenId;
}

LayerZero Fee Assessment and Payment

Transfer through LayerZero isn't free: user pays with native currency of source chain for:

  • Gas on source chain (Endpoint.send)
  • Oracle and relayer fee (goes to LayerZero)
  • Gas estimation on destination chain (prepaid)

Client code must call estimateSendFee() before transfer and pass result as msg.value. If msg.value is less than estimate — transaction reverts.

function estimateSendFee(
    uint16 _dstChainId,
    bytes calldata _toAddress,
    uint _tokenId,
    bool _useZro,
    bytes calldata _adapterParams
) public view returns (uint nativeFee, uint zroFee);

Typical transfer cost ETH → Arbitrum: $0.50–2.00 in ETH depending on congestion.

Trusted Remote Configuration

Each ONFT contract on each chain must know addresses of its "siblings" on other chains. This is trustedRemote — authorized list. Without it, any contract could mint ONFT through LayerZero message.

// Executed after deployment on each chain
function setTrustedRemoteAddress(
    uint16 _remoteChainId,   // LayerZero chain ID
    bytes calldata _remoteAddress
) external onlyOwner;

Common mistake: forget to set trusted remote bidirectionally. Transfer Ethereum→Polygon works, Polygon→Ethereum doesn't — because Polygon contract didn't add Ethereum to trusted remote.

Nonce and Ordering Guarantees

LayerZero v1 guarantees ordered delivery: messages between two chains are delivered in order sent. If transaction with nonce N gets stuck (relayer didn't deliver) — all subsequent with nonce N+1, N+2 wait. This can block all transfers from specific chain.

LayerZero v2 (2024) switches to unordered delivery with application-level ordering — more flexible model, doesn't block queue.

Stack for Full ONFT Project

Contracts: Solidity 0.8.x, @layerzerolabs/lz-evm-oapp-v2 (for LZ v2) or @layerzerolabs/solidity-examples (LZ v1), OpenZeppelin ERC721.

Testing: Foundry with LZEndpointMock — mock LayerZero endpoint for local cross-chain call testing without real oracle. Test: send from chain A, verify mint on chain B.

Frontend: wagmi/viem for multi-chain support, network switching on transfer, displaying estimated fee through estimateSendFee.

Deployment: Foundry scripts for parallel deployment to multiple chains + script for setting trustedRemote for all pairs.

Process and Timeline

Design (1 day): list of target chains, presence of on-chain state for synchronization, custom logic in _debitFrom/_creditTo.

Contract Development (2–3 days): ONFT721 with custom logic, tests via LZEndpointMock.

Frontend Component (1 day): bridge interface with destination chain selection, fee estimation, transfer status.

Deployment and Configuration (0.5 day): deploy to all chains, set trustedRemote.

Total: 3–5 days for basic ONFT without on-chain state. With complex state synchronization — 1–2 weeks. Cost calculated individually.