Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-5058: 可锁定的非同质化代币

可锁定的 EIP-721 代币

Authors Tyler (@radiocaca), Alex (@gojazdev), John (@sfumato00)
Created 2022-04-30
Discussion Link https://ethereum-magicians.org/t/eip-5058-erc-721-lockable-standard/9201
Requires EIP-20, EIP-165, EIP-721

摘要

我们建议通过一种安全的锁定机制来扩展 EIP-721 标准。NFT 所有者通过 setLockApprovalForAll()lockApprove() 批准操作者锁定 NFT。批准的操作者通过 lock() 锁定 NFT。在锁定周期结束之前,锁定的 NFT 无法转移。一个直接的用例是允许 NFT 参与智能合约,而无需离开其所有者的钱包。

动机

EIP-721 启用的 NFT 的需求已经爆发。总市值和生态系统持续增长,出现越来越多的蓝筹 NFT,它们在传统意义上大约等同于流行的知识产权。尽管取得了巨大的成功,但仍有一些不足之处。流动性一直是 NFT 面临的最大挑战之一。已经进行了一些尝试来应对流动性挑战:NFTFi 和 BendDAO 等等。利用目前流行的 EIP-721 标准,这些项目需要将参与的 NFT 转移到项目的合约中,这给所有者带来了不便和风险:

  1. 智能合约风险:由于合约中的错误或漏洞,NFT 可能会丢失或被盗。
  2. 效用损失:NFT 具有效用价值,例如头像和炫耀的权利,当 NFT 不再在所有者的保管下可见时,这些价值就会丢失。
  3. 错过空投:所有者无法再直接收到 NFT 应得的空投。考虑到某些空投的价值和价格波动,错过或没有及时获得空投可能会对所有者产生经济影响。

以上所有都是糟糕的 UX,我们认为可以通过采用原生锁定机制来改进 EIP-721 标准:

  1. NFT 不会被转移到智能合约,而是保留在自我保管中但被锁定。
  2. 当 NFT 被锁定时,禁止其转移。其他属性保持不变。
  3. 所有者可以自己接收或申领空投。

NFT 的价值可以体现在两个方面:收藏价值和效用价值。收藏价值需要确保持有者的钱包永远保留 NFT 的所有权。效用价值需要确保持有者可以在其他项目中验证其 NFT 所有权。这两个方面都需要 NFT 保留在其所有者的钱包中。

所提出的标准允许通过扩展 EIP-721 标准以原生支持常见的 NFTFi 用例,包括锁定、质押、借贷和众筹,从而安全便捷地管理底层 NFT 资产。我们相信,所提出的标准将鼓励 NFT 所有者更积极地参与 NFTFi 项目,从而改善整个 NFT 生态系统的生计。

规范

本文档中的关键词“必须(MUST)”、“禁止(MUST NOT)”、“需要(REQUIRED)”、“应该(SHALL)”、“不应该(SHALL NOT)”、“推荐(SHOULD)”、“不推荐(SHOULD NOT)”、“可以(MAY)”和“可选(OPTIONAL)”应按照 RFC 2119 中的描述进行解释。

可锁定的 EIP-721 必须 实现 IERC5058 接口:

// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.8;

/**
 * @dev EIP-721 非同质化代币标准,可选的可锁定扩展
 * 可以锁定一段时间且不能转移的 ERC721 代币。
 * 这是为稍后出现的非托管质押合约设计的,用于锁定用户的 NFT
 * 同时仍然让他们将其保存在他们的钱包中。
 * 此扩展可以确保用户代币在质押期间的安全性。
 * 如果 nft 借贷协议与此扩展兼容,则可以避免 NFT 引起的麻烦
 * 空投,因为空投仍然在用户的钱包中
 */
interface IERC5058 {
    /**
     * @dev 当 `tokenId` 代币被 `operator` 从 `from` 锁定时发出。
     */
    event Locked(address indexed operator, address indexed from, uint256 indexed tokenId, uint256 expired);

    /**
     * @dev 当 `tokenId` 代币被 `operator` 从 `from` 解锁时发出。
     */
    event Unlocked(address indexed operator, address indexed from, uint256 indexed tokenId);

    /**
     * @dev 当 `owner` 允许 `approved` 锁定 `tokenId` 代币时发出。
     */
    event LockApproval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**
     * @dev 当 `owner` 允许或禁止 (`approved`) `operator` 锁定其所有代币时发出。
     */
    event LockApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**
     * @dev 返回锁定 `tokenId` 代币的locker。
     *
     * 要求:
     *
     * - `tokenId` 必须存在。
     */
    function lockerOf(uint256 tokenId) external view returns (address locker);

    /**
     * @dev 锁定 `tokenId` 代币,直到区块号大于 `expired` 才能解锁。
     *
     * 要求:
     *
     * - `tokenId` 代币必须由 `owner` 拥有。
     * - `expired` 必须大于 block.number
     * - 如果调用者不是 `owner`,则必须被批准才能锁定此代币
     * 通过 {lockApprove} 或 {setLockApprovalForAll}。
     *
     * 发出 {Locked} 事件。
     */
    function lock(uint256 tokenId, uint256 expired) external;

