ERC721-O: 全链 NFT的标准协议和实现

Catddle
11 min readApr 17, 2022

ERC721-OCatddle NFT的开发团队 Catddle Lab 设计。Catddle NFT 将是第一个使用 ERC721-O 开发的 全链 NFT。

*ERC721-O 是完全开源的项目;欢迎在GitHub 上参与贡献!

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

概述

ERC721-O 是基于ERC721的 全链 NFT (Non-Fungible Token)的标准协议和实现
ERC721-O 协议旨在协助第三方程式(例如钱包/交易所)与任何实现了该协议的NFT进行统一的验证与交互 。

ERC721-O 新增了多种跨链功能,并提出了一系列解决跨链安全问题的方案。

背景

LayerZero 是一个新的区块链基础设施,能够提供不同区块链之间的消息传递服务。根据 LayerZero 的论文,这种消息传递服务保证了有效传递。当接收方收到来自有效传递服务的消息 m 时,该服务会确保关联的交易 t 有效并且已在发送方侧链上提交。
通过这个通用消息协议,全链 NFT的实现成为了可能。

ERC721-O 与现有全链NFT*的区别

ERC721-O的改进

更安全

· 跨链燃料保护机制

尽管我们有非阻塞等一系列安全设计,但当跨链燃料限制(destination gas limit)设置比较低的时候,NFT仍然可能在跨链时被卡住。
为避免这种情况,我们引入了跨链燃料保护机制;合约所有者能够通过设置合理的最低跨链燃料限制来保证所有NFT都能成功完成跨链,而不会出现燃料不足的现象,同时还能保证其他跨链能力不受影响。

· 远程地址安全设置

更新使用中的全链 NFT 合约可能会导致资金永久损失。此外,当将全链NFT的合约部署到新链,且未按照特定的函数调用顺序调用时,NFT可能会在跨链时被阻塞,甚至永久丢失。
为了解决这些问题,ERC721-O引入了安全的远程地址设置机制。通过该机制合约所有者可以避免合约更新中的资金损失问题,且无需关心区块链之间的函数调用顺序。

· 固定三方库版本

发送和接收消息依赖于 LayerZero 库。在 LayerZero 更新时,必须有能力固定第三方库的版本以避免升级带来的潜在错误。这也是 LayerZero 官方推荐的最佳做法。

· 保留最后手段

即使引入了多种安全设计(例如,非阻塞,跨链燃料保护),仍然有可能出现消息路径被阻塞并且无法成功重试的情况。

forceResumeReceive是我们可以使用的最后手段,否则链错误将被永久卡住。我们把它作为最后的手段保留在实现中。

更多功能

· 提供三方转账 (Approval) 模式

批准(Approve)第三方转移你的Token是 ERC20 和 ERC721 中提供的一项关键能力。我们添加此功能是为了帮助第三方应用程序能够更好地与全链NFT交互。

· 自由选择跨链转账目标地址

用户现在可以将NFT转移到其他链上的任何地址,而不是只将NFT转移到另一条链上的同一地址。

· 发送NFT同时可以完成Gas空投

在用户将 NFT 发送到另一条链后,如果用户想使用它,则需要将额外的原生燃料(Ether、BNB、AVAX 等)发送到在目标链上的地址,这个额外的步骤很麻烦。
现在Gas跨链空投能够直接与发送NFT同时完成, 在目标链上不再需要额外的操作。

· 允许使用token支付跨链手续费

当 LayerZero 推出其Token时,用户可以通过设置特定的Token所有者来支付跨链费用。他们也许会提供一些折扣:)

更透明

· 关键部署信息公开

在ERC721-O协议设计中,发送端口地址(endpoint)和远程合约地址(remote)都需要强制公开,所以任何用户和第三方都可以检查发送端口地址(endpoint)是否来自LayerZero官方,以及远程合约地址(remote)是否正确设置。

因此,交易所和钱包等第三方可以轻松地为 ERC721-O Token实施检查机制,以保护用户免受潜在的资金损失。

· 重要链上活动披露

当重要的链上活动发生时,发送事件可以帮助用户和第三方在第一时间了解在链上发生了什么。
在 ERC721-O 接口设计中,向另一条链发送NFT和接收NFT都会强制发送事件。这可以帮助用户和第三方清楚地了解链上发生的事情。

在 ERC721-O 实现中,其他关键的活动(例如暂停,设置远程合约地址)也会发出事件以通知用户发生重要变化。

