Introduces an extension for ERC-721 Non-Fungible Tokens (NFTs) and Soulbound Tokens (SBTs) that adds an expiration mechanism, allowing tokens to become invalid after a predefined period. This additional layer of functionality ensures that the expiration mechanism does not interfere with existing NFTs or SBTs, preserving transferability for NFTs and compatibility with current DApps such as NFT Marketplace. Expiration can be defined using either block height or timestamp, offering flexibility for various use cases.
Motivation
Introduces an extension for ERC-721 Non-Fungible Tokens (NFTs) and Soulbound Tokens (SBTs), which facilitates the implementation of an expiration mechanism.
Use cases include:
Access and Authentication
Authentication for Identity and Access Management (IAM)
Membership for Membership Management System (MMS)
Ticket and Press for Meetings, Incentive Travel, Conventions, and Exhibitions (MICE) when using with ERC-2135 or ERC-7578.
Subscription-based access for digital platforms.
Digital Certifications, Contracts, Copyrights, Documents, Licenses, Policies, etc.
Loyalty Program voucher or coupon
Governance and Voting Rights
Financial Product
Bonds, Loans, Hedge, and Options Contract
Specification
The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Interface
// SPDX-License-Identifier: CC0-1.0
pragmasolidity>=0.8.0<0.9.0;/**
* @title ERC-7858: Expirable NFTs and SBTs
* @notice unique/granular expiry
*/// import "./IERC721.sol";
// The EIP-165 identifier of this interface is `0x3ebdfa31`.
interfaceIERC7858/**is IERC721 */{enumEXPIRY_TYPE{BLOCKS_BASED,// block.number
TIME_BASED// block.timestamp
}/**
* @dev Emitted when the expiration date of a token is set or updated.
* @param tokenId The identifier of the token ERC721 `tokenId`.
* @param startTime The start time of the token (block number or timestamp based on `expiryType`).
* @param endTime The end time of the token (block number or timestamp based on `expiryType`).
*/eventTokenExpiryUpdated(uint256indexedtokenId,uint256indexedstartTime,uint256indexedendTime);/**
* @dev Returns the type of the expiry.
* @return EXPIRY_TYPE Enum value indicating the unit of an expiry.
*/functionexpiryType()externalviewreturns(EXPIRY_TYPE);/**
* @dev Checks whether a specific token is expired.
* @param tokenId The identifier representing the `tokenId` (ERC721).
* @return bool True if the token is expired, false otherwise.
*/functionisTokenExpired(uint256tokenId)externalviewreturns(bool);// return depends on the type `block.timestamp` or `block.number`
// {ERC-5007} return in uint64 MAY not suitable for `block.number` based.
functionstartTime(uint256tokenId)externalviewreturns(uint256);functionendTime(uint256tokenId)externalviewreturns(uint256);}
Behavior Specification
balanceOf that inherited from ERC-721MUST return all tokens even if expired; they still exist but are unusable due to the limitation of tracking expired token on-chain.
For NFTs transferFrom, and safeTransferFromMUST allow transferring tokens even if they expired. This ensures that expired tokens remain transferable and tradable, preserving compatibility with existing applications already deployed. However, expired tokens MUST be considered invalid and unusable in contracts that check for token validity.
expiryTypeMUST return the type of expiry used by the contract, which can be either BLOCK or TIME.
isTokenExpired is used for retrieving the status of the given tokenId the function MUST return true if the token is expired and MUST revert if the tokenId does not exist for implementation that use custom error SHOULD revert with ERC721NonexistentToken following the relevant ERC-6093 and for implementation that using Solidity version below v0.8.4 or those preferring to use revert with string error SHOULD revert with NonexistToken, If the tokenId exists and is not expired, the function MUST return false.
startTime and endTime of tokenId, can be block.number or block.timestamp depending on expiryType. The startTimeMUST less than endTime and SHOULD except when both are set to 0. A startTime and endTime of 0 indicates that the tokenId has no time-limited. If the tokenId does not exist MUST revert same as in function isTokenExpired.
supportInterface for IERC7858 is 0x3ebdfa31 for IERC7858Epoch is 0x8f55b98a
TokenExpiryUpdatedMUST be emitted when the token is minted or when its expiration details (startTime or endTime) are updated.
Extension Interface
Epochs represent a specific period or block range during which certain tokens are valid borrowing concepts from ERC-7818, tokens are grouped under an epoch and share the same validityDuration.
// SPDX-License-Identifier: CC0-1.0
pragmasolidity>=0.8.0<0.9.0;/**
* @title ERC-7858: Expirable NFTs and SBTs
* @notice epoch expiry extension
*/// import "./IERC7858.sol";
// The EIP-165 identifier of this interface is `0x8f55b98a`.
interfaceIERC7858Epoch/** is IERC7858 */{/**
* @dev Retrieves the current epoch of the contract.
* @return uint256 The current epoch of the token contract,
* often used for determining active/expired states.
*/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 EXPIRY_TYPE Enum value indicating the unit of an epoch.
*/functionepochType()externalviewreturns(EXPIRY_TYPE);/**
* @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 `EXPIRY_TYPE` measurement (e.g., block count or time duration).
*/functionisEpochExpired(uint256epoch)externalviewreturns(bool);/**
* @dev Retrieves the balance of unexpired tokens owned by an account.
* @param account The address of the account.
* @return uint256 The amount of unexpired tokens owned by an account.
*/functionunexpiredBalanceOf(addressaccount)externalviewreturns(uint256);/**
* @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.
*/functionunexpiredBalanceOfAtEpoch(uint256epoch,addressaccount)externalviewreturns(uint256);/**
* @dev Retrieves the validity duration of each token.
* @return uint256 The validity duration of each token in `epoch` unit.
*/functionvalidityDuration()externalviewreturns(uint256);}
unexpiredBalanceOfAtEpochMUST return unexpired or usable 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 unexpiredBalanceOfAtEpoch(5, address) returns 0 even if there were tokens previously held in that epoch.
unexpiredBalanceOfMUST return only unexpired or usable tokens.
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.
Additional Potential Useful Function
These OPTIONAL functions provide additional functionality that might be useful depending on the specific use case.
Base (default)
getRemainingDurationBeforeTokenExpired returns the remaining time or blocks before the given tokenId is expired.
Epoch (extension)
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 list of tokenId 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
First, do no harm
Introducing expirability as an additional layer of functionality ensures it doesn’t interfere with existing use cases or applications. For non-SBT tokens, transferability remains intact, maintaining compatibility with current systems. Expired tokens are simply flagged as unusable during validity checks, treating expiration as an enhancement rather than a fundamental change.
Expiry Types
Defining expiration by either block height (block.number) or block timestamp (block.timestamp) offers flexibility for various use cases. Block-based expiration suits applications that rely on network activity and require precise consistency, while time-based expiration is ideal for networks with variable block intervals.
Backwards Compatibility
This standard is fully compatible with ERC-721, ERC-5484 and other SBTs.