Expirable ERC-20
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 specifiedepoch
, If the specified epoch is expired, this function MUST return0
. For example, ifepoch
5 has expired, callingbalanceOfByEpoch(5, address)
returns0
even if there were tokens previously held in that epoch.currentEpoch
MUST return the currentepoch
of the contract.epochLength
MUST return duration betweenepoch
in blocks or time in seconds.epochType
MUST return the type of epoch used by the contract, which can be eitherBLOCKS_BASED
orTIME_BASED
.validityDuration
MUST return the validity duration of tokens in terms ofepoch
counts.isEpochExpired
MUST return true if the givenepoch
is expired, otherwisefalse
.transfer
andtransferFrom
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
andtransferFromAtEpoch
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 MUSTrevert
or returnfalse
totalSupply
SHOULD be set to0
ortype(uint256).max
due to the challenges of tracking only valid (non-expired) tokens.- The implementation MAY use a standardized custom error, such as
ERC7818TransferredExpiredToken
orERC7818TransferredExpiredToken(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 givenepoch
, even if theepoch
has expired.getEpochInfo
returns both the start and end of the specifiedepoch
.getNearestExpiryOf
returns the token amount closest to expiration, along with an estimated expiration block number or timestamp based onepochType
.getRemainingDurationBeforeEpochChange
returns the remaining time or blocks before theepoch
change happens, based on theepochType
.
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
Copyright and related rights waived via CC0.