· 允许进行Nonce跟踪

Nonce标志了某笔交易是从特定地址发送的第几笔。LayerZero 提供Nonce跟踪能力,ERC721-O将其纳入合约的跨链事件中。通过发生的事件提供的信息,用户和第三方可以轻松匹配源链和目标链上的交易。

减少Gas消耗

由于 LayerZero 已经为用户做了 gas 预估检查,所以发送消息前的 gas 预估检查是多余的。ERC721-O将其删除以减少跨链时的燃料消耗。

代码易于覆盖和重用

所有函数和变量都设计为易于覆盖和重用。

作为基类,virtualoverrideinternal被广泛用于代码中,并提供了良好的注释,以便在开发者继承合约代码时给出更明确的提示与指引。

安全设计背后的原理

LayerZero 官方提供了很好的基本实现示例。此外,像Gh0stly Gh0sts这样的NFT向所有人证明了跨链 NFT 的巨大潜力。

但是,如果 全链Token想要走得更远,必须注意并解决一些严重的安全问题。为了解决这些问题,ERC721-O 引入了一系列特有的安全设计。

在我们深入介绍具体设计之前,这里应该介绍一个重要的概念:跨链消息阻塞。跨链消息阻塞是一种无法从源合约向目标链送达消息的状态。在这种状态下,源合约能够继续发送消息,但它们都被卡在空中,不在源链上也不在目标链上,无法被接受和执行。

它不仅让NFT用户感到不愉快,而且是一种危险的状态,因为:

  1. 所有阻塞消息都在链上消失,并在 LayerZero 维护的队列中等待,这可能会存在风险。
  2. 如果失败消息无法解析,被阻塞的消息队列将永远卡住。
  3. 如果合约所有者使用forceResumeReceive函数处理失败的消息,而非retryPayload,则失败的消息可能会永远丢失。请注意,在全链NFT的实现中,丢失消息等同于丢失NFT。

因此,安全设计的主要目标就是尽量减少消息阻塞的可能性。

· 跨链燃料保护机制

需要注意的是,即使使用非阻塞机制,Token也可能由于跨链燃料限制(desination gas limit)较低的问题而被卡住。

在 ERC721-O 设计中,适配器参数向用户完全开放以提供更多能力,例如可以在发送NFT的同时空投Gas Token (例如Ether, BNB, AVA等)。用户还可以根据需要手动设置跨链燃料限制。如果用户设置100为跨链燃料费限制,然后发起跨链请求,这对于 LayerZero 端点是合法的,但实际会造成交易阻塞。即使使用非阻塞模式也是如此。

这是因为当跨链燃料限制过低时,接收方链上的交易将在 try-catch 部分代码运行之前失败。换句话说,非阻塞模式在这种情况下无效。

为了解决这个问题,ERC721-O引入了燃料保护机制。这个机制将检查用户输入适配器参数的有效性。如果适配器参数不合法或分配的燃料低于合约所有者指定的最小燃料限制,则交易将将无法发送。这个保护机制并不会限制用户使用增强型适配器参数,用户依然可以通过增强型参数来实现空投Gas等功能。

通过这个机制,合约所有者可以根据他们的接收函数的燃料消耗设置一个合理的最小燃料限制,以避免出现阻塞问题。

·远程地址安全设置

任何人都可以通过 LayerZero 向任何人发送消息,因此必须设置远程合约以避免执行来自他人的非法消息。在现有实现中,setTrustedRemote函数就能够帮助限制消息发送方。合约所有者将调用setTrustRemote 设置远程合约地址,然后该合约将只收到来自该远程合约的消息。

这听起来很合理,但这里没有考虑不同链上操作提交的时间总是存在间隔。

假设一个场景,A 链上的合约 a 和 B 链上的合约 b 已经相互设置对方为远程合约,并都处于使用中。使用中意味着两条链上都有已铸造的NFT存在。而现在我们希望将 A 链上的合约 a 更新为同在 A 链上的新合约 c。

我们可以简单地在 B 链上调用setTrustRemote函数,将 c 设置为合约 b 的新远程合约地址。但这样可能会发生糟糕的情况。此时 A 链上的用户仍可以继续向 B 链发送NFT,而由于 B 链上的远程合约地址已改变,不再是 A 链,因此他们发送的所有NFT将永久丢失。请注意,非堵塞机制在这里无济于事,因为它仅在正确设置远程合约地址后才会起作用。

