Skip to content

Commit

Permalink
refactor: reference implementation
Browse files Browse the repository at this point in the history
Signed-off-by: MASDXI <sirawitt42@gmail.com>
  • Loading branch information
MASDXI committed Nov 29, 2024
1 parent 5446318 commit a0f1025
Show file tree
Hide file tree
Showing 9 changed files with 411 additions and 382 deletions.
20 changes: 13 additions & 7 deletions ERCS/erc-7818.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,21 @@ interface IERC7818 is IERC20 {
/**
* @dev Returns the type of the epoch.
* @return EPOCH_TYPE Enum value indicating the unit of epoch.
* @return EPOCH_TYPE Enum value indicating the unit of epoch.
*/
function epochType() external view returns (EPOCH_TYPE);
/**
* @dev Returns the fixed validity period for tokens, in epochs.
* @return uint256 Duration tokens remain valid, measured in epochs.
* @notice The duration aligns with the unit defined by `epochType()` (blocks or seconds).
*/
function validityPeriod() 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 `EPOCH_TYPE` measurement (e.g., block count or time duration).
*/
function isEpochExpired(uint256 epoch) external view returns (bool);
Expand Down Expand Up @@ -161,10 +165,12 @@ interface IERC7818 is IERC20 {
### 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 epochs, 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.
- `balanceOfEpoch` returns the 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 `balanceOfByEpoch(address, 5)` **MUST** return `0` even if there were tokens previously held in that epoch."
- `balanceOfAtEpoch` returns the 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 `balanceOfByEpoch(address, 5)` **MUST** return `0` even if there were tokens previously held in that epoch.
- `validityPeriod` **MUST** return the duration for which tokens are valid, expressed in both `blocks` and `seconds`, depending on the `epochType`.
- `isEpochExpired` implementation contracts **MUST** define and document the logic for determining expiration, typically by comparing the latest epoch with the given `epoch` value, based on the `EPOCH_TYPE` measurement (e.g., blocks count or time duration).
- `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.
- `transferByEpoch` and `transferFromByEpoch` **MUST** transfer the specified number of tokens held by an account at the specified epoch to the recipient, If the epoch has expired, the transaction **MUST** `revert` or return `false`
- `transferAtEpoch` and `transferFromAtEpoch` **MUST** transfer the specified number of tokens held by an account at the specified epoch to the recipient, If the epoch has expired, the transaction **MUST** `revert` or return `false`
- `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 epoch)`, to clearly indicate that the operation failed due to attempting to transfer expired tokens.

Expand Down
70 changes: 42 additions & 28 deletions assets/erc-7818/contracts/IERC7818.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,75 @@
pragma solidity >=0.8.0 <0.9.0;

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

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

interface IERC7818 is IERC20 {
/**
* @dev Enum represents the types of `epoch` that can be used.
* @notice The implementing contract may use one of these types to define how the `epoch` is measured.
*/
enum EPOCH_TYPE {
BLOCKS_BASED, // measured in the number of blocks (e.g., 1000 blocks)
TIME_BASED // measured in seconds (UNIX time) (e.g., 1000 seconds)
}

/**
* @dev Retrieves the balance of a specific `epoch` owned by an account.
* @param account The address of the account.
* @param epoch "MAY" represents an epoch, round, or period.
* @param epoch The `epoch for which the balance is checked.
* @return uint256 The balance of the specified `epoch`.
* @notice `epoch` "MUST" represent a unique identifier, and its meaning "SHOULD"
* align with how contract maintain the `epoch` in the implementing contract.
* @notice "MUST" return 0 if the specified `epoch` is expired.
*/
function balanceOf(
function balanceOfAtEpoch(
address account,
uint256 epoch
) 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.
* @dev Retrieves the latest epoch currently tracked by the contract.
* @return uint256 The latest epoch of the contract.
*/
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 EPOCH_TYPE Enum value indicating the unit of epoch.
*/
function epoch() external view returns (uint256);
function epochType() external view returns (EPOCH_TYPE);

/**
* @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.
* @dev Returns the fixed validity period for tokens, in epochs.
* @return uint256 Duration tokens remain valid, measured in epochs.
* @notice The duration aligns with the unit defined by `epochType()` (blocks or seconds).
*/
function duration() external view returns (uint256);
function validityPeriod() external view returns (uint256);

/**
* @dev Checks whether a specific `epoch` is expired.
* @param epoch "MAY" represents an epoch, round, or period.
* @param epoch The `epoch` to check.
* @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 `epoch`.
*/
function expired(uint256 epoch) external view returns (bool);
function isEpochExpired(uint256 epoch) external view returns (bool);

/**
* @dev Transfers a specific `epoch` and value to a recipient.
* @param to The recipient address.
* @param epoch "MAY" represents an epoch, round, or period.
* @param epoch The `epoch` for the transfer.
* @param value The amount to transfer.
* @return bool True if the transfer succeeded, false or reverted if give `epoch` it's expired.
* @notice The transfer "MUST" revert if the token `epoch` is expired.
* @return bool True if the transfer succeeded, otherwise false.
*/
function transfer(
function transferAtEpoch(
address to,
uint256 epoch,
uint256 value
Expand All @@ -65,12 +80,11 @@ interface IERC7818 is IERC20 {
* @dev Transfers a specific `epoch` and value from one account to another.
* @param from The sender's address.
* @param to The recipient's address.
* @param epoch "MAY" represents an epoch, round, or period.
* @param epoch The `epoch` for the transfer.
* @param value The amount to transfer.
* @return bool True if the transfer succeeded, false or reverted if give `epoch` it's expired.
* @notice The transfer "MUST" revert if the token `epoch` is expired.
* @return bool True if the transfer succeeded, otherwise false.
*/
function transferFrom(
function transferFromAtEpoch(
address from,
address to,
uint256 epoch,
Expand Down
51 changes: 29 additions & 22 deletions assets/erc-7818/contracts/abstracts/ERC20Expirable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,9 @@ abstract contract ERC20Expirable is Context, IERC20Errors, IERC7818 {
}
}

function _expired(uint256 id) internal view returns (bool) {
function _expired(uint256 epoch) internal view returns (bool) {
unchecked {
if (_blockNumberProvider() - id >= _getFrameSizeInBlockLength()) {
if (_blockNumberProvider() - epoch >= _getFrameSizeInBlockLength()) {
return true;
}
}
Expand Down Expand Up @@ -781,64 +781,71 @@ abstract contract ERC20Expirable is Context, IERC20Errors, IERC7818 {
}

/// @inheritdoc IERC7818
/// @notice implementation defined `id` with token id
function balanceOf(
function balanceOfAtEpoch(
address account,
uint256 id
uint256 epoch
) external view returns (uint256) {
if (_expired(id)) {
if (_expired(epoch)) {
return 0;
}
(uint256 era, uint8 slot) = _calculateEraAndSlot(id);
return _balances[account][era][slot].blockBalances[id];
(uint256 era, uint8 slot) = _calculateEraAndSlot(epoch);
return _balances[account][era][slot].blockBalances[epoch];
}

/// @inheritdoc IERC7818
/// @notice implementation define duration unit in blocks
function duration() public view virtual returns (uint256) {
function epochLength() public view virtual returns (uint256) {
return _getBlockPerEra();
}

/// @inheritdoc IERC7818
function validityPeriod() public view virtual returns (uint256) {
return _getFrameSizeInBlockLength();
}

/// @inheritdoc IERC7818
function epoch() public view virtual returns (uint256) {
function currentEpoch() public view virtual returns (uint256) {
(uint256 era, ) = _calculateEraAndSlot(_blockNumberProvider());
return era;
}

/// @inheritdoc IERC7818
function expired(uint256 id) public view virtual returns (bool) {
return _expired(id);
function epochType() public pure returns (EPOCH_TYPE) {
return EPOCH_TYPE.BLOCKS_BASED;
}

/// @inheritdoc IERC7818
/// @notice implementation defined `id` with token id
function transfer(
function isEpochExpired(uint256 epoch) public view virtual returns (bool) {
return _expired(epoch);
}

/// @inheritdoc IERC7818
function transferAtEpoch(
address to,
uint256 id,
uint256 epoch,
uint256 value
) public override returns (bool) {
if (_expired(id)) {
if (_expired(epoch)) {
revert ERC7818TransferExpired();
}
address owner = _msgSender();
_transferSpecific(owner, to, id, value);
_transferSpecific(owner, to, epoch, value);
return true;
}

/// @inheritdoc IERC7818
/// @notice implementation defined `id` with token id
function transferFrom(
function transferFromAtEpoch(
address from,
address to,
uint256 id,
uint256 epoch,
uint256 value
) public virtual returns (bool) {
if (_expired(id)) {
if (_expired(epoch)) {
revert ERC7818TransferExpired();
}
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transferSpecific(from, to, id, value);
_transferSpecific(from, to, epoch, value);
return true;
}
}
Loading

0 comments on commit a0f1025

Please sign in to comment.