ERC-7818 ERC-20 token with an expiry period (Expirable ERC-20)

Final · Tokens · created 2024-11-13

An extension standard that attaches an expiry period to an ERC-20 token. Once the set period has passed, the token expires and can no longer be transferred or used.

Why it is needed

A basic ERC-20 token has no concept of time. A token, once issued, stays valid forever, which makes it hard to represent assets such as bonds with a maturity date, or reward points, game items and prepaid credits that should disappear after a certain period. For example, to turn a benefit that must be used within a set period, such as a mobile carrier data coupon or cashback, into a token, the issuer has to reclaim tokens by hand at each expiry or manage validity on a separate server. This approach leads to incompatible, ad hoc implementations, and wallets and apps have no standard way to check when a token expires. This standard solves the problem by defining a common interface that enforces expiry rules at the contract level while remaining fully compatible with existing ERC-20.

What to implement

In depth

The epoch mechanism

The core unit of this standard is the epoch. An epoch represents a specific period or block range during which a token is valid, and there are two kinds. A block-based epoch (BLOCKS_BASED) is defined by a specific number of blocks (for example, 1000 blocks), and a time-based epoch (TIME_BASED) is defined by a duration in seconds (for example, 1000 seconds, in UNIX time). A token tied to an epoch is valid while that epoch is active. Once the set number of blocks or amount of time has passed, the epoch expires and the tokens belonging to it are treated as expired as well. Which method is used is queried through epochType, and the length of one epoch through epochLength.

Balance calculation looking back over past epochs

The available balance is obtained by summing the valid tokens from the current epoch back through the past n epochs, where n is the validity period (number of epochs) returned by validityDuration. In the source example, with 100 in epoch 1, 150 in epoch 2 and 200 in epoch 3, if the current epoch is 3 and tokens stay valid for 1 past epoch, only the tokens in epochs 2 and 3 are added together, giving an available balance of 350. balanceOf must return only the total of tokens that have not expired and must not include expired tokens. balanceOfAtEpoch returns the balance at a specific epoch, but it must return 0 if that epoch has expired, even if tokens were held there earlier.

Transfer rules

transfer and transferFrom must transfer only tokens that have not expired at the time of the transaction. An attempt to send expired tokens must revert the transaction or return false. Implementations MAY include priority logic that automatically spends the tokens closest to expiry first. transferAtEpoch and transferFromAtEpoch, which specify an epoch, must move the given amount of tokens held in the specified epoch to the recipient, and must likewise revert or return false if that epoch has expired. To make the reason for failure clear, implementations MAY use a standardized custom error such as ERC7818TransferredExpiredToken or ERC7818TransferredExpiredToken(address sender, uint256 epoch).

totalSupply handling and optional functions

Because it is hard to maintain a total that tracks only unexpired tokens, totalSupply SHOULD be set to 0 or type(uint256).max. The standard also presents optional functions that are useful though not required. getEpochBalance returns the amount of tokens stored in a given epoch, including expired epochs, and getEpochInfo returns the start and end of an epoch. getNearestExpiryOf returns the amount of tokens closest to expiry and the expected expiry block number or timestamp depending on epochType, and getRemainingDurationBeforeEpochChange returns the time or number of blocks remaining until the next epoch transition.

Why the epoch is kept abstract

Defining the term epoch abstractly allows a variety of implementations. An implementation can track tokens at a finer granularity within an epoch to control precisely when they become valid and when they expire, or it can use batch expiry, where all tokens in the same epoch expire at once. The epoch also lets expiry tracking be handled lazily. Instead of users or a separate service continuously updating expiry state with write operations, the current epoch can be computed with a read operation, which is more gas efficient.

Design choices in the reference implementation

The reference implementation automatically selects the tokens closest to expiry in first-in, first-out (FIFO) order using a sorted list, and uses a sliding window algorithm based on block.number instead of block.timestamp. The source points out that block.timestamp has been criticized for its security and resilience. In particular, as L2 usage grows, many L2s are centralized and unstable, and if a network halts, relying on timestamps directly affects asset integrity and can make the tokens effectively unusable. This standard is fully compatible with ERC-20.

Things to watch out for

Original ERC · ← ERCs