ERC721-O: a standard interface and implementation for Omnichain NFT

Catddle
10 min readApr 14, 2022

--

ERC721-O is designed by Catddle Lab, the dev team of Catddle NFT. Catddle NFT will be the first Omnichain NFT developed with ERC721-O.

*ERC721-O is open-source; welcome to contribute on GitHub

Overview

ERC721-O is a standard interface and implementation for Omnichain non-fungible tokens(NFT) based on ERC721.
ERC721-O aims to allow third-party applications (e.g., wallet/marketplace) to interact with any tokens implemented this interface uniformly.

ERC721-O also provides implementations with multiple new features and proposed solutions to mitigate security issues in the cross-chain domain.

Background

Layerzero is a new infrastructure that provides a message delivery service between different blockchains. According to the LayerZero paper, this message delivery service guarantees valid delivery. When the receiver receives a message from the valid delivery service, the service ensures an associated transaction is valid and has been committed on the sender-side chain.
With this general message protocol, an Omnichain Non-Fungible Token is possible to implement.

ERC721-O v.s. Existing OmniNFT*

Improvements of ERC721-O

More Secure

· Gas Guard Design

Though we have many security designs, tokens can be stuck due to the low cross-chain gas limit.
To avoid this, ERC721-O introduced a gas guard design; as the contract owner sets a proper minimum cross-chain gas limit, all tokens should be delivered without the insufficient gas issue. In addition, this design will not affect other cross-chain functions.

· Safe Remote Address Setting

Updating Omnichain NFT contracts in use can cause fund loss. In addition, tokens can be stuck in the air when deploying Omnichain NFT contracts to a new chain without specific function invoking order.
To mitigate these issues, ERC721-O introduced a safe remote address setting pattern. With this design, contract owners can avoid fund loss in contract updating and not need to care about the order of function invoking between chains.

· Fixing Library Version

Sending and receiving messages rely on LayerZero libraries. It is essential to fix the versions of libraries to avoid potential bugs when LayerZero updates. This is also the best practice recommended by LayerZero officials.

· Keep Last Resort

Even though multiple secure designs (e.g., nonBlocking, gasGuard) are introduced, it's still possible that the message path is blocked and cannot retry successfully.

forceResumeReceive is the last resort we can use, or the chain error will be stuck permanently. We keep it as a last resort here.

More Functional

· Approval Transfer Support

Approving a third party to transfer your token is a critical ability provided in ERC20 and ERC721. We added this to help third-party applications work with Omnichain NFTs better.

· Pick your destination address

Rather than transfer tokens only to the same address on the other chain, users can transfer tokens to any address on other chains now.

· Pay crossing chain fees with tokens

When LayerZero launches its tokens, users can pay the cross-chain fee by setting a specific token owner. Maybe some discounts will be provided.

· Send NFT with native tokens

After you send NFTs to the other chain, it is annoying to find that you need to send extra native tokens(Ether, BNB, AVAX, etc.) to your address on the new chain if you want to play with it.
Now you can directly finish this on the source chain; no more transactions are required on the new chain.

More Transparent

· Transparent to security check

In the ERC721-O interface design, both endpoint and remotes are forced to be exposed to the public, so any users and third parties can check if the endpoint is from LayerZero official and remotes are correct.

As a result, third parties like marketplaces and wallets can easily implement a check mechanism for ERC721-O tokens to protect users from potential fund loss.

· Easy to Monitor

When important on-chain activity happens, the emitting event can help users and third parties understand what happened the first time.
In the ERC721-O interface design, both sending tokens to the other chain and receiving tokens are forced to emit events with essential information.
This can help users and third parties to know what happened on-chain clearly.

In the ERC72-O implementations, critical activities like Pause and SetRemote also emit events to notice user important change happens.

· Nonce Tracking

The nonce is the number of transactions sent from a specific address. LayerZero provides nonce tracking ability and ERC721-O incorporates it in the cross-chain events. With the information provided by emitting events, users and third parties can easily match the transactions on the source chain and destination chain.

More Gas Efficient

As LayerZero has already made a gas estimation check for users, the gas estimation check before sending messages is redundant. We remove it to lower gas consumption.

Code Design: Easy to override and reuse

All functions and variables are designed to be easily overridden and reused.

As a base class, virtual, override, and internal are widely used in the codes to give more explicit hints when inherits.

The Rationale Behind the Security Designs

The LayerZero official provides good examples for basic implements. Also, tokens like Gh0stly Gh0sts proved the great potential of cross-chain NFT to everyone.

However, if omnichain tokens want to go further, some severe security issues must be noticed and mitigated. To solve these problems, unique security designs are introduced in the ERC721-O.

Before we dive into the specific designs, an important concept about blocking should be introduced here. Message Blocking is a state that no further message can be delivered to the destination chain. Under this state, the source contract can continue to send messages, though these messages are all stuck in the air, not on any blockchain, and can not be received and executed.

It makes not only token users feel unpleasant but also a dangerous state because:

  1. All the blocking messages are disappeared on-chain and wait in a queue maintained by LayerZero, which is possible to be messed with.
  2. If the failed message cannot be resolved, the blocking messages will be stuck forever
  3. If the contract owner handled the failed message with the forceResumeReceive function rather than retryPayload, the failed message could be lost forever. Notice that in omnichain token implementations, the message lost equals tokens lost.

As a result, the main goal of security designs is to minimize the possibility of message blocking.

· Gas Guard Design

A caveat is that even with the NonBlocking design, tokens can be stuck due to the issue of a low cross-chain gas limit.

