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
pragmasolidity>=0.8.0<0.9.0;/**
* @title ERC-7818 interface
* @dev Interface for adding expirable functionality to ERC20 tokens.
*/import"./IERC20.sol";interfaceIERC7818isIERC20{/**
* @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.
*/enumEPOCH_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.
*/functionbalanceOfAtEpoch(uint256epoch,addressaccount)externalviewreturns(uint256);/**
* @dev Retrieves the latest epoch currently tracked by the contract.
* @return uint256 The latest epoch of the contract.
*/functioncurrentEpoch()externalviewreturns(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.
*/functionepochLength()externalviewreturns(uint256);/**
* @dev Returns the type of the epoch.
* @return EPOCH_TYPE Enum value indicating the unit of an epoch.
*/functionepochType()externalviewreturns(EPOCH_TYPE);/**
* @dev Retrieves the validity duration in `epoch` counts.
* @return uint256 The validity duration in `epoch` counts.
*/functionvalidityDuration()externalviewreturns(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).
*/functionisEpochExpired(uint256epoch)externalviewreturns(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.
*/functiontransferAtEpoch(uint256epoch,addressto,uint256value)externalreturns(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.
*/functiontransferFromAtEpoch(uint256epoch,addressfrom,addressto,uint256value)externalreturns(bool);}
Behavior specification
balanceOfMUST 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.
balanceOfAtEpochMUST 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.
currentEpochMUST return the current epoch of the contract.
epochLengthMUST return duration between epoch in blocks or time in seconds.
epochTypeMUST return the type of epoch used by the contract, which can be either BLOCKS_BASED or TIME_BASED.
validityDurationMUST return the validity duration of tokens in terms of epoch counts.
isEpochExpiredMUST return true if the given epoch is expired, otherwise false.
transfer and transferFromMUST 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 transferFromAtEpochMUST transfer the specified number of tokens held by an account at the specified epoch to the recipient, If the epoch has expired, the transaction MUSTrevert or return false
totalSupplySHOULD 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.
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.