Alert Source Discuss
⚠️ Review Standards Track: ERC

ERC-7858: 可过期的 NFT 和 SBT

具有过期功能的非同质化代币 (NFT) 和 Soulbound 代币 (SBT),支持有时限的用例。

Authors sirawt (@MASDXI), ADISAKBOONMARK (@ADISAKBOONMARK), parametprame (@parametprame), Nacharoen (@najaroen)
Created 2024-01-04
Requires EIP-165, EIP-721

摘要

ERC-721 非同质化代币 (NFT) 和 Soulbound 代币 (SBT) 引入了一个扩展,添加了一种过期机制,允许代币在预定义的时间段后失效。这个额外的功能层确保了过期机制不会干扰现有的 NFT 或 SBT,从而保持了 NFT 的可转移性以及与当前 DApp(如 NFT 市场)的兼容性。可以使用区块高度或时间戳来定义过期时间,从而为各种用例提供灵活性。

动机

在此 EIP 之前,NFT 和 SBT 通过自定义映射实现过期机制。然而,这种方法存在挑战,例如与其他智能合约的集成不一致,自定义的 ERC-721 实现在 NFT 市场中使用时可能会出现异常行为。此 EIP 通过提供与现有基础设施兼容的内置过期机制和接口来解决这些问题。它支持每个代币和基于 epoch 的过期,允许开发人员为他们的用例选择合适的过期类型,包括:

  • 访问和身份验证
    • 用于身份和访问管理 (IAM) 的身份验证
    • 用于会员管理系统 (MMS) 的会员资格
    • ERC-2135ERC-7578 一起使用时,用于会议、奖励旅游、大会和展览 (MICE) 的门票和媒体通行证。
    • 基于订阅的数字平台访问。
  • 数字认证、合同、版权、文档、许可证、政策等。
  • 忠诚度计划凭证或优惠券
  • 治理和投票权
  • 金融产品
    • 债券、贷款、对冲和期权合约

规范

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

接口

// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;

/**
 * @title ERC-7858: Expirable NFTs and SBTs
 * @notice unique/granular expiry
 */

// import "./IERC721.sol";

// The EIP-165 identifier of this interface is `0x3ebdfa31`.
interface IERC7858 /**is IERC721 */ {
    enum EXPIRY_TYPE {
        BLOCKS_BASED, // block.number
        TIME_BASED // block.timestamp
    }
    
    /**
     * @dev Emitted when the expiration date of a token is set or updated.
     * @param tokenId The identifier of the token ERC721 `tokenId`.
     * @param startTime The start time of the token (block number or timestamp based on `expiryType`).
     * @param endTime The end time of the token (block number or timestamp based on `expiryType`).
     */
    event TokenExpiryUpdated(
        uint256 indexed tokenId,
        uint256 indexed startTime,
        uint256 indexed endTime
    );

    /**
     * @dev Returns the type of the expiry.
     * @return EXPIRY_TYPE  Enum value indicating the unit of an expiry.
     */
    function expiryType() external view returns (EXPIRY_TYPE);

    /**
     * @dev Checks whether a specific token is expired.
     * @param tokenId The identifier representing the `tokenId` (ERC721).
     * @return bool True if the token is expired, false otherwise.
     */
    function isTokenExpired(uint256 tokenId) external view returns (bool);

    // return depends on the type `block.timestamp` or `block.number`
    // {ERC-5007} return in uint64 MAY not suitable for `block.number` based.
    function startTime(uint256 tokenId) external view returns (uint256);
    function endTime(uint256 tokenId) external view returns (uint256);
}

行为规范

  • ERC-721 继承的 balanceOf MUST 返回所有代币,即使已过期;它们仍然存在,但由于链上跟踪过期代币的限制而无法使用。
  • 对于 NFT,transferFromsafeTransferFrom MUST 允许转移代币,即使它们已过期。这确保了过期的代币仍然可以转移和交易,从而保持与已部署的现有应用程序的兼容性。但是,过期的代币在检查代币有效性的合约中 MUST 被视为无效和无法使用。
  • expiryType MUST 返回合约使用的过期类型,可以是 BLOCKTIME
  • isTokenExpired 用于检索给定 tokenId 的状态,如果代币已过期,该函数 MUST 返回 true,如果 tokenId 不存在,则 MUST 回退,对于使用自定义错误的实现 SHOULD 使用 ERC721NonexistentToken 回退,遵循相关的 ERC-6093,对于使用低于 v0.8.4 的 Solidity 版本或那些喜欢使用字符串错误回退的实现 SHOULD 使用 NonexistToken 回退。如果 tokenId 存在且未过期,则该函数 MUST 返回 false
  • tokenIdstartTimeendTime 可以是 block.numberblock.timestamp,具体取决于 expiryTypestartTime MUST 小于 endTime,并且 SHOULD 除当两者都设置为 0 时。startTimeendTime0 表示 tokenId 没有时间限制。如果 tokenId 不存在,则 MUST 与函数 isTokenExpired 中一样回退。
  • IERC7858supportInterface0x3ebdfa31IERC7858EpochsupportInterface0x8f55b98a
  • 当代币被铸造或其过期详细信息(startTimeendTime)被更新时,MUST 发出 TokenExpiryUpdated

扩展接口

Epochs 代表一个特定的时期或区块范围,在此期间某些代币是有效的,借鉴了 ERC-7818 的概念,代币被分组在一个 epoch 下,并共享相同的 validityDuration

// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;

