Alert Source Discuss
Standards Track: ERC

ERC-7818: 可过期的 ERC-20

一个用于创建具有过期时间的可替换代币的 ERC-20 扩展,支持有时限的使用场景。

Authors sirawt (@MASDXI), ADISAKBOONMARK (@ADISAKBOONMARK)
Created 2024-11-13
Requires EIP-20

摘要

引入 ERC-20 代币的扩展,该扩展有助于实现过期机制。通过此扩展,代币具有预定的有效期,在此之后,它们将变为无效,并且不能再被转移或使用。此功能在诸如有时限的债券、忠诚度奖励或需要在特定持续时间后自动失效的游戏代币等场景中被证明是有益的。该扩展旨在与现有的 ERC-20 标准无缝对齐,确保与现有代币智能合约的平滑集成,同时引入在合约级别管理和强制执行代币过期的能力。

动机

此扩展有助于开发具有过期日期的 ERC-20 标准兼容代币。此功能拓宽了潜在应用程序的范围,特别是那些涉及时间敏感资产的应用程序。可过期代币非常适合需要临时有效性的场景,包括:

  • 具有明确到期日的债券或金融工具
  • 游戏生态系统内的时间限制资产
  • 包含过期奖励或积分的下一代忠诚度计划
  • 用于公用事业或服务的预付费积分(例如,现金返还、数据包、燃料、计算资源),如果在指定时间内未使用则过期
  • 后付费电信数据包分配在结算周期结束时过期,促使用户在使用数据重置之前使用其数据
  • 用于闭环生态系统的代币化电子货币,例如交通、美食广场和零售支付

规范

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

Epoch 机制

Epochs(纪元) 表示特定时期或区块范围,在此期间某些代币有效。它们可以分为两种类型

  • 基于区块 由特定数量的区块定义(例如,1000 个 blocks)。
  • 基于时间 由特定的持续时间(以秒为单位)定义(例如,1000 seconds)。

epoch 关联的代币只要 epoch 处于活动状态就保持有效。一旦指定的 blocks 数量或 seconds 中的持续时间过去,epoch 就会过期,并且与其关联的任何代币都被视为已过期。

跨 Epoch 查询余额

要检索可用余额,需要从当前 epoch 中检查代币,并与过去 epoch 进行比较(它可以是往回任何 n 个 epochs)。过去 epoch 可以设置为任何值 n,从而可以灵活地跟踪和求和从过去 epoch 仍然有效的代币,最多可以回溯 n 个 epochs。

可用余额是在当前 epoch过去 epoch 之间有效的代币的总和,以确保仅考虑未过期的代币。

示例场景

epoch balance
1 100
2 150
3 200
  • 当前 Epoch:3
  • 过去 Epoch:往回 1 个 epoch
  • 可用余额:350

来自 Epoch 2Epoch 3 的代币有效。相同的逻辑适用于任何 n 个 epochs,其中可用余额包括来自当前 epoch 和所有先前有效 epoch 的代币。

兼容的实现 MUST 继承自 ERC-20 的接口,并且 MUST 具有以下所有函数,并且所有函数行为 MUST 符合规范。

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

/**
 * @title ERC-7818 interface
 * @dev Interface for adding expirable functionality to ERC20 tokens.
 */

import "./IERC20.sol";

interface IERC7818 is IERC20 {
    /**
     * @dev Enum represents the types of `epoch` that can be used.
     * @notice The implementing contract may use one of these types to define how the `epoch` is measured.
     */
    enum EPOCH_TYPE {
        BLOCKS_BASED, // measured in the number of blocks (e.g., 1000 blocks)
        TIME_BASED // measured in seconds (UNIX time) (e.g., 1000 seconds)
    }

    /**
     * @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 balanceOfAtEpoch(
        uint256 epoch,
        address account
    ) external view returns (uint256);

    /**
     * @dev Retrieves the latest epoch currently tracked by the contract.
     * @return uint256 The latest epoch of the contract.
     */
    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 EPOCH_TYPE  Enum value indicating the unit of an epoch.
     */
    function epochType() external view returns (EPOCH_TYPE);
    
    /**
     * @dev Retrieves the validity duration in `epoch` counts.
     * @return uint256 The validity duration in `epoch` counts.
     */
    function validityDuration() external view returns (uint256);

    /**
     * @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 `EPOCH_TYPE` measurement (e.g., block count or time duration).
     */
    function isEpochExpired(uint256 epoch) external view returns (bool);

    /**
     * @dev Transfers a specific `epoch` and value to a recipient.
     * @param epoch The `epoch` for the transfer.
     * @param to The recipient address.
     * @param value The amount to transfer.
     * @return bool True if the transfer succeeded, otherwise false.
     */
    function transferAtEpoch(
        uint256 epoch,
        address to,
        uint256 value
    ) external returns (bool);

