Skip to content

Add ERC: Expirable ERC-20 #718

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

Merged
merged 16 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
162 changes: 162 additions & 0 deletions ERCS/erc-7818.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
eip: 7818
title: Expirable ERC-20
description: An ERC-20 extension for creating fungible tokens with expiration, supporting time-limited use cases.
author: sirawt (@MASDXI), ADISAKBOONMARK (@ADISAKBOONMARK)
discussions-to: https://ethereum-magicians.org/t/erc-7818-expirable-erc20/21655
status: Draft
type: Standards Track
category: ERC
created: 2024-11-13
requires: 20
---

## Abstract

Introduces an extension for [ERC-20](./eip-20.md) tokens, which facilitates the implementation of an expiration mechanism. Through this extension, tokens have a predetermined validity period, after which they become invalid and can no longer be transferred or used. This functionality proves beneficial in scenarios such as time-limited bonds, loyalty rewards, or game tokens necessitating automatic invalidation after a specific duration. The extension is crafted to seamlessly align with the existing [ERC-20](./eip-20.md) standard, ensuring smooth integration with the prevailing token smart contract while introducing the capability to govern and enforce token expiration at the contract level.

## Motivation

This extension facilitates the development of [ERC-20](./eip-20.md) standard compatible tokens featuring expiration dates. This capability broadens the scope of potential applications, particularly those involving time-sensitive assets. Expirable tokens are well-suited for scenarios necessitating temporary validity, including

- Bonds or financial instruments with defined maturity dates
- Time-constrained assets within gaming ecosystems
- Next-gen loyalty programs incorporating expiring rewards or points
- Prepaid credits for utilities or services (e.g., cashback, data packages, fuel, computing resources) that expire if not used within a specified time frame
- Postpaid telecom data package allocations that expire at the end of the billing cycle, motivating users to utilize their data before it resets
- Tokenized e-Money for a closed-loop ecosystem, such as transportation, food court, and retail payments

## Specification

The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Implementers of this **REQUIRED** inheritance [ERC-20](./eip-20.md) interface and **MUST** have all of the following functions and all function behavior **MUST** meet the specification.

```solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;

/**
* @title ERC-7818: Expirable ERC20
* @dev Interface for creating expirable ERC20 tokens.
*/

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IERC7818 is IERC20 {

/**
* @dev Retrieves the balance of a specific `identifier` owned by an account.
* @param account The address of the account.
* @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
* @return uint256 The balance of the specified `identifier`.
* @notice `identifier` "MUST" represent a unique identifier, and its meaning "SHOULD"
* align with how contract maintain the `epoch` in the implementing contract.
*/
function balanceOf(
address account,
uint256 identifier
) 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 epoch() external view returns (uint256);

/**
* @dev Retrieves the duration a token remains valid.
* @return uint256 The validity duration.
* @notice `duration` "MUST" specify the token's validity period.
* The implementing contract "SHOULD" clearly document,
* whether the unit is blocks or time in seconds.
*/
function duration() external view returns (uint256);

/**
* @dev Checks whether a specific `identifier` is expired.
* @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
* @return bool True if the token is expired, false otherwise.
* @notice Implementing contracts "MUST" define the logic for determining expiration,
* typically by comparing the current `epoch()` with the given `identifier`.
*/
function expired(uint256 identifier) external view returns (bool);

/**
* @dev Transfers a specific `identifier` and value to a recipient.
* @param to The recipient address.
* @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
* @param value The amount to transfer.
* @return bool True if the transfer succeeded, false or reverted if give `identifier` it's expired.
* @notice The transfer "MUST" revert if the token `identifier` is expired.
*/
function transfer(
address to,
uint256 identifier,
uint256 value
) external returns (bool);

/**
* @dev Transfers a specific `identifier` and value from one account to another.
* @param from The sender's address.
* @param to The recipient's address.
* @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
* @param value The amount to transfer.
* @return bool True if the transfer succeeded, false or reverted if give `identifier` it's expired.
* @notice The transfer "MUST" revert if the token `identifier` is expired.
*/
function transferFrom(
address from,
address to,
uint256 identifier,
uint256 value
) external returns (bool);
}
```