/**
 * @title ERC-7858: Expirable NFTs and SBTs
 * @notice epoch expiry extension
 */

// import "./IERC7858.sol";

// The EIP-165 identifier of this interface is `0x8f55b98a`.
interface IERC7858Epoch /** is IERC7858 */ {
    /**
     * @dev Retrieves the current epoch of the contract.
     * @return uint256 The current epoch of the token contract,
     * often used for determining active/expired states.
     */
    function currentEpoch() external view returns (uint256);

    /**
     * @dev Retrieves the duration of a single epoch.
     * @return uint256 The duration of a single epoch.
     * @notice The unit of the epoch length is determined by the `validityPeriodType` function.
     */
    function epochLength() external view returns (uint256);
    
    /**
     * @dev Returns the type of the epoch.
     * @return EXPIRY_TYPE  Enum value indicating the unit of an epoch.
     */
    function epochType() external view returns (EXPIRY_TYPE);

    /**
     * @dev Checks whether a specific `epoch` is expired.
     * @param epoch The `epoch` to check.
     * @return bool True if the token is expired, false otherwise.
     * @notice Implementing contracts "MUST" define and document the logic for determining expiration,
     * typically by comparing the latest epoch with the given `epoch` value,
     * based on the `EXPIRY_TYPE` measurement (e.g., block count or time duration).
     */
    function isEpochExpired(uint256 epoch) external view returns (bool);

    /**
     * @dev Retrieves the balance of unexpired tokens owned by an account.
     * @param account The address of the account.
     * @return uint256 The amount of unexpired tokens owned by an account.
     */
    function unexpiredBalanceOf(address account) external view returns (uint256);

    /**
     * @dev Retrieves the balance of a specific `epoch` owned by an account.
     * @param epoch The `epoch for which the balance is checked.
     * @param account The address of the account.
     * @return uint256 The balance of the specified `epoch`.
     * @notice "MUST" return 0 if the specified `epoch` is expired.
     */
    function unexpiredBalanceOfAtEpoch(uint256 epoch, address account) external view returns (uint256);

    /**
     * @dev Retrieves the validity duration of each token.
     * @return uint256 The validity duration of each token in `epoch` unit.
     */
    function validityDuration() external view returns (uint256);
}
  • unexpiredBalanceOfAtEpoch MUST 返回帐户在指定 epoch 持有的未过期或可用的代币余额,如果指定的 epoch 已过期,则此函数 MUST 返回 0。例如,如果 epoch 5 已过期,则调用 unexpiredBalanceOfAtEpoch(5, address) 返回 0,即使之前在该 epoch 中持有代币。
  • unexpiredBalanceOf MUST 仅返回未过期或可用的代币。
  • currentEpoch MUST 返回合约的当前 epoch
  • epochLength MUST 返回 epoch 之间的持续时间(以区块或秒为单位的时间)。
  • epochType MUST 返回合约使用的 epoch 类型,可以是 BLOCKS_BASEDTIME_BASED
  • validityDuration MUST 返回代币的有效期限(以 epoch 计数为单位)。
  • 如果给定的 epoch 已过期,isEpochExpired MUST 返回 true,否则返回 false

其他潜在的有用函数

这些 OPTIONAL 函数提供了额外的功能,这些功能可能根据具体的用例很有用。

基础(默认)

function getRemainingDurationBeforeTokenExpired(uint256 tokenId) public view returns (uint256);
  • getRemainingDurationBeforeTokenExpired 返回给定的 tokenId 过期前的剩余时间或区块。

Epoch(扩展)

function getEpochBalance(uint256 epoch) public view returns (uint256);
  • getEpochBalance 返回存储在给定 epoch 中的代币数量,即使该 epoch 已过期。
function getEpochInfo(uint256 epoch) public view returns (uint256,uint256);
  • getEpochInfo 返回指定 epoch 的开始和结束时间。
function getNearestExpiryOf(uint256 tokenId) public view returns (uint256);
  • getNearestExpiryOf 返回最接近过期的 tokenId 列表,以及基于 epochType 的估计过期区块号或时间戳。
function getRemainingDurationBeforeEpochChange() public view returns (uint256);
  • getRemainingDurationBeforeEpochChange 返回在发生 epoch 更改之前剩余的时间或区块,基于 epochType

理由

首先,不要造成伤害

引入可过期性作为额外的功能层,可确保它不会干扰现有的用例或应用程序。对于非 SBT 代币,可转移性保持不变,从而保持与当前系统的兼容性。过期的代币仅在有效性检查期间被标记为不可用,将过期视为增强功能,而不是根本性的变化。

过期类型

通过区块高度 (block.number) 或区块时间戳 (block.timestamp) 定义过期为各种用例提供了灵活性。基于区块的过期适用于依赖于网络活动并要求精确一致性的应用程序,而基于时间的过期非常适合具有可变区块间隔的网络。

向后兼容性

此标准与 ERC-721ERC-5484 和其他 SBT 完全兼容。

参考实现

您可以在此处找到我们的参考实现。

安全注意事项

销毁和重新铸造

实现应确保销毁代币并使用相同的 tokenId 重新铸造它不会引入未经授权的续订。

版权

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

Citation

Please cite this document as:

sirawt (@MASDXI), ADISAKBOONMARK (@ADISAKBOONMARK), parametprame (@parametprame), Nacharoen (@najaroen), "ERC-7858: 可过期的 NFT 和 SBT [DRAFT]," Ethereum Improvement Proposals, no. 7858, January 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7858.