Alert Source Discuss
Standards Track: ERC

ERC-5725: 可转移的归属 NFT

用于可转移的归属 NFT 的接口,该 NFT 随时间释放底层 token。

Authors Apeguru (@Apegurus), Marco De Vries <marco@paladinsec.co>, Mario <mario@paladinsec.co>, DeFiFoFum (@DeFiFoFum), Elliott Green (@elliott-green)
Created 2022-09-08
Requires EIP-721

摘要

一种 Non-Fungible Token (NFT) 标准,用于在归属释放曲线中归属 token(ERC-20 或其他)。

以下标准允许为基于 NFT 的合约实现标准 API,该合约持有并表示任何底层 token(ERC-20 或其他)的归属和锁定属性,这些 token 将被发送给 NFT 持有者。 此标准是 ERC-721 token 的扩展,它提供了创建归属 NFT、声明 token 和读取归属曲线属性的基本功能。

动机

归属合约(包括时间锁定合约)缺乏标准和统一的接口,这导致了此类合约的各种实现。 将此类合约标准化为单个接口将允许围绕这些合约创建链上和链下工具生态系统。 此外,以非同质化资产形式存在的流动归属可以证明是对传统 Simple Agreement for Future Tokens (SAFTs) 或基于 Externally Owned Account (EOA) 的归属的巨大改进,因为它实现了可转移性以及附加元数据的能力,类似于传统 NFT 提供的现有功能。

这样的标准不仅将提供急需的 ERC-20 token 锁定标准,而且还将能够创建专为半流动 SAFT 量身定制的二级市场。

该标准还允许轻松实现各种不同的归属曲线。

这些曲线可以表示:

  • 线性归属
  • 悬崖归属
  • 指数归属
  • 自定义确定性归属

用例

  1. 一个在设定的时间内释放 token 的框架,可用于构建许多类型的 NFT 金融产品,例如债券、国库券和许多其他产品。
  2. 以标准化的半流动归属 NFT 资产形式复制 SAFT 合约。
    • SAFT 通常是链下的,而今天的链上版本主要是基于地址的,这使得将归属股份分配给多个代表变得困难。 标准化简化了此复杂的过程。
  3. 为归属和 token 时间锁定合约的标准化提供途径。
    • 在野外有许多这样的合约,并且它们在接口和实现上都不同。
  4. 专门用于归属 NFT 的 NFT 市场。
    • 可以从 token 归属 NFT 的通用标准中创建全新的接口和分析集。
  5. 将归属 NFT 集成到 Safe Wallet 等服务中。
    • 一个标准将意味着像 Safe Wallet 这样的服务可以更轻松、更 uniform 地支持与多重签名合约中的这些类型合约的交互。
  6. 启用标准化的筹款实施和一般的筹款,以更透明的方式出售归属 token(例如 SAFT)。
  7. 允许工具、前端应用程序、聚合器等显示更全面的归属 token 视图和用户可用的属性。
    • 目前,每个项目都需要编写自己的归属资产归属计划的可视化文件。 如果将其标准化,则可以开发第三方工具来为用户聚合来自所有项目的所有归属 NFT,显示其计划并允许用户执行聚合的归属操作。
    • 此类工具可以通过 ERC-165 supportsInterface(InterfaceID) 检查轻松发现合规性。
  8. 使单个包装实施更容易在所有归属标准中使用,这些标准定义了多个接收者、归属 token 的定期租赁等。

规范

本文档中使用的关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY”和“OPTIONAL”应按照 RFC 2119 中的描述进行解释。

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

/**
 * @title Non-Fungible Vesting Token Standard.
 * @notice A non-fungible token standard used to vest ERC-20 tokens over a vesting release curve 
 *  scheduled using timestamps.
 *  一种非同质化 token 标准,用于通过使用时间戳计划的 vesting 释放曲线来分配 ERC-20 token。
 * @dev Because this standard relies on timestamps for the vesting schedule, it's important to keep track of the 
 *  tokens claimed per Vesting NFT so that a user cannot withdraw more tokens than allotted for a specific Vesting NFT.
 *  由于此标准依赖于时间戳进行归属计划,因此跟踪每个归属 NFT 声明的 token 非常重要,这样用户才不会提取比特定归属 NFT 分配的 token 更多。
 * @custom:interface-id 0xbd3a202b
 */