### Behavior specification

- `balanceOf` **MUST** return the total balance of tokens held by an account that are still valid (i.e., have not expired). This includes any tokens associated with specific periods, epochs, or other identifiers, provided they remain within their validity duration. Expired tokens **MUST NOT** be included in the returned balance, ensuring that only actively usable tokens are reflected in the result.
- `transfer` and `transferFrom` **MUST** exclusively transfer tokens that remain non-expired at the time of the transaction. Attempting to transfer expired tokens **MUST** revert the transaction or return false. Additionally, implementations **MAY** include logic to prioritize the automatic transfer of tokens closest to expiration, ensuring that the earliest expiring tokens are used first, provided they meet the non-expired condition.
- `totalSupply` **SHOULD** be set to `0` or `type(uint256).max` due to the challenges of tracking only valid (non-expired) tokens.
- The implementation **MAY** use a standardized revert message, such as `ERC7818TransferredExpiredToken` or `ERC7818TransferredExpiredToken(address sender, uint256 identifier)`, to clearly indicate that the operation failed due to attempting to transfer expired tokens.

## Rationale

The rationale for developing an expirable [ERC-20](./eip-20.md) token extension is based on several key requirements that ensure its practicality and adaptability for various applications to

### Compatibility with the existing [ERC-20](./erc-20.md) standard.
The extension should integrate smoothly with the [ERC-20](./eip-20.md) interface, This ensures compatibility with existing token ecosystems and third-party tools like wallets and blockchain explorers.

### Flexible interface for various implementation.
The smart contract should be extensible, allowing businesses to tailor the expiration functionality to their specific needs like expiry in bulk or each token independent expire, whether it’s dynamic reward systems or time-sensitive applications.

## Backwards Compatibility

This standard is fully [ERC-20](./erc-20.md) compatible.

## Reference Implementation

For reference implementation can be found [here](../assets/erc-7818/README.md), But in the reference implementation, we employ a sorted list to automatically select the token that nearest expires first with a First-In-First-Out (`FIFO`) and sliding window algorithm that operates based on the `block.number` as opposed to relying on `block.timestamp`, which has been criticized for its lack of security and resilience, particularly given the increasing usage of Layer 2 (L2) networks over Layer 1 (L1) networks. Many L2 networks exhibit centralization and instability, which directly impacts asset integrity, rendering them potentially unusable during periods of network halting, as they are still reliant on the timestamp.

## Security Considerations

### SC06: Denial Of Service
Run out of gas problem due to the operation consuming high gas used if transferring multiple groups of small tokens.

### SC09: Gas Limit Vulnerabilities
Exceeds block gas limit if the blockchain has a block gas limit lower than the gas used in the transaction.

### SWC116: Block values as a proxy for time
if using `block.timestamp` for calculating `epoch()`

### Fairness Concerns
In a straightforward implementation, where all tokens within the same epoch share the same expiration (e.g., at `epoch`:`x`), bulk expiration occurs.

### Risks in Liquidity Pools
When tokens with expiration dates are deposited into liquidity pools (e.g., in DEXs), they may expire while still in the pool.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
18 changes: 18 additions & 0 deletions assets/erc-7818/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
node_modules
.env

# Hardhat files
/cache
/artifacts

# TypeChain files
/typechain
/typechain-types

# solidity-coverage files
/coverage
/coverage.json

# Hardhat Ignition default folder for deployments against a local node
ignition
ignition/deployments/chain-31337
1 change: 1 addition & 0 deletions assets/erc-7818/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v20.18.0
7 changes: 7 additions & 0 deletions assets/erc-7818/.solcover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
skipFiles: [
"./libraries",
"./abstracts/ERC20ExpirableBulk.sol",
"MockERC20Expirable.sol",
],
};
93 changes: 93 additions & 0 deletions assets/erc-7818/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# ERC-7818

