---
eip: 7858
title: Expirable NFTs and SBTs
description: Non-fungible (NFT) and soulbound (SBT) tokens with expiration, supporting time-limited use cases.
author: sirawt (@MASDXI), ADISAKBOONMARK (@ADISAKBOONMARK), parametprame (@parametprame), Nacharoen (@najaroen)
discussions-to: https://ethereum-magicians.org/t/erc-7858-expirable-nft-sbt/22406
status: Final
type: Standards Track
category: ERC
created: 2024-01-04
requires: 165, 721
---

## Abstract

Introduces an extension for [ERC-721](./eip-721.md) 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

Before this EIP, NFTs and SBTs implemented expiration mechanisms via custom mappings. However, this approach presents challenges, such as: inconsistent integration with other smart contracts, and the custom [ERC-721](./eip-721.md) implementations might misbehave when used in NFT marketplaces. This EIP addresses these issues by providing a built-in expiration mechanism and interfaces that are compatible with existing infrastructure. It supports both per-token and epoch-based expiry, allowing developers to choose the appropriate expiry type for their use cases, including:

- 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](./eip-2135.md) or [ERC-7578](./eip-7578.md).
  - 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
- Rental
  - Real Estate Unit, Digital Access, DePIN, etc

## 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.

Compatible implementations MUST implement the `IERC7858` interface and MUST inherit from [ERC-721](./eip-721.md)'s interface. All functions defined in the interface MUST be present and all function behavior MUST meet the interface specification requirements.

### Interface

```solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=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`.
interface IERC7858 /**is IERC721 */ {
    enum EXPIRY_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`).
     */
    event TokenExpiryUpdated(
        uint256 indexed tokenId,
        uint256 indexed startTime,
        uint256 indexed endTime
    );

    /**
     * @dev Returns the type of the expiry.
     * @return EXPIRY_TYPE  Enum value indicating the unit of an expiry.
     */
    function expiryType() external view returns (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.
     */
    function isTokenExpired(uint256 tokenId) external view returns (bool);

    // return depends on the type `block.timestamp` or `block.number`
    // {ERC-5007} return in uint64 MAY not suitable for `block.number` based.
    function startTime(uint256 tokenId) external view returns (uint256);
    function endTime(uint256 tokenId) external view returns (uint256);
}
```

### Behavior Specification

- `balanceOf` that inherited from [ERC-721](./eip-721.md) MUST 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 `safeTransferFrom` MUST 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.
- `expiryType` MUST 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. Implementations that use custom errors SHOULD revert with `ERC721NonexistentToken` following the relevant [ERC-6093](./eip-6093.md) and implementations 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 `startTime` MUST be less than or equal to `endTime` and `startTime` SHOULD be strictly less than `endTime` except when both are set to `0`. A `startTime` and `endTime` of `0` indicates that the `tokenId` has no expiration. Tokens with a non-zero `startTime` and a zero `endTime` are also considered to have no expiration. If the `tokenId` does not exist, this function MUST revert in the same way as `isTokenExpired`.
- The interface ID for [ERC-165](./eip-165.md)'s `supportsInterface` for `IERC7858` is `0x3ebdfa31`, and for `IERC7858Epoch` it is `0xec7ffd66`
* `TokenExpiryUpdated` MUST 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](./eip-7818.md), tokens are grouped under an `epoch` and share the same `validityDuration`. For implementations that require epoch-based expiration management, the `IERC7858Epoch` extension interface MUST be implemented alongside the base `IERC7858` interface, and all functions and behaviors MUST meet both `IERC7858` and `IERC7858Epoch` interface specification requirements.

```solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=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 `0xec7ffd66`.
interface IERC7858Epoch /** 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.
     */
    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 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).
     */
    function isEpochExpired(uint256 epoch) external view returns (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.
     */
    function unexpiredBalanceOf(address account) external view returns (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.
     */
    function unexpiredBalanceOfAtEpoch(uint256 epoch, address account) external view returns (uint256);

    /**
     * @dev Retrieves the validity duration of each token.
     * @return uint256 The validity duration of each token in `epoch` unit.
     */
    function validityDuration() external view returns (uint256);
}
```
### Extension Behavior Specification

- `unexpiredBalanceOfAtEpoch` MUST return the count of usable (unexpired) tokens held by an account from 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.
- `unexpiredBalanceOf` MUST return total count of usable (unexpired) tokens owned by the account.
- `currentEpoch` MUST return the current `epoch` of the contract.
- `epochLength` MUST return duration between `epoch` in blocks or time in seconds.
- `validityDuration` MUST return the validity duration of tokens in terms of `epoch` counts.
- `isEpochExpired` MUST return true if the given `epoch` is expired, otherwise `false`.

### Additional Potential Useful Function

These OPTIONAL functions provide additional functionality that developers MAY choose to implement based on their specific requirements and use cases.

#### Base (default)

``` Solidity
function getRemainingDurationBeforeTokenExpired(uint256 tokenId) public view returns (uint256);
```

- `getRemainingDurationBeforeTokenExpired` returns the remaining time or blocks before the given `tokenId` is expired.

#### Epoch (extension)

``` Solidity
function getEpochBalance(uint256 epoch) public view returns (uint256);
```

- `getEpochBalance` returns the amount of tokens stored in a given epoch, even if the epoch has expired.

``` Solidity
function getEpochInfo(uint256 epoch) public view returns (uint256,uint256);
```

- `getEpochInfo` returns both the start and end of the specified `epoch`.

``` Solidity
function getNearestExpiryOf(address account) public view returns (uint256);
```

- `getNearestExpiryOf` returns the list of `tokenId` closest to expiration, along with an estimated expiration block number or timestamp based on `epochType`.

``` Solidity
function getRemainingDurationBeforeEpochChange() public view returns (uint256);
```

- `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](./eip-721.md), [ERC-5484](./eip-5484.md) and other SBTs.

## Reference Implementation

You can find our reference implementation [here](../assets/eip-7858/README.md).

## Security Considerations

### Burn and Re-Mint

Implementation should ensure that burning token and re-minting it with the same `tokenId` will not introduce an unauthorized renewal.

### Unauthorized Update

Implementation should ensure that only authorized can update `startTime` and `endTime`.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
