ERC-7818: Expirable ERC20

Update 14/11/2024 Link to PR on github
Update 18/11/2024 Revised to make it more straightforward design, moving away from a library-based structure
Update 20/11/2024 Update in section Security Considerations

Simple Summary

An extended interface enables fungible tokens to possess expiration capabilities, allowing them to expire after a predetermined period.

Abstract

Introduces an extension for ERC20 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 ERC20 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 ERC20 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.

Implementers of this REQUIRED inheritance ERC20 interface and MUST have all of 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: Expirable ERC20
 * @dev Interface for creating expirable ERC20 tokens.
 */

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IERC7818 is IERC20 {

    /**
     * @dev Retrieves the balance of a specific `identifier` owned by an account.
     * @param account The address of the account.
     * @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
     * @return uint256 The balance of the specified `identifier`.
     * @notice `identifier` "MUST" represent a unique identifier, and its meaning "SHOULD"
     * align with how contract maintain the `epoch` in the implementing contract.
     */
    function balanceOf(
        address account,
        uint256 identifier
    ) external view returns (uint256);

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

    /**
     * @dev Retrieves the duration a token remains valid.
     * @return uint256 The validity duration.
     * @notice `duration` "MUST" specify the token's validity period.
     * The implementing contract "SHOULD" clearly document,
     * whether the unit is blocks or time in seconds.
     */
    function duration() external view returns (uint256);

    /**
     * @dev Checks whether a specific `identifier` is expired.
     * @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
     * @return bool True if the token is expired, false otherwise.
     * @notice Implementing contracts "MUST" define the logic for determining expiration,
     * typically by comparing the current `epoch()` with the given `identifier`.
     */
    function expired(uint256 identifier) external view returns (bool);

    /**
     * @dev Transfers a specific `identifier` and value to a recipient.
     * @param to The recipient address.
     * @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
     * @param value The amount to transfer.
     * @return bool True if the transfer succeeded, false or reverted if give `identifier` it's expired.
     * @notice The transfer "MUST" revert if the token `identifier` is expired.
     */
    function transfer(
        address to,
        uint256 identifier,
        uint256 value
    ) external returns (bool);

    /**
     * @dev Transfers a specific `identifier` and value from one account to another.
     * @param from The sender's address.
     * @param to The recipient's address.
     * @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
     * @param value The amount to transfer.
     * @return bool True if the transfer succeeded, false or reverted if give `identifier` it's expired.
     * @notice The transfer "MUST" revert if the token `identifier` is expired.
     */
    function transferFrom(
        address from,
        address to,
        uint256 identifier,
        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 periods, epochs, or other identifiers, 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.
  • 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.
  • 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 revert message, such as ERC7818TransferredExpiredToken or ERC7818TransferredExpiredToken(address sender, uint256 identifier), to clearly indicate that the operation failed due to attempting to transfer expired tokens.

Rationale

The rationale for developing an expirable ERC20 token extension is based on several key requirements that ensure its practicality and adaptability for various applications to

Compatibility with the existing ERC-20 standard.

The extension should integrate smoothly with the ERC20 interface, This ensures compatibility with existing token ecosystems and third-party tools like wallets and blockchain explorers.

Flexible interface for various implementations.

The smart contract should be extensible, allowing businesses to tailor the expiration functionality to their specific needs like expiry in bulk or each token independent expire, whether it’s dynamic reward systems or time-sensitive applications.

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

SC06: Denial Of Service

Run out of gas problem due to the operation consuming high gas used if transferring multiple groups of small tokens.

SC09: Gas Limit Vulnerabilities

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

SWC116: Block values as a proxy for time

if using block.timestamp for calculating epoch()

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.

Historical links related to this standard

  • Example Implementation from TokenX
  • Example Implementation from Tokenine
  • Example Implementation from Bitkub
  • Ethereum stack exchange question #27379
  • Ethereum stack exchange question #63937
3 Likes

I like the use cases. My concern is that at the point that the core transfer and balance method signatures are being changed from ERC20, why this wouldn’t make more sense as ERC-1155 extension, especially since there’s identifiers being used. balanceOf is the same siganture in ERC1155 as well

Hi @0xTraub, Thanks for your feedback and your concern about the function signature of overloading balanceOf with the identifier did you think it should be changed to something like balanceOfByIdentifier borrowing style form ERC-1400

The reason I am not proposing this as an extension to the ERC-1155. The specification would need to spread down into multiple behaviors and may become more complicated when implemented, I think ERC-7818 is more fungible because the expiration date becomes a shared, uniform trait across tokens.

  • ERC20 can be split into 2 characteristics (Fungible Token) (Uniform)
    • BulkExpire All tokens have the same expiration date. very straight forward

      • Example: seasonal loyalty points, bonds, or simple governance rights issued in limited amounts, which expire together under a single rule. In such cases, the
        ability to mint new tokens continuously may not be suitable.

      Behavior: All tokens expired after block x

    • IndependentExpire Each token has an individual expiration date.

      • Example: Common loyalty points, commodities have a life span, data packages, or e-money where each token can have a unique expiration, allowing more flexibility in managing user rewards.

      Behavior: Each token is valid for n blocks


  • ERC721 can be split into multiple characteristics (Non-Fungible Token) (Unique)
    • BulkExpire All tokenId have the same expiration date.

      • Example: seasonal coupons, vouchers, and rights, intended for services that affect a broad user base, where a uniform expiration is necessary.

      Behavior: All tokens were minted on the different blocks but expired at block x (same block)

    • IndependentExpireByTokenId Each tokenId has an individual expiration date.

      • Example: Very special coupon, or privileges tailored to individual users, where each token can expire independently.

      Behavior:
      tokenId 1 minted on block a and expired at block x
      tokenId 2 minted on block b and expired at block y
      tokenId 3 minted on block c and expired at block z

    • IndependentExpire Each tokenId has an individual expiration date but has the same valid period or duration.

      • Example: a very special coupons, vouchers, or rights can’t be stack

      Behavior: Each tokenId is valid for n blocks


  • ERC1155 can be split into multiple characteristics (Semi-Fungible Token) (Mixed)
    • BulkExpire All tokenId under the same smart contract has the same expiration date.

      • Example: vouchers, coupons, docs, or event tickets multiple types,s in the same contract, where all tokens within the same smart contract expire simultaneously.

      Behavior: All tokens of tokenId1, tokenId2, and tokenId3 expired at block x (same block)

    • BulkExpireByType All tokens under the same type Id have the same expiration date.

      • Example: limited quantity coupons, vouchers, and time-limited game items for events e.g. for loyalty use cases is Black Friday or Flash Sales, all tokens of a specific type (same id) expire simultaneously.

      Behavior:
      All tokens of token type Id 1 are valid for n1 blocks
      All tokens of token type Id 2 are valid for n2 blocks

    • IndependentExpireByType Each token under the same tokenId has an individual expiration date.

      • Example: casual coupon, voucher, and temporary access right within the same category can expire at different times.

      Behavior:
      Each token of token type Id 1 are valid for n1 blocks
      Each token of token type Id 2 are valid for n2 blocks,