This is reference implementation of [ERC-7818](../../ERCS/erc-7818.md)

## Implementation Describe

#### Sliding Window Algorithm to look for expiration balance

This contract creates an abstract implementation that adopts the `Sliding Window Algorithm` to maintain a window over a period of time (block height). This efficient approach allows for the look back and calculation of `usable balances` for each account within that window period. With this approach, the contract does not require a variable acting as a `counter` or a `state` to keep updating the latest state, nor does it need any interaction calls to keep updating the current period, which is an effortful and costly design.

<p align="center">
<img src="implementation.svg">
</p>

#### Era and Slot for storing data in vertical and horizontal way

```solidity
// skipping

struct Slot {
uint256 slotBalance;
mapping(uint256 => uint256) blockBalances;
SortedList.List list;
}

// skipping

// O(n→) fot traversal each slot in era.
// O(n↓) for traversal each element in list.
mapping(address => mapping(uint256 => mapping(uint8 => Slot))) private _balances;
mapping(uint256 => uint256) private _worldBlockBalance;
```

With this struct `Slot` it provides an abstract loop in a horizontal way more efficient for calculating the usable balance of the account because it provides `slotBalance` which acts as suffix balance so you don't need to get to iterate or traversal over the `list` for each `Slot` to calculate the entire slot balance if the slot can presume not to expire. otherwise struct `Slot` also provides vertical in a sorted list.
The `_worldBlockBalance` mapping tracks the total token balance across all accounts that minted tokens within a particular block. This structure allows the contract to trace expired balances easily. By consolidating balance data for each block.

#### Buffering 1 slot rule for ensuring safety

In this design, the buffering slot is the critical element that requires careful calculation to ensure accurate handling of balances nearing expiration. By incorporating this buffer, the contract guarantees that any expiring balance is correctly accounted for within the sliding window mechanism, ensuring reliability and preventing premature expiration or missed balances.

#### First-In-First-Out (FIFO) priority to enforce token expiration rules

Enforcing `FIFO` priority ensures that tokens nearing expiration are processed before newer ones, aligning with the token lifecycle and expiration rules. This method eliminates the need for additional off-chain computation and ensures that all token processing occurs efficiently on-chain, fully compliant with the ERC20 interface.
A **sorted** list is integral to this approach. Each slot maintains its own list, sorted by token creation which is can be `block.timestamp` or `blocknumber`, preventing any overlap with other slots. This separation ensures that tokens in one slot do not interfere with the balance handling in another. The contract can then independently manage token expirations within each slot, minimizing computation while maintaining accuracy and predictability in processing balances.

---

#### Token Receipt and Transaction Likelihood across various blocktime

Assuming each `Era` contains 4 `slots`, which aligns with familiar time-based divisions like a year being divided into four quarters, the following table presents various scenarios based on block time and token receipt intervals. It illustrates the potential transaction frequency and likelihood of receiving tokens within a given period.

| Block Time (ms) | Receive Token Every (ms) | Index/Slot | Transactions per Day | Likelihood |
| --------------- | ------------------------ | ---------- | -------------------- | ------------- |
| 100 | 100 | 78,892,315 | 864,000 | Very Unlikely |
| 500 | 500 | 15,778,463 | 172,800 | Very Unlikely |
| 1000 | 1000 | 7,889,231 | 86,400 | Very Unlikely |
| 1000 | 28,800,000 | 273 | 3 | Unlikely |
| 1000 | 86,400,000 | 91 | 1 | Possible |
| 5000 | 86,400,000 | 18 | 1 | Very Likely |
| 10000 | 86,400,000 | 9 | 1 | Very Likely |

> [!IMPORTANT]
> - Transactions per day are assumed based on loyalty point earnings.
> - Likelihood varies depending on the use case; for instance, gaming use cases may have higher transaction volumes than the given estimates.

## Security Considerations in The Reference Implementation