In the ERC721-O design, adapter parameters are open to users to provide more abilities, such as sending NFTs with native gas tokens. Users can also set the destination gas limit manually to their needs. If a user sets 100 as the cross-chain gas limit and moves tokens to other chains, which is legal for the LayerZero endpoint, this transaction can cause blocking even with the NonBlocking extension.

When the cross-chain gas limit is too low, transactions on the receiver side chain will fail before try-catch part codes run. In other words, NonBlocking invalidates under this condition.

To solve this issue, ERC721-O introduced a Gas Guard Mechanism. This function will check the validity of user input adapter parameters. If the adapter parameters are illegal or assign a lower gas limit than the minimum gas limit specified by the contract owner, the transaction will be reverted on the source chain. Notice that this guarding mechanism will not restrict the usage of enhanced adapter parameters. Users can still send NFT with the native gas token of the destination chain via enhanced adapter parameters.

In this way, the contract owners can set a proper minimum gas limit according to the gas consumption of their receiver functions to avoid low cross-chain gas limit issue happens.

· Safe Remote Design

Anyone can send a message via LayerZero to anyone, so a remote contract must be set to avoid illegal messages from others. In existing implementations, the setTrustedRemote function is used. The contract owner will invoke setTrustRemoteto assign the trusted contract on other chains, and then the contract will only receive a message from trust contracts.

It sounds all reasonable, but a time gap is out of consideration here.

Suppose contract a on chain A and contract b on chain B are already set each other as remote contract and in use. In use means tokens live on both chains. Now we want to update contract a to a new contract c on chain A.

We can invokesetTrustRemote on chain B to set c as the new remote contract address of contract b. Then unexpected things can happen. Token users on chain A can keep sending tokens to chain B, and all their sending tokens will be permanently lost due to the trusted remote contract on chain B already changed and will not be contract a then. Notice that the NonBlocking design will not help here because it only works after the remote contract has been set correctly.

If we change the remote contract address for contract a first on the contrary, the result is the same as before. The token users on chain B can send tokens to contract a before everything is set. This can also cause tokens to be lost forever due to the remote contract address on contract a​ already being changed and will not back to contract b after settings.

Notice that we can never make certain actions on different chains can happen simultaneously. So it seems we can not update contracts after they are already connected and in use. In fact, there are ways to solve it, and a safeRemote design is introduced here.

We introduced a pauseMovefunction. It accepts one parameter chainId. The pauseMovefunction will lock chainId chain, which means users can not send tokens to the chainId chain for the time being. The contract owners can call unPause(chainId) to unlock the move on chainId chain after they ensure the setRemoteaction has been commited on both chains, so that users can send tokens to chainId chain again.

With this design, contract owners can have ways to deal with the problem of updating in used contracts. Back to our example, the contract owner can simply invoke pauseMove(A) on chain B. After that, users on chain B cannot send tokens to chain A while users on chain A can continue to send tokens to chain B. Notice that contract b can still receive tokens from the contract a properly at this time, so that no tokens will be lost during this time gap. Then the contract owner can invoke pauseMove(B)on contract a, then token users on chain A also can not send NFT to chain B. Similarly, invoke pauseMove(B)on contract c.

Now contract owners can invoke setRemotefunction on contract b and contract c in any order they want to set each other as remote contract address. Finally, invoke unpauseon contract b and contract c to continue cross-chain sending. At this point, contract a have been replaced by contract c successfully.

This design also works for other scenes. For instance, when a token minted on chain A want to extend to chain B, contract owners must set remote contract address on chain B first, or tokens can be stuck. With the pauseMove function, owners can set the remote on chain A first or chain B first as they want without worrying about the function invoking order.

Notice that this pattern can also extend to other types of tokens like ERC20.

Discussion

· Should we remove on-chain gas estimation?

The LayerZero official documents recommend doing on-chain gas estimations to avoid transactions failing at the destination chain.

However, they have already implemented this check in the LayerZero deployed smart contracts. In addition, as for multiple message types, even on-chain gas estimation can not guarantee the correct gas estimation on the source chain. Even if gas estimation is wrong, a contract with a NonBlocking design can handle this perfectly without any substantial loss.

So it seems meaningless to enforce on-chain gas estimation and remove it from our current version.

· Should we remove forceResumeReceive function?

ForceResumeRecive function can make fund loss. However, if an unexpected payload stuck the message path and cannot retry successfully, ForceResumeRecive is the only way to clear the blocking messages. Notice that NonBlocking cannot always ensure the message path keeps open. To avoid unexpected blocking happens, we keep it in the current implementation. You can override and invalidate this function if and only if your contract has IFG (Instant Finality Guarantee). The instant finality means if a transaction was accepted at the source chain, then it will be accepted at the destination chain.

· Should we put NonBlocking mechanism into the base implementation?

Blocking is harmful to user experience and even dangerous. As a result, we try to minimize the possibility of blocking. Since the order of transactions is not important in NFT cross chain operation, we can leverage the NonBlocking mechanism to ensure the message path is still open when some unexpected errors/exceptions happen. If your contract never reverts after the remote check, the nonBlocking part can be removed.

ERC721-O is still a new standard open to review. We believe that ERC721-O will become the standard protocol for Omnichain NFT in the future with the contribution of open-source communities.

All conclusions are drawn from open-source documents and codes of LayerZero labs and experiments on LayerZero official endpoints. If you found any mistake, it is great appreciated you can comment or raise issues in Github. Thanks for reading!

* We use Gh0stly Gh0sts and official examples as comparison here

--

--