    /**
     * @dev Transfers a specific `epoch` and value from one account to another.
     * @param epoch The `epoch` for the transfer.
     * @param from The sender's address.
     * @param to The recipient's address.
     * @param value The amount to transfer.
     * @return bool True if the transfer succeeded, otherwise false.
     */
    function transferFromAtEpoch(
        uint256 epoch,
        address from,
        address to,
        uint256 value
    ) external returns (bool);
}

行为规范

  • balanceOf MUST 返回帐户持有的仍然有效(即尚未过期)的代币总余额。 这包括与特定 epochs 关联的任何代币,前提是它们保持在其有效期内。 过期的代币 MUST NOT 包含在返回的余额中,以确保结果中仅反映有效可用的代币。
  • balanceOfAtEpoch MUST 返回帐户在指定的 epoch 时持有的代币余额。如果指定的 epoch 已过期,则此函数 MUST 返回 0。 例如,如果 epoch 5 已过期,则调用 balanceOfByEpoch(5, address) 将返回 0,即使之前在该 epoch 中持有过代币。
  • currentEpoch MUST 返回合约的当前 epoch
  • epochLength MUST 返回 epoch 之间以区块或时间(以秒为单位)为单位的持续时间。
  • epochType MUST 返回合约使用的 epoch 类型,它可以是 BLOCKS_BASEDTIME_BASED
  • validityDuration MUST 返回以 epoch 计数表示的代币有效期。
  • isEpochExpired MUST 在给定的 epoch 过期时返回 true,否则返回 false
  • transfertransferFrom MUST 专门转移在交易时仍然未过期的代币。 尝试转移过期的代币 MUST 恢复交易或返回 false。 此外,实现 MAY 包括逻辑,以优先自动转移最接近到期的代币,确保首先使用最早到期的代币,前提是它们满足未过期的条件。
  • transferAtEpochtransferFromAtEpoch MUST 将指定 epoch 由账户持有的指定数量的代币转移给接收者,如果该 epoch 已经过期,则事务 MUST revert 或返回 false
  • 由于仅跟踪有效(未过期)代币的挑战,totalSupply SHOULD 设置为 0type(uint256).max
  • 该实现 MAY 使用标准化的自定义错误,例如 ERC7818TransferredExpiredTokenERC7818TransferredExpiredToken(address sender, uint256 epoch),以清楚地表明由于尝试转移过期的代币而导致操作失败。

其他潜在有用的函数

这些 OPTIONAL 函数提供了额外的功能,具体取决于特定的用例,这些功能可能会很有用。

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

原理

尽管术语 epoch 是一个抽象概念,但它为各种实现留出了空间。 例如,epochs 可以支持对每个 epoch 中代币的更精细的跟踪,从而可以更好地控制链上代币何时有效或过期。 另外,epochs 可以支持批量过期,其中同一 epoch 中的所有代币同时过期。 这种灵活性使得可以采用不同的方法来跟踪代币过期,具体取决于用例的特定需求。 epoch 还引入了一种“lazy”方式,以灵活且节省 gas 的方式简化代币过期跟踪。 无需用户或额外服务通过 write 操作不断更新过期状态,而是可以使用 read 操作来计算当前 epoch。

向后兼容性

此标准与 ERC-20 完全兼容。

参考实现

有关参考实现,请参见 此处,但是在参考实现中,我们采用排序列表来自动选择最接近到期的代币,并采用先进先出(FIFO)和滑动窗口算法,该算法基于 block.number 运行,而不是依赖于因缺乏安全性和弹性而受到批评的 block.timestamp,尤其是在 Layer 2 (L2) 网络的使用量超过 Layer 1 (L1) 网络的情况下。 许多 L2 网络表现出中心化和不稳定性,这直接影响了资产完整性,使得它们在网络停止期间可能无法使用,因为它们仍然依赖于时间戳。

安全注意事项

拒绝服务

如果传输多个小组的小型代币或循环传输,则由于操作消耗更高的 gas 而导致 gas 不足问题。

Gas 限制漏洞

如果区块链的区块 gas 限制低于交易中使用的 gas,则超出区块 gas 限制。

将区块值作为时间的代理

如果使用 block.timestamp 计算 epoch,并且在极少数网络停止的情况下,阻止了区块生产,冻结了 block.timestamp 并扰乱了基于时间的逻辑。 这会威胁资产完整性和状态不一致。

公平性问题

在一个简单的实现中,其中同一 epoch 中的所有代币共享相同的到期时间(例如,在 epoch:x 时),则会发生批量到期。

流动性池中的风险

当具有到期日期的代币存入流动性池(例如,在 DEX 中)时,它们可能会在池中过期。

版权

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

sirawt (@MASDXI), ADISAKBOONMARK (@ADISAKBOONMARK), "ERC-7818: 可过期的 ERC-20," Ethereum Improvement Proposals, no. 7818, November 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7818.