- Solidity Division Rounding Down This implementation contract may encounter scenarios where the calculated expiration block is shorter than the actual expiration block. This discrepancy can arise from the outputs of `blockPerYear` and `blockPerSlot * slotPerEra`, which may differ. Additionally, Solidity's division operation only returns integers, rounding down to the nearest whole number. However, by enforcing valid block times within the defined limits of `MINIMUM_BLOCK_TIME_IN_MILLISECONDS` and `MAXIMUM_BLOCK_TIME_IN_MILLISECONDS`, the contract mitigates this risk effectively.

## Usage

#### Install Dependencies
```bash
yarn install
```

#### Compile the Contract
Compile the reference implementation
```bash
yarn compile
```

#### Run Tests
Execute the provided test suite to verify the contract's functionality and integrity
```bash
yarn test
```

### Cleaning Build Artifacts
To clean up compiled files and artifacts generated during testing or deployment
```bash
yarn clean
```
79 changes: 79 additions & 0 deletions assets/erc-7818/contracts/IERC7818.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;

/**
* @title ERC-7818: Expirable ERC20
* @dev Interface for creating expirable ERC20 tokens.
*/

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IERC7818 is IERC20 {
/**
* @dev Retrieves the balance of a specific `identifier` owned by an account.
* @param account The address of the account.
* @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
* @return uint256 The balance of the specified `identifier`.
* @notice `identifier` "MUST" represent a unique identifier, and its meaning "SHOULD"
* align with how contract maintain the `epoch` in the implementing contract.
*/
function balanceOf(
address account,
uint256 identifier
) 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 epoch() external view returns (uint256);

/**
* @dev Retrieves the duration a token remains valid.
* @return uint256 The validity duration.
* @notice `duration` "MUST" specify the token's validity period.
* The implementing contract "SHOULD" clearly document,
* whether the unit is blocks or time in seconds.
*/
function duration() external view returns (uint256);

/**
* @dev Checks whether a specific `identifier` is expired.
* @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
* @return bool True if the token is expired, false otherwise.
* @notice Implementing contracts "MUST" define the logic for determining expiration,
* typically by comparing the current `epoch()` with the given `identifier`.
*/
function expired(uint256 identifier) external view returns (bool);

/**
* @dev Transfers a specific `identifier` and value to a recipient.
* @param to The recipient address.
* @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
* @param value The amount to transfer.
* @return bool True if the transfer succeeded, false or reverted if give `identifier` it's expired.
* @notice The transfer "MUST" revert if the token `identifier` is expired.
*/
function transfer(
address to,
uint256 identifier,
uint256 value
) external returns (bool);

/**
* @dev Transfers a specific `identifier` and value from one account to another.
* @param from The sender's address.
* @param to The recipient's address.
* @param identifier The Identifier "MAY" represents an epoch, round, period, or token identifier.
* @param value The amount to transfer.
* @return bool True if the transfer succeeded, false or reverted if give `identifier` it's expired.
* @notice The transfer "MUST" revert if the token `identifier` is expired.
*/
function transferFrom(
address from,
address to,
uint256 identifier,
uint256 value
) external returns (bool);
}
32 changes: 32 additions & 0 deletions assets/erc-7818/contracts/MockERC20Expirable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.5.0 <0.9.0;

import "../contracts/abstracts/ERC20Expirable.sol";

contract MockERC20Expirable is ERC20Expirable {
constructor(
string memory _name,
string memory _symbol,
uint16 blockTime_,
uint8 frameSize_,
uint8 slotSize_
) ERC20Expirable(_name, _symbol, block.number, blockTime_, frameSize_, slotSize_) {}

function mint(address to, uint256 value) public {
_mint(to, value);
}

function burn(address from, uint256 value) public {
_burn(from, value);
}

function badApprove(address owner, address spender, uint256 value) public returns (bool) {
_approve(owner, spender, value);
return true;
}

function badTransfer(address from, address to, uint256 value) public returns (bool) {
_transfer(from, to, value);
return true;
}
}
Loading
Loading