interface IERC5725 is IERC721 {
    /**
     *  This event is emitted when the payout is claimed through the claim function.
     *  @param tokenId the NFT tokenId of the assets being claimed.
     *  @param recipient The address which is receiving the payout.
     *  @param claimAmount The amount of tokens being claimed.
     */
    event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 claimAmount);

    /**
     *  This event is emitted when an `owner` sets an address to manage token claims for all tokens.
     *  @param owner The address setting a manager to manage all tokens.
     *  @param spender The address being permitted to manage all tokens.
     *  @param approved A boolean indicating whether the spender is approved to claim for all tokens.
     */
    event ClaimApprovalForAll(address indexed owner, address indexed spender, bool approved);

    /**
     *  This event is emitted when an `owner` sets an address to manage token claims for a `tokenId`.
     *  @param owner The `owner` of `tokenId`.
     *  @param spender The address being permitted to manage a tokenId.
     *  @param tokenId The unique identifier of the token being managed.
     *  @param approved A boolean indicating whether the spender is approved to claim for `tokenId`.
     */
    event ClaimApproval(address indexed owner, address indexed spender, uint256 indexed tokenId, bool approved);

    /**
     * @notice Claim the pending payout for the NFT.
     *  @notice 声明 NFT 的待处理付款。
     * @dev MUST grant the claimablePayout value at the time of claim being called to `msg.sender`. 
     *  MUST revert if not called by the token owner or approved users. 
     *  MUST emit PayoutClaimed. 
     *  SHOULD revert if there is nothing to claim.
     * @param tokenId The NFT token id.
     */
    function claim(uint256 tokenId) external;

    /**
     * @notice Number of tokens for the NFT which have been claimed at the current timestamp.
     *  @notice 在当前时间戳已声明的 NFT 的 token 数量。
     * @param tokenId The NFT token id.
     * @return payout The total amount of payout tokens claimed for this NFT.
     */
    function claimedPayout(uint256 tokenId) external view returns (uint256 payout);

    /**
     * @notice Number of tokens for the NFT which can be claimed at the current timestamp.
     *  @notice 可以在当前时间戳声明的 NFT 的 token 数量。
     * @dev It is RECOMMENDED that this is calculated as the `vestedPayout()` subtracted from `payoutClaimed()`.
     *  建议将其计算为从 `vestedPayout()` 中减去的 `payoutClaimed()`。
     * @param tokenId The NFT token id.
     * @return payout The amount of unlocked payout tokens for the NFT which have not yet been claimed.
     */
    function claimablePayout(uint256 tokenId) external view returns (uint256 payout);

    /**
     * @notice Total amount of tokens which have been vested at the current timestamp. 
     *  This number also includes vested tokens which have been claimed.
     *  @notice 在当前时间戳已 vesting 的 token 总量。
     *  此数字还包括已声明的 vesting token。
     * @dev It is RECOMMENDED that this function calls `vestedPayoutAtTime` 
     *  with `block.timestamp` as the `timestamp` parameter.
     *  建议此函数使用 `block.timestamp` 作为 `timestamp` 参数调用 `vestedPayoutAtTime`。
     * @param tokenId The NFT token id.
     * @return payout Total amount of tokens which have been vested at the current timestamp.
     */
    function vestedPayout(uint256 tokenId) external view returns (uint256 payout);

    /**
     * @notice Total amount of vested tokens at the provided timestamp. 
     *  This number also includes vested tokens which have been claimed.
     *  @notice 在提供的时间戳已 vesting 的 token 总量。
     *  此数字还包括已声明的 vesting token。
     * @dev `timestamp` MAY be both in the future and in the past. 
     *  Zero MUST be returned if the timestamp is before the token was minted.
     *  `timestamp` 可以是将来和过去的时间。
     *  如果时间戳在 token 创建之前,则必须返回零。
     * @param tokenId The NFT token id.
     * @param timestamp The timestamp to check on, can be both in the past and the future.
     * @return payout Total amount of tokens which have been vested at the provided timestamp.
     */
    function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) external view returns (uint256 payout);

    /**
     * @notice Number of tokens for an NFT which are currently vesting.
     *  @notice 当前正在 vesting 的 NFT 的 token 数量。
     * @dev The sum of vestedPayout and vestingPayout SHOULD always be the total payout.
     *  vestedPayout 和 vestingPayout 的总和应始终为总付款。
     * @param tokenId The NFT token id.
     * @return payout The number of tokens for the NFT which are vesting until a future date.
     */
    function vestingPayout(uint256 tokenId) external view returns (uint256 payout);

    /**
     * @notice The start and end timestamps for the vesting of the provided NFT. 
     *  MUST return the timestamp where no further increase in vestedPayout occurs for `vestingEnd`.
     *  @notice 提供的 NFT 归属的开始和结束时间戳。
     *  必须返回 `vestingEnd` 的 vestedPayout 不再增加的时间戳。
     * @param tokenId The NFT token id.
     * @return vestingStart The beginning of the vesting as a unix timestamp.
     * @return vestingEnd The ending of the vesting as a unix timestamp.
     */
    function vestingPeriod(uint256 tokenId) external view returns (uint256 vestingStart, uint256 vestingEnd);

    /**
     * @notice Token which is used to pay out the vesting claims.
     *  @notice 用于支付归属声明的 token。
     * @param tokenId The NFT token id.
     * @return token The token which is used to pay out the vesting claims.
     */
    function payoutToken(uint256 tokenId) external view returns (address token);

    /**
     * @notice Sets a global `operator` with permission to manage all tokens owned by the current `msg.sender`.
     *  @notice 设置一个全局 `operator`,该 `operator` 有权管理当前 `msg.sender` 拥有的所有 token。
     * @param operator The address to let manage all tokens.
     * @param approved A boolean indicating whether the spender is approved to claim for all tokens.
     */
    function setClaimApprovalForAll(address operator, bool approved) external;

    /**
     * @notice Sets a tokenId `operator` with permission to manage a single `tokenId` owned by the `msg.sender`.
     *  @notice 设置一个 tokenId `operator`,该 `operator` 有权管理 `msg.sender` 拥有的单个 `tokenId`。
     * @param operator The address to let manage a single `tokenId`.
     * @param tokenId the `tokenId` to be managed.
     * @param approved A boolean indicating whether the spender is approved to claim for all tokens.
     */
    function setClaimApproval(address operator, bool approved, uint256 tokenId) external;

    /**
     * @notice Returns true if `owner` has set `operator` to manage all `tokenId`s.
     *  @notice 如果 `owner` 已设置 `operator` 来管理所有 `tokenId`,则返回 true。
     * @param owner The owner allowing `operator` to manage all `tokenId`s.
     * @param operator The address who is given permission to spend tokens on behalf of the `owner`.
     */
    function isClaimApprovedForAll(address owner, address operator) external view returns (bool isClaimApproved);

    /**
     * @notice Returns the operating address for a `tokenId`. 
     *  If `tokenId` is not managed, then returns the zero address.
     *  @notice 返回 `tokenId` 的操作地址。
     *  如果未管理 `tokenId`,则返回零地址。
     * @param tokenId The NFT `tokenId` to query for a `tokenId` manager.
     */
    function getClaimApproved(uint256 tokenId) external view returns (address operator);
}