    /**
     * @dev 解锁 `tokenId` 代币。
     *
     * 要求:
     *
     * - `tokenId` 代币必须由 `owner` 拥有。
     * - 调用者必须是通过 {lock} 锁定代币的操作者
     *
     * 发出 {Unlocked} 事件。
     */
    function unlock(uint256 tokenId) external;

    /**
     * @dev 授予 `to` 锁定 `tokenId` 代币的权限。
     *
     * 要求:
     *
     * - 调用者必须拥有该代币或是一个被批准的锁定操作者。
     * - `tokenId` 必须存在。
     *
     * 发出 {LockApproval} 事件。
     */
    function lockApprove(address to, uint256 tokenId) external;

    /**
     * @dev 批准或移除 `operator` 作为调用者的锁定操作者。
     * 操作者可以为调用者拥有的任何代币调用 {lock}。
     *
     * 要求:
     *
     * - `operator` 不能是调用者。
     *
     * 发出 {LockApprovalForAll} 事件。
     */
    function setLockApprovalForAll(address operator, bool approved) external;

    /**
     * @dev 返回为 `tokenId` 代币批准的锁定账户。
     *
     * 要求:
     *
     * - `tokenId` 必须存在。
     */
    function getLockApproved(uint256 tokenId) external view returns (address operator);

    /**
     * @dev 返回 `operator` 是否被允许锁定 `owner` 的所有资产。
     *
     * 参见 {setLockApprovalForAll}
     */
    function isLockApprovedForAll(address owner, address operator) external view returns (bool);

    /**
     * @dev 返回 `tokenId` 代币是否被锁定。
     */
    function isLocked(uint256 tokenId) external view returns (bool);

    /**
     * @dev 返回 `tokenId` 代币锁定的过期时间。
     */
    function lockExpiredTime(uint256 tokenId) external view returns (uint256);
}

原理

NFT 锁定批准

NFT 所有者可以通过批准函数授予另一个受信任的操作者锁定其 NFT 的权利。lockApprove() 函数仅批准指定的 NFT,而 setLockApprovalForAll() 批准钱包下集合的所有 NFT。当用户参与 NFTFi 项目时,项目合约调用 lock() 来锁定用户的 NFT。锁定的 NFT 不能被转移,但 NFTFi 项目合约可以使用解锁函数 unlock() 来解锁 NFT。

NFT 锁定/解锁

授权的项目合约有权使用 lock 方法锁定 NFT。锁定的 NFT 在锁定时间到期之前不能被转移。项目合约也有权通过 unlock 函数提前解锁 NFT。请注意,只有被锁定的 NFT 的地址才有权解锁该 NFT。

NFT 锁定周期

当锁定 NFT 时,必须指定锁定到期区块号,该区块号必须大于当前区块号。当当前区块号超过到期区块号时,NFT 将自动释放并且可以被转移。

绑定 NFT

绑定 NFT 是此 EIP 的一个扩展,它实现了在 NFT 锁定期间铸造 boundNFT 的能力。boundNFT 与锁定的 NFT 元数据相同,并且可以被转移。然而,boundNFT 仅在 NFT 锁定期间存在,并且将在 NFT 解锁后被销毁。 BoundNFT 可以用于借贷,作为合约的质押凭证。该凭证可以锁定在合约中,也可以锁定在用户身上。在 NFT 租赁中,boundNFT 可以出租给用户,因为 boundNFT 本质上等同于 NFT。如果所有项目都接受此共识,boundNFT 将为 NFT 带来更多的创造力。

绑定 NFT 工厂

绑定 NFT 工厂是一个常见的 boundNFT 工厂,类似于 Uniswap 的 EIP-20 交易对工厂。它使用 create2 方法为任何 NFT 确定性地创建 boundNFT 合约地址。已创建的 BoundNFT 合约只能由原始 NFT 合约控制。

向后兼容性

此标准与 EIP-721 兼容。

测试用例

使用 hardhat 编写的测试用例可以在 这里 找到。

参考实现

你可以在 assets 文件夹中找到此标准的实现。

安全注意事项

在被锁定后,NFT 无法被转移,因此在授权其他项目合约锁定权限之前,你必须确认项目合约可以解锁 NFT。否则存在 NFT 被永久锁定的风险。建议在使用项目时给予合理的锁定周期。NFT 可以自动解锁,这可以在一定程度上降低风险。

版权

版权和相关权利已通过 CC0 放弃。

Citation

Please cite this document as:

Tyler (@radiocaca), Alex (@gojazdev), John (@sfumato00), "ERC-5058: 可锁定的非同质化代币 [DRAFT]," Ethereum Improvement Proposals, no. 5058, April 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5058.