ERC-7858 NFTs and SBTs with an Expiry (Expirable NFTs and SBTs)

Final · NFTs & multi-token · created 2024-01-04

An ERC-721 extension standard that adds a validity period to NFTs and soulbound tokens (SBTs). When a set block number or time passes, the token becomes expired, which makes it possible to represent time-limited uses such as memberships, tickets, and subscriptions.

Why it is needed

To make something that must be valid only for a certain period, such as a membership, subscription, event ticket, coupon, or lease, into an NFT or SBT, the token needs an expiry point. Before this standard, each project implemented expiry with its own mapping. As a result, integration with other smart contracts was inconsistent, and ERC-721 implementations that deviated from the standard could behave incorrectly on NFT marketplaces. Wallets and apps had no common way to check whether a token was still valid. This standard solves the problem by building in an expiry interface that is compatible with existing infrastructure. It supports both a per-token expiry and a scheme that manages tokens grouped by epoch (a unit of time period), so you can choose the one that fits the use case.

What to implement

In depth

An Extension Standard That Inherits ERC-721

A compliant implementation must implement the IERC7858 interface and must inherit the ERC-721 interface. It must have every function defined in the interface, and the behavior of the functions must also satisfy the requirements of the specification. The interface IDs for ERC-165 supportsInterface are 0x3ebdfa31 for IERC7858 and 0xec7ffd66 for the epoch extension IERC7858Epoch. This standard is fully compatible with ERC-721 and other SBTs, including ERC-5484.

Expiry Basis: Block Number or Timestamp

expiryType must return the expiry basis the contract uses. The EXPIRY_TYPE enum has two values, BLOCKS_BASED (based on block.number) and TIME_BASED (based on block.timestamp), and the unit in which startTime and endTime are interpreted is also determined by this value. According to the design rationale, block-based expiry depends on network activity and suits applications that need precise consistency, while time-based expiry suits networks where block intervals are not constant. The return type of startTime and endTime is uint256, and the source comment notes that returning uint64 as ERC-5007 does may not be suitable for block.number-based expiry.

startTime and endTime Rules and Perpetual Tokens

startTime must be less than or equal to endTime, and except when both are 0, it is recommended that startTime be strictly less than endTime. If startTime and endTime are both 0, that tokenId has no expiry. A token whose startTime is not 0 and whose endTime is 0 is also treated as having no expiry. If a nonexistent tokenId is queried, these functions must revert in the same way as isTokenExpired. The TokenExpiryUpdated event must be emitted when a token is minted and when startTime or endTime is updated.

isTokenExpired Return Value and Error Handling

isTokenExpired queries the state of the given tokenId. It must return true if the token has expired and must revert if the tokenId does not exist. An implementation that uses custom errors is recommended to revert with ERC721NonexistentToken, following ERC-6093. An implementation that uses a Solidity version below v0.8.4 or prefers string errors is recommended to revert with NonexistToken. If the tokenId exists and has not expired, it must return false.

Transfers and Balance Counts Stay the Same After Expiry

balanceOf, inherited from ERC-721, must return all tokens, including expired ones. Because expired tokens are hard to track on-chain, a token remains in existence but in an unusable state. The NFT's transferFrom and safeTransferFrom must also allow transfers of expired tokens. This is to preserve compatibility with already deployed applications and tradability. However, a contract that checks validity must treat an expired token as invalid and unusable. The source text explains this design with the principle "First, do no harm." Expiry is added as an extra layer that does not interfere with existing uses, treating expiry as an enhancement rather than a fundamental change.

Epoch Extension: Expiry Grouped by Period

An epoch refers to the period or block range during which certain tokens are valid, and the concept is borrowed from ERC-7818. Tokens are grouped under one epoch and share the same validityDuration. An implementation that needs epoch-based expiry must implement IERC7858Epoch together with the base IERC7858 and must satisfy the requirements of both interfaces. currentEpoch must return the current epoch, epochLength the length between epochs in blocks or seconds, and validityDuration the token's validity period in number of epochs. isEpochExpired must return true if the epoch has expired. unexpiredBalanceOf must return the total number of unexpired tokens an account holds, and unexpiredBalanceOfAtEpoch must return the number of unexpired tokens held in a specific epoch, returning 0 if that epoch has expired even if tokens were previously held there.

Convenience Functions That Are Not Required

The standard also defines OPTIONAL functions that can be implemented depending on requirements. On the base side, getRemainingDurationBeforeTokenExpired returns the time or number of blocks remaining until the given tokenId expires. The epoch extension has four. getEpochBalance returns the token amount stored in an epoch even if that epoch has expired, and getEpochInfo returns both the start and end of a specified epoch. getNearestExpiryOf returns the list of tokenIds closest to expiry along with the expected expiry block number or timestamp, and getRemainingDurationBeforeEpochChange returns the time or number of blocks remaining until the epoch changes.

Things to watch out for

Original ERC · ← ERCs