原理

术语

这些是围绕规范使用的基本术语,函数名称和定义基于这些术语。

  • vesting:归属 NFT 在未来日期之前归属的 token。
  • vested:归属 NFT 已归属的 token 总量。
  • claimable:可以解锁的已归属 token 的数量。
  • claimed:从归属 NFT 解锁的 token 总量。
  • timestamp:用于归属的日期的 unix timestamp(秒)表示。

归属功能

vestingPayout + vestedPayout

vestingPayout(uint256 tokenId)vestedPayout(uint256 tokenId) 加起来是可以由归属计划结束时声明的 token 总数。 这也等于 vestedPayoutAtTime(uint256 tokenId, uint256 timestamp),其中 type(uint256).max 作为 timestamp

这样做的理由是保证 token vested 和 token vesting 始终同步。 目的是使创建的归属曲线在整个 vestingPeriod 中是确定性的。 这为与这些 NFT 集成创造了有用的机会。 例如:可以迭代归属计划,并且可以在链上或链下可视化归属曲线。

vestedPayout vs claimedPayout & claimablePayout

vestedPayout - claimedPayout - claimablePayout = lockedPayout
  • vestedPayout(uint256 tokenId) 提供已归属 包括 claimedPayout(uint256 tokenId) 的付款 token 总量。
  • claimedPayout(uint256 tokenId) 提供在当前 timestamp 解锁的付款 token 总量。
  • claimablePayout(uint256 tokenId) 提供可以在当前 timestamp 解锁的付款 token 数量。