相反的,即使我们先更改合约 a 的远程合约地址,结果也是如此。 B 链上的NFT用户可以在B 链也设置好之前发送NFT给合约 a。这也将导致NFT永久丢失,因为合约 a 上的远程合约地址已经被更改,并且不再会设置到合约 b 的地址。

这里需要注意的是,我们永远无法确保不同链上的操作可以同时发生。那么似乎我们无法在合约正在使用之后去更新合约?

其实是有办法解决的,例如ERC721-O中的的远程合约安全设置机制就可以解决这个问题。我们引入了一个pauseMove函数,它接受一个参数chainId。它会将move功能在chainId 链上加锁。这意味着当前链上的用户暂时无法向chainId链发送NFT。合约所有者可以在确认两条链上的setRemote操作均已经确认后,再调用unPauseMove(chainId)以解锁对chainId链进行move操作,以便用户可以再次向chainId链发送NFT。

通过这样的设计,合约所有者得以处理使用中合约的更新问题。回到我们的例子,合约所有者可以在 B 链上调用pauseMove(A),这样 B 链上的用户会无法向 A 链发送NFT,而 A 链上的用户可以继续向 B 链发送NFT。请注意,此时合约 b 仍然可以正确地接收来自合约 a 的NFT,因此在这段时间间隔内不会丢失任何NFT。然后合约所有者在 A 链的合约 a 上调用pauseMove(B),这样 A 链上的用户也无法向 B 链发送NFT。同样的,还需要在A 链的合约 c 上调用pauseMove(B)

这时候合约所有者就可以以任意的顺序在合约 b 和 合约 c 上调用setRemote函数将对方设置为远程合约地址。最后通过在合约 b 和合约 c 上调用unpause来恢复跨链发送,至此 A 链上的合约 a 就成功地被合约 c 替换了。

这种设计也适用于其他场景。例如,当一个部署在 A 链上并且已经在使用的NFT想要部署到 B 链上时,合约所有者必须首先在 B 链上设置远程地址,否则NFT可能会被卡住。有了pauseMove功能,合约所有者就可以先pauseMove然后再在链 A 或链 B 上设置对方为远程合约地址,而不必担心函数调用顺序带来的影响。

事实上这种模式还可以扩展到其他类型的Token,例如 ERC20,这里不再详细展开。

讨论

· 我们应该取消链上燃料估算吗?

LayerZero 官方文档建议进行链上燃料估算,以避免在目标链上发生交易失败。

但事实上他们已经在 官方部署的智能合约中实施了这项检查。另外,对于多种消息类型的情况,即使是进行链上燃料估计也无保证源链上燃料估计的正确性。此外,即使燃料估计错误,具有非阻塞设计的ERC721-O合约也可以完美地处理这个问题,而不会造成任何重大的损失。

因此,强制执行链上燃料估算似乎并没有实质上的意义,我们将其从我们当前的协议版本中移除了。

· 我们应该移除forceResumeReceive函数吗?

ForceResumeRecive功能很可能会导致资金损失。但是,如果某条意外的错误消息卡住了消息路径并且无法成功重试,ForceResumeRecive是清除这样的阻塞状态的唯一方法。需要注意的是,非阻塞设计并不能始终确保消息路径处于畅通状态。为避免发生意料之外的阻塞,我们将其保留在当前实现中。当且仅当您的合约具有 IFG性质时(即时确定性)时,您才能覆盖此函数并令其无效。即时确定性意味着如果交易在源链被接受,那么它也将在目标链被接受。

· 我们应该将非阻塞机制放入基础实现中吗?

阻塞对用户体验有害,甚至会带来资金损失的风险的。因此,我们希望尽量减少阻塞发生可能性。鉴于交易的顺序在 NFT 跨链操作中并不重要,我们可以利用 非阻塞机制来确保在发生一些意外错误/异常发送后,消息路径仍然是畅通的。如果您的合约能够确保在经过远程地址检查后总是正确执行,则可以移除非阻塞机制的部分。

ERC721-O 仍然是一个开放的需要社区共建的全新标准。我们相信,ERC721-O 将在社区的贡献下,成为全链 NFT 的标准协议。

以上所有结论均来自 LayerZero 实验室的开源文档和开源代码,以及在 LayerZero 官方节点上的实验。如果您发现任何错误,非常感谢您可以发表评论或者在 Github 中或提出issue。感谢您的阅读!

* 注: 我们在文中使用Gh0stly Gh0sts实现和官方示例作为对比的标准

--

--