Abstract

Introduces an extension for ERC-20 tokens, which facilitates the implementation of an expiration mechanism. Through this extension, tokens have a predetermined validity period, after which they become invalid and can no longer be transferred or used. This functionality proves beneficial in scenarios such as time-limited bonds, loyalty rewards, or game tokens necessitating automatic invalidation after a specific duration. The extension is crafted to seamlessly align with the existing ERC-20 standard, ensuring smooth integration with the prevailing token smart contract while introducing the capability to govern and enforce token expiration at the contract level.

Motivation

This extension facilitates the development of ERC-20 standard compatible tokens featuring expiration dates. This capability broadens the scope of potential applications, particularly those involving time-sensitive assets. Expirable tokens are well-suited for scenarios necessitating temporary validity, including:

  • Bonds or financial instruments with defined maturity dates
  • Time-constrained assets within gaming ecosystems
  • Next-gen loyalty programs incorporating expiring rewards or points
  • Prepaid credits for utilities or services (e.g., cashback, data packages, fuel, computing resources) that expire if not used within a specified time frame
  • Postpaid telecom data package allocations that expire at the end of the billing cycle, motivating users to utilize their data before it resets
  • Tokenized e-Money for a closed-loop ecosystem, such as transportation, food court, and retail payments

Specification

The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Epoch Mechanism

Epochs represent a specific period or block range during which certain tokens are valid. They can be categorized into two types

  • block-based Defined by a specific number of blocks (e.g., 1000 blocks).
  • time-based Defined by a specific duration in seconds (e.g., 1000 seconds).

Tokens linked to an epoch remain valid as long as the epoch is active. Once the specified number of blocks or the duration in seconds has passed, the epoch expires, and any tokens associated with it are considered expired.

Balance Look Back Over Epochs

To retrieve the usable balance, tokens are checked from the current epoch against a past epoch (which can be any n epochs back). The past epoch can be set to any value n, allowing flexibility in tracking and summing tokens that are still valid from previous epochs, up to n epochs back.

The usable balance is the sum of tokens valid between the current epoch and the past epoch, ensuring that only non-expired tokens are considered.

Example Scenario

epoch balance
1 100
2 150
3 200
  • Current Epoch: 3
  • Past Epoch: 1 epoch back
  • Usable Balance: 350

Tokens from Epoch 2 and Epoch 3 are valid. The same logic applies for any n epochs back, where the usable balance includes tokens from the current epoch and all prior valid epochs.

Compatible implementations MUST inherit from ERC-20’s interface and MUST have all the following functions and all function behavior MUST meet the specification.

// 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);
}

Behavior specification

  • balanceOf MUST return the total balance of tokens held by an account that are still valid (i.e., have not expired). This includes any tokens associated with specific epochs, provided they remain within their validity duration. Expired tokens MUST NOT be included in the returned balance, ensuring that only actively usable tokens are reflected in the result.
  • balanceOfAtEpoch MUST returns the balance of tokens held by an account at the specified epoch, If the specified epoch is expired, this function MUST return 0. For example, if epoch 5 has expired, calling balanceOfByEpoch(5, address) returns 0 even if there were tokens previously held in that epoch.
  • currentEpoch MUST return the current epoch of the contract.
  • epochLength MUST return duration between epoch in blocks or time in seconds.
  • epochType MUST return the type of epoch used by the contract, which can be either BLOCKS_BASED or TIME_BASED.
  • validityDuration MUST return the validity duration of tokens in terms of epoch counts.
  • isEpochExpired MUST return true if the given epoch is expired, otherwise false.
  • transfer and transferFrom MUST exclusively transfer tokens that remain non-expired at the time of the transaction. Attempting to transfer expired tokens MUST revert the transaction or return false. Additionally, implementations MAY include logic to prioritize the automatic transfer of tokens closest to expiration, ensuring that the earliest expiring tokens are used first, provided they meet the non-expired condition.
  • transferAtEpoch and transferFromAtEpoch MUST transfer the specified number of tokens held by an account at the specified epoch to the recipient, If the epoch has expired, the transaction MUST revert or return false
  • totalSupply SHOULD be set to 0 or type(uint256).max due to the challenges of tracking only valid (non-expired) tokens.
  • The implementation MAY use a standardized custom error, such as ERC7818TransferredExpiredToken or ERC7818TransferredExpiredToken(address sender, uint256 epoch), to clearly indicate that the operation failed due to attempting to transfer expired tokens.

Additional Potential Useful Function

These OPTIONAL functions provide additional functionality that might be useful depending on the specific use case.

  • getEpochBalance returns the amount of tokens stored in a given epoch, even if the epoch has expired.
  • getEpochInfo returns both the start and end of the specified epoch.
  • getNearestExpiryOf returns the token amount closest to expiration, along with an estimated expiration block number or timestamp based on epochType.
  • getRemainingDurationBeforeEpochChange returns the remaining time or blocks before the epoch change happens, based on the epochType.

Rationale

Although the term epoch is an abstract concept, it leaves room for various implementations. For example, epochs can support more granular tracking of tokens within each epoch, allowing for greater control over when tokens are valid or expired on-chain. Alternatively, epochs can support bulk expiration, where all tokens within the same epoch expire simultaneously. This flexibility enables different methods of tracking token expiration, depending on the specific needs of the use case. epoch also introduces a “lazy” way to simplify token expiration tracking in a flexible and gas-efficient manner. Instead of continuously updating the expiration state with write operations by the user or additional services, the current epoch can be calculated using a read operation.

Backwards Compatibility

This standard is fully ERC-20 compatible.

Reference Implementation

For reference implementation can be found here, But in the reference implementation, we employ a sorted list to automatically select the token that nearest expires first with a First-In-First-Out (FIFO) and sliding window algorithm that operates based on the block.number as opposed to relying on block.timestamp, which has been criticized for its lack of security and resilience, particularly given the increasing usage of Layer 2 (L2) networks over Layer 1 (L1) networks. Many L2 networks exhibit centralization and instability, which directly impacts asset integrity, rendering them potentially unusable during periods of network halting, as they are still reliant on the timestamp.

Security Considerations

Denial Of Service

Run out of gas problem due to the operation consuming higher gas if transferring multiple groups of small tokens or loop transfer.

Gas Limit Vulnerabilities

Exceeds block gas limit if the blockchain has a block gas limit lower than the gas used in the transaction.

Block values as a proxy for time

if using block.timestamp for calculating epoch() and In rare network halts, block production stops, freezing block.timestamp and disrupting time-based logic. This risks asset integrity and inconsistent states.

Fairness Concerns

In a straightforward implementation, where all tokens within the same epoch share the same expiration (e.g., at epoch:x), bulk expiration occurs.

Risks in Liquidity Pools

When tokens with expiration dates are deposited into liquidity pools (e.g., in DEXs), they may expire while still in the pool.

Copyright and related rights waived via CC0.