Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ERC: Expirable NFTs and SBTs #841

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f90c26d
update: fix backtick and REAME.md
MASDXI Dec 12, 2024
bf0ee7c
Merge branch 'ethereum:master' into master
MASDXI Dec 27, 2024
17f6ddf
update: missing word RFC 8174
MASDXI Jan 2, 2025
4d9d512
Merge branch 'ethereum:master' into master
MASDXI Jan 3, 2025
1e655b0
Update ERC-7818.md
MASDXI Jan 3, 2025
981ab6f
chore: initial md
parametprame Jan 6, 2025
ff4b88c
chore: update md
parametprame Jan 7, 2025
1207603
Merge branch 'ethereum:master' into master
MASDXI Jan 7, 2025
9eecee5
Update erc-7818.md move to last call
MASDXI Jan 7, 2025
215ca21
Update adding reference implementation
MASDXI Jan 8, 2025
dd3e1b4
Merge branch 'ethereum:master' into master
MASDXI Jan 8, 2025
5a319cd
Merge remote-tracking branch 'sirawt/master'
MASDXI Jan 8, 2025
925d9b2
Update erc-xxxx.md
MASDXI Jan 9, 2025
ab3d9ae
Update erc-xxxx.md
MASDXI Jan 9, 2025
66f37d3
Update erc-xxxx.md
MASDXI Jan 9, 2025
8e14c63
Update erc-xxxx.md
MASDXI Jan 9, 2025
03211f8
Merge branch 'ethereum:master' into master
parametprame Jan 9, 2025
9fcc3bd
fix: invalid git branch
MASDXI Jan 9, 2025
669a381
Update and rename erc-xxxx.md to erc-7858.md
SamWilsn Jan 9, 2025
4f2fac3
Update and rename README.md to README.md
SamWilsn Jan 9, 2025
5f368d0
Update and rename ERCXXXX.sol to ERC7858.sol
SamWilsn Jan 9, 2025
8be1b83
Update and rename IERCXXXX.sol to IERC7858.sol
SamWilsn Jan 9, 2025
d9f8dba
Update erc-7858.md
MASDXI Jan 10, 2025
62e0de7
update erc-7858.md
MASDXI Jan 13, 2025
fc0ab26
fix: incorrect function name
MASDXI Jan 14, 2025
d1ef82a
update erc-7858.md
MASDXI Jan 15, 2025
4e54e58
update erc-7858.md
MASDXI Jan 15, 2025
6183b72
update erc-7858.md
MASDXI Jan 15, 2025
474afc7
fix typo and detail in erc-7858.md
MASDXI Jan 15, 2025
20785e5
update erc-7858.md
MASDXI Jan 15, 2025
ffd8e9f
update erc-7858.md
MASDXI Jan 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions ERCS/erc-7858.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
---
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: Draft
type: Standards Track
category: ERC
created: 2024-01-04
requires: 165
---

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

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

```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";

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 Id The identifier representing the `tokenId` (ERC721).
* @return bool True if the token is expired, false otherwise.
*/
function isTokenValid(uint256 Id) 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 it still exists but unusable due to limitation to tracking expire 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`.
- `startTime` and `endTime` of `tokenId`, can be `block.number` or `block.timestamp` depending on `expiryType`. The `startTime` **MUST** less than `endTime` and **SHOULD** except when both are set to 0. A `startTime` and `endTime` of 0 indicates that the `tokenId` or `tokenType` has no time-limited.
- `isTokenValid` is used for retrieving the status of the given `tokenId` or `tokenType` the function **MUST** return `true` if the token is still valid otherwise `false`.
- `supportInterface` for `IERC7858` is `0xea1196aa` for `IERC7858Epoch` is `0x3ca721a3`
* `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`.

```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";

interface IERC7858Epoch is IERC7858 {
/**
* @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 valid balance owned by an account.
* @param account The address of the account.
* @return uint256 The amount of valid token owned by an account.
*/
function validBalanceOf(address account) external view returns (uint256);

/**
* @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 Returns the type of the epoch.
* @return EXPIRY_TYPE Enum value indicating the unit of an epoch.
*/
function epochType() external view returns (EXPIRY_TYPE);

/**
* @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);

/**
* @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);
}
```

- `balanceOfAtEpoch` **MUST** return the balance of tokens held by an account at the specified `epoch`, even if the `epoch` has expired.
- `validBalanceOf` **MUST** return only valid or usable tokens.
- `currentEpoch` **MUST** return the current `epoch` of the contract.
- `epochLength` **MUST** return duration between `epoch` in blocks or time in seconds.
- `epochType` **MUST** return the type of epoch used by the contract, which can be either `BLOCKS_BASED` or `TIME_BASED`.
- `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 might be useful depending on the specific use case.

- `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](./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

No security considerations were found.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
3 changes: 3 additions & 0 deletions assets/erc-7858/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ERC-7858

<!-- TODO -->
60 changes: 60 additions & 0 deletions assets/erc-7858/contracts/ERC7858.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./interfaces/IERC7858.sol";

contract ERC7858 is ERC721, IERC7858 {

constructor(
string memory name_,
string memory symbol_)
ERC721(name_,symbol_) {}

// mapping variable
mapping(uint256 => uint256) internal _startBlock;
mapping(uint256 => uint256) internal _endBlock;

// functional
function _mint(address to, uint256 tokenId, uint256 startBlock, uint256 endBlock) internal {
_mint(to, tokenId);
// store data to mapping
_startBlock[tokenId] = startBlock;
_endBlock[tokenId] = endBlock;

emit ExpirationUpdated(tokenId, startBlock, endBlock);
}

function startTime(uint256 tokenId) public view returns (uint256) {
return _startBlock[tokenId];
}

function endTime(uint256 tokenId) public view returns (uint256) {
return _endBlock[tokenId];
}

function expiryType() external pure returns (EXPIRY_TYPE) {
return IERC7858.EXPIRY_TYPE.BLOCK_BASED;
}

function isTokenValid(uint256 tokenId) external view returns (bool) {
uint256 startTimeCache = startTime(tokenId);
uint256 endTimeCache = endTime(tokenId);
if (startTimeCache == 0 && endTimeCache == 0) {
return false;
} else {
return block.number > endTime(tokenId);
}
}

function mint(address to, uint256 tokenId, uint256 startBlock, uint256 endBlock) public {
if (endBlock < startBlock) {
revert ();
}
_mint(to, tokenId,startBlock,endBlock);
}

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721) returns (bool) {
return interfaceId == type(IERC7858).interfaceId || super.supportsInterface(interfaceId);
}
}
33 changes: 33 additions & 0 deletions assets/erc-7858/contracts/interfaces/IERC7858.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

interface IERC7858 {
enum EXPIRY_TYPE {
BLOCK_BASED, // block.number
TIME_BASED // block.timestamp
}

event ExpirationUpdated(
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 Id The identifier representing the token type `Id` (ERC1155) or `tokenId` (ERC721).
* @return bool True if the token is expired, false otherwise.
*/
function isTokenValid(uint256 Id) external view returns (bool);

// inherit from ERC-5007 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);
}
Loading