提供三个函数的基本原理是为了支持许多功能:

  1. vestedPayout(uint256 tokenId) 的返回值将始终与 vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) 的返回值匹配,其中 block.timestamp 作为 timestamp
  2. claimablePayout(uint256 tokenId) 可用于轻松查看当前的付款解锁金额,并通过在 timestamp 过去之前返回零来允许解锁悬崖。
  3. claimedPayout(uint256 tokenId) 有助于查看从 NFT 解锁的 token,对于计算归属但锁定的付款 token 也是必要的:vestedPayout - claimedPayout - claimablePayout = lockedPayout。 这将取决于此标准实施的归属曲线的配置方式。

vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) 提供通过 vestingPeriod(uint256 tokenId) 迭代并提供释放曲线视觉效果的功能。 目的是创建释放曲线,使 vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) 具有确定性。

时间戳

通常在 Solidity 开发中,建议不要使用 block.timestamp 作为状态依赖变量,因为区块的时间戳可以由矿工操纵。 选择使用 timestamp 而不是 block 是为了使接口可以在多个 Ethereum Virtual Machine (EVM) 兼容网络上工作,这些网络的区块时间通常不同。 具有显着伪造时间戳的区块提议通常会被所有节点实施放弃,这使得滥用窗口可以忽略不计。

timestamp 使跨链集成变得容易,但在内部,参考实现会跟踪每个归属 NFT 的 token 付款,以确保无法声明归属条款分配的过多 token。

范围限制

  • 历史声明:虽然可以使用 vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) 在链上确定历史归属计划,但历史声明需要通过历史交易数据来计算。 最有可能查询 PayoutClaimed 事件以构建历史图表。

扩展可能性

这些功能不受标准支持,但可以扩展该标准以支持这些更高级的功能。

  • 自定义归属曲线:此标准旨在在给定 NFT tokenId时间戳 作为输入的情况下返回确定性的 vesting 值。 这是故意的,因为它提供了底层归属曲线工作方式的灵活性,而不会约束那些打算构建复杂智能合约归属架构的项目。
  • NFT 租赁:如果可以出租归属 NFT,则可以创建进一步复杂的 DeFi 工具。

这样做是有意让基本标准保持简单。 这些功能可以并且很可能会通过此标准的扩展来添加。

向后兼容性

  • Vesting NFT 标准旨在与任何当前的 ERC-721 集成和市场完全向后兼容。
  • Vesting NFT 标准还支持 ERC-165 接口检测,用于检测 EIP-721 兼容性以及 Vesting NFT 兼容性。

测试用例

参考归属 NFT 存储库包括用 Hardhat 编写的测试。

参考实现

此 EIP 的参考实现可以在 ERC-5725 assets 中找到。

安全考虑

时间戳

  • 归属计划基于时间戳。 因此,重要的是跟踪已声明的 token 数量,并且不要给出超过特定归属 NFT 分配的 token 数量。
    • 例如,vestedPayoutAtTime(tokenId, type(uint256).max) 必须返回给定 tokenId 的总付款

批准

  • 当在归属 NFT 上进行 ERC-721 批准时,操作员有权将归属 NFT 转移给自己,然后声明已归属的 token。
  • 当在归属 NFT 上进行 ERC-5725 批准时,操作员有权声明已归属的 token,但不能将 NFT 从所有者处转移出去。

版权

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

Citation

Please cite this document as:

Apeguru (@Apegurus), Marco De Vries <marco@paladinsec.co>, Mario <mario@paladinsec.co>, DeFiFoFum (@DeFiFoFum), Elliott Green (@elliott-green), "ERC-5725: 可转移的归属 NFT," Ethereum Improvement Proposals, no. 5725, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5725.