From 297669899dc2234b514a6055c3d15ef80d713d49 Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Wed, 29 May 2024 09:47:54 +0200 Subject: [PATCH 1/4] Update ERC-7540: ERC7540 rename `requester` => `controller` Merged by EIP-Bot. --- ERCS/erc-7540.md | 86 ++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/ERCS/erc-7540.md b/ERCS/erc-7540.md index ab4d1134c0..1e54476d45 100644 --- a/ERCS/erc-7540.md +++ b/ERCS/erc-7540.md @@ -41,7 +41,7 @@ The existing definitions from [ERC-4626](./eip-4626.md) apply. In addition, this - asynchronous deposit Vault: a Vault that implements asynchronous Requests for deposit flows - asynchronous redemption Vault: a Vault that implements asynchronous Requests for redemption flows - fully asynchronous Vault: a Vault that implements asynchronous Requests for both deposit and redemption flows -- requester: owner of the Request, who can manage any actions related to the Request including claiming the `assets` or `shares` +- controller: owner of the Request, who can manage any actions related to the Request including claiming the `assets` or `shares` - operator: an account that can manage Requests on behalf of another account. ### Request Flows @@ -58,7 +58,7 @@ Asynchronous deposit Vaults MUST override the ERC-4626 specification as follows: Asynchronous redeem Vaults MUST override the ERC-4626 specification as follows: 1. The `redeem` and `withdraw` methods do not transfer `shares` to the Vault, because this already happened on `requestRedeem`. -2. The `owner` field of `redeem` and `withdraw` SHOULD be renamed to `requester`, and the requester MUST be `msg.sender` unless the `requester` has approved the `msg.sender` as an operator. +2. The `owner` field of `redeem` and `withdraw` SHOULD be renamed to `controller`, and the controller MUST be `msg.sender` unless the `controller` has approved the `msg.sender` as an operator. 3. `previewRedeem` and `previewWithdraw` MUST revert for all callers and inputs. ### Request Lifecycle @@ -67,9 +67,9 @@ After submission, Requests go through Pending, Claimable, and Claimed stages. An | **State** | **User** | **Vault** | |-------------|---------------------------------|-----------| -| Pending | `requestDeposit(assets, requester, owner)` | `asset.transferFrom(owner, vault, assets)`; `pendingDepositRequest[requester] += assets` | -| Claimable | | *Internal Request fulfillment*: `pendingDepositRequest[requester] -= assets`; `claimableDepositRequest[requester] += assets` | -| Claimed | `deposit(assets, receiver)` | `claimableDepositRequest[requester] -= assets`; `vault.balanceOf[receiver] += shares` | +| Pending | `requestDeposit(assets, controller, owner)` | `asset.transferFrom(owner, vault, assets)`; `pendingDepositRequest[controller] += assets` | +| Claimable | | *Internal Request fulfillment*: `pendingDepositRequest[controller] -= assets`; `claimableDepositRequest[controller] += assets` | +| Claimed | `deposit(assets, receiver)` | `claimableDepositRequest[controller] -= assets`; `vault.balanceOf[receiver] += shares` | Note that `maxDeposit` increases and decreases in sync with `claimableDepositRequest`. @@ -90,7 +90,7 @@ If a Request becomes partially claimable, all requests of the same `requestId` M There are no assumptions or requirements of requests with different `requestId`. I.e. they MAY transition to Claimable at different times and exchange rates with no ordering or correlation enforced in any way. -When `requestId==0`, the Vault MUST use purely the `requester` to discriminate the request state. The Pending and Claimable state of multiple requests from the same `requester` would be aggregated. If a Vault returns `0` for the `requestId` of any request, it MUST return `0` for all requests. +When `requestId==0`, the Vault MUST use purely the `controller` to discriminate the request state. The Pending and Claimable state of multiple requests from the same `controller` would be aggregated. If a Vault returns `0` for the `requestId` of any request, it MUST return `0` for all requests. ### Methods @@ -98,9 +98,9 @@ When `requestId==0`, the Vault MUST use purely the `requester` to discriminate t Transfers `assets` from `owner` into the Vault and submits a Request for asynchronous `deposit`. This places the Request in Pending state, with a corresponding increase in `pendingDepositRequest` for the amount `assets`. -The output `requestId` is used to partially discriminate the request along with the `requester`. See [Request Ids](#request-ids) section for more info. +The output `requestId` is used to partially discriminate the request along with the `controller`. See [Request Ids](#request-ids) section for more info. -When the Request is Claimable, `claimableDepositRequest` will be increased for the `requester`. `deposit` or `mint` can subsequently be called by `requester` to receive `shares`. A Request MAY transition straight to Claimable state but MUST NOT skip the Claimable state. +When the Request is Claimable, `claimableDepositRequest` will be increased for the `controller`. `deposit` or `mint` can subsequently be called by `controller` to receive `shares`. A Request MAY transition straight to Claimable state but MUST NOT skip the Claimable state. The `shares` that will be received on `deposit` or `mint` MAY NOT be equivalent to the value of `convertToShares(assets)` at the time of Request, as the price can change between Request and Claim. @@ -122,7 +122,7 @@ MUST emit the `RequestDeposit` event. inputs: - name: assets type: uint256 - - name: requester + - name: controller type: address - name: owner type: address @@ -133,7 +133,7 @@ MUST emit the `RequestDeposit` event. #### pendingDepositRequest -The amount of requested `assets` in Pending state for the `requester` with the given `requestId` to `deposit` or `mint`. +The amount of requested `assets` in Pending state for the `controller` with the given `requestId` to `deposit` or `mint`. MUST NOT include any `assets` in Claimable state for `deposit` or `mint`. @@ -149,7 +149,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i inputs: - name: requestId type: uint256 - - name: requester + - name: controller type: address outputs: @@ -159,7 +159,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i #### claimableDepositRequest -The amount of requested `assets` in Claimable state for the `requester` with the given `requestId` to `deposit` or `mint`. +The amount of requested `assets` in Claimable state for the `controller` with the given `requestId` to `deposit` or `mint`. MUST NOT include any `assets` in Pending state for `deposit` or `mint`. @@ -175,7 +175,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i inputs: - name: requestId type: uint256 - - name: requester + - name: controller type: address outputs: @@ -187,7 +187,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i Assumes control of `shares` from `owner` and submits a Request for asynchronous `redeem`. This places the Request in Pending state, with a corresponding increase in `pendingRedeemRequest` for the amount `shares`. -The output `requestId` is used to partially discriminate the request along with the `requester`. See [Request Ids](#request-ids) section for more info. +The output `requestId` is used to partially discriminate the request along with the `controller`. See [Request Ids](#request-ids) section for more info. MAY support either a locking or a burning mechanism for `shares` depending on the Vault implementation. @@ -195,7 +195,7 @@ If a Vault uses a locking mechanism for `shares`, those `shares` MUST be burned MUST support a redeem Request flow where the control of `shares` is taken from `owner` directly where `msg.sender` has ERC-20 approval over the `shares` of `owner`, or the `owner` has approved the `msg.sender` as an operator. -When the Request is Claimable, `claimableRedeemRequest` will be increased for the `requester`. `redeem` or `withdraw` can subsequently be called by `requester` to receive `assets`. A Request MAY transition straight to Claimable state but MUST NOT skip the Claimable state. +When the Request is Claimable, `claimableRedeemRequest` will be increased for the `controller`. `redeem` or `withdraw` can subsequently be called by `controller` to receive `assets`. A Request MAY transition straight to Claimable state but MUST NOT skip the Claimable state. The `assets` that will be received on `redeem` or `withdraw` MAY NOT be equivalent to the value of `convertToAssets(shares)` at the time of Request, as the price can change between Pending and Claimed. @@ -211,7 +211,7 @@ MUST emit the `RequestRedeem` event. inputs: - name: shares type: uint256 - - name: requester + - name: controller type: address - name: owner type: address @@ -222,7 +222,7 @@ MUST emit the `RequestRedeem` event. #### pendingRedeemRequest -The amount of requested `shares` in Pending state for the `requester` with the given `requestId` to `redeem` or `withdraw`. +The amount of requested `shares` in Pending state for the `controller` with the given `requestId` to `redeem` or `withdraw`. MUST NOT include any `shares` in Claimable state for `redeem` or `withdraw`. @@ -238,7 +238,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i inputs: - name: requestId type: uint256 - - name: requester + - name: controller type: address outputs: @@ -248,7 +248,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i #### claimableRedeemRequest -The amount of requested `shares` in Claimable state for the `requester` with the given `requestId` to `redeem` or `withdraw`. +The amount of requested `shares` in Claimable state for the `controller` with the given `requestId` to `redeem` or `withdraw`. MUST NOT include any `shares` in Pending state for `redeem` or `withdraw`. @@ -264,7 +264,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i inputs: - name: requestId type: uint256 - - name: requester + - name: controller type: address outputs: @@ -274,7 +274,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i #### `isOperator` -Returns `true` if the `operator` is approved as an operator for a `requester`. +Returns `true` if the `operator` is approved as an operator for a `controller`. ```yaml - name: isOperator @@ -282,7 +282,7 @@ Returns `true` if the `operator` is approved as an operator for a `requester`. stateMutability: view inputs: - - name: requester + - name: controller type: address - name: operator type: address @@ -320,22 +320,22 @@ MUST return True. #### `deposit` and `mint` overloaded methods -Implementations MUST support an additional overloaded `deposit` and `mint` method on the specification from [ERC-4626](./eip-4626.md), with an additional `requester` input of type `address`: +Implementations MUST support an additional overloaded `deposit` and `mint` method on the specification from [ERC-4626](./eip-4626.md), with an additional `controller` input of type `address`: -- `deposit(uint256 assets, address receiver, address requester)` -- `mint(uint256 shares, address receiver, address requester)` +- `deposit(uint256 assets, address receiver, address controller)` +- `mint(uint256 shares, address receiver, address controller)` -The `requester` field is used to look up the Request for which the `assets` should be claimed. +The `controller` field is used to look up the Request for which the `assets` should be claimed. -Calls MUST revert unless `msg.sender` is either equal to `requester` or an operator approved by `requester`. +Calls MUST revert unless `msg.sender` is either equal to `controller` or an operator approved by `controller`. -When the `Deposit` event is emitted, the first parameter MUST be the `requester`, and the second parameter MUST be the `receiver`. +When the `Deposit` event is emitted, the first parameter MUST be the `controller`, and the second parameter MUST be the `receiver`. ### Events #### DepositRequest -`owner` has locked `assets` in the Vault to Request a deposit with request ID `requestId`. `requester` controls this Request. `sender` is the caller of the `requestDeposit` which may not be equal to the `owner`. +`owner` has locked `assets` in the Vault to Request a deposit with request ID `requestId`. `controller` controls this Request. `sender` is the caller of the `requestDeposit` which may not be equal to the `owner`. MUST be emitted when a deposit Request is submitted using the `requestDeposit` method. @@ -344,7 +344,7 @@ MUST be emitted when a deposit Request is submitted using the `requestDeposit` m type: event inputs: - - name: requester + - name: controller indexed: true type: address - name: owner @@ -363,7 +363,7 @@ MUST be emitted when a deposit Request is submitted using the `requestDeposit` m #### RedeemRequest -`sender` has locked `shares`, owned by `owner`, in the Vault to Request a redemption. `requester` controls this Request, but is not necessarily the `owner`. +`sender` has locked `shares`, owned by `owner`, in the Vault to Request a redemption. `controller` controls this Request, but is not necessarily the `owner`. MUST be emitted when a redemption Request is submitted using the `requestRedeem` method. @@ -372,7 +372,7 @@ MUST be emitted when a redemption Request is submitted using the `requestRedeem` type: event inputs: - - name: requester + - name: controller indexed: true type: address - name: owner @@ -391,7 +391,7 @@ MUST be emitted when a redemption Request is submitted using the `requestRedeem` #### `OperatorSet` -The `requester` has set the `approved` status to an `operator`. +The `controller` has set the `approved` status to an `operator`. MUST be logged when the operator status is set. @@ -402,7 +402,7 @@ MAY be logged when the operator status is set to the same status it was before t type: event inputs: - - name: requester + - name: controller indexed: true type: address - name: operator @@ -482,7 +482,7 @@ The state transition of a Request from Pending to Claimable happens at the Vault ### Reversion of Preview Functions in Async Request Flows -The preview functions do not take an address parameter, therefore the only way to discriminate discrepancies in the exchange rate is via the `msg.sender`. However, this could lead to integration/implementation complexities where support contracts cannot determine the output of a claim on behalf of a `requester`. +The preview functions do not take an address parameter, therefore the only way to discriminate discrepancies in the exchange rate is via the `msg.sender`. However, this could lead to integration/implementation complexities where support contracts cannot determine the output of a claim on behalf of a `controller`. In addition, there is no on-chain benefit to previewing the Claim step as the only valid state transition is to Claim anyway. If the output of a Claim is undesirable for any reason, the calling contract can revert on the output of that function call. @@ -508,9 +508,9 @@ The interface is fully backward compatible with [ERC-4626](./eip-4626.md). The s mapping(address => uint256) public claimableDepositRequest; - mapping(address requester => mapping(address operator => bool)) public isOperator; + mapping(address controller => mapping(address operator => bool)) public isOperator; - function requestDeposit(uint256 assets, address requester, address owner) external returns (uint256 requestId) { + function requestDeposit(uint256 assets, address controller, address owner) external returns (uint256 requestId) { require(assets != 0); require(owner == msg.sender || isOperator[owner][msg.sender]); @@ -518,9 +518,9 @@ The interface is fully backward compatible with [ERC-4626](./eip-4626.md). The s asset.safeTransferFrom(owner, address(this), assets); // asset here is the Vault underlying asset - pendingDepositRequest[requester] += assets; + pendingDepositRequest[controller] += assets; - emit DepositRequest(requester, owner, requestId, msg.sender, assets); + emit DepositRequest(controller, owner, requestId, msg.sender, assets); return requestId; } @@ -528,17 +528,17 @@ The interface is fully backward compatible with [ERC-4626](./eip-4626.md). The s * Include some arbitrary transition logic here from Pending to Claimable */ - function deposit(uint256 assets, address receiver, address requester) external returns (uint256 shares) { + function deposit(uint256 assets, address receiver, address controller) external returns (uint256 shares) { require(assets != 0); - require(requester == msg.sender || isOperator[requester][msg.sender]); + require(controller == msg.sender || isOperator[controller][msg.sender]); - claimableDepositRequest[requester] -= assets; // underflow would revert if not enough claimable assets + claimableDepositRequest[controller] -= assets; // underflow would revert if not enough claimable assets shares = convertToShares(assets); // this naive example uses the instantaneous exchange rate. It may be more common to use the rate locked in upon Claimable stage. balanceOf[receiver] += shares; - emit Deposit(requester, receiver, assets, shares); + emit Deposit(controller, receiver, assets, shares); } function setOperator(address operator, bool approved) public returns (bool) { From 3033abc21892360e91bc5eb76ab75aaf70000567 Mon Sep 17 00:00:00 2001 From: Josh Weintraub Date: Wed, 29 May 2024 17:03:13 -0400 Subject: [PATCH 2/4] Update ERC-6551: Update erc-6551.md Merged by EIP-Bot. --- ERCS/erc-6551.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-6551.md b/ERCS/erc-6551.md index 5e0bd1a450..ebe6f689c2 100644 --- a/ERCS/erc-6551.md +++ b/ERCS/erc-6551.md @@ -2,7 +2,7 @@ eip: 6551 title: Non-fungible Token Bound Accounts description: An interface and registry for smart contract accounts owned by non-fungible tokens -author: Jayden Windle (@jaydenwindle), Benny Giang , Steve Jang, Druzy Downs (@druzydowns), Raymond Huynh (@huynhr), Alanah Lam , Wilkins Chung (@wwhchung) , Paul Sullivan (@sullivph) , Auryn Macmillan (@auryn-macmillan), Jan-Felix Schwarz (@jfschwarz), Anton Bukov (@k06a), Mikhail Melnik (@ZumZoom), Josh Weintraub (@jhweintraub) , Rob Montgomery (@RobAnon) , vectorized (@vectorized), Víctor Martínez (@vnmrtz), Adrián Pajares (@0xadrii) +author: Jayden Windle (@jaydenwindle), Benny Giang , Steve Jang, Druzy Downs (@druzydowns), Raymond Huynh (@huynhr), Alanah Lam , Wilkins Chung (@wwhchung) , Paul Sullivan (@sullivph) , Auryn Macmillan (@auryn-macmillan), Jan-Felix Schwarz (@jfschwarz), Anton Bukov (@k06a), Mikhail Melnik (@ZumZoom), Josh Weintraub (@jhweintraub) , Rob Montgomery (@RobAnon) , vectorized (@vectorized), Víctor Martínez (@vnmrtz), Adrián Pajares (@0xadrii) discussions-to: https://ethereum-magicians.org/t/non-fungible-token-bound-accounts/13030 status: Review type: Standards Track From 4b7fd77dbe95d3bc42023696eedcda3786eb6eb5 Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Thu, 30 May 2024 21:55:07 +0200 Subject: [PATCH 3/4] Update ERC-7540: Update ERC7540 Merged by EIP-Bot. --- ERCS/erc-7540.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-7540.md b/ERCS/erc-7540.md index 1e54476d45..b59cbf6f33 100644 --- a/ERCS/erc-7540.md +++ b/ERCS/erc-7540.md @@ -2,7 +2,7 @@ eip: 7540 title: Asynchronous ERC-4626 Tokenized Vaults description: Extension of ERC-4626 with asynchronous deposit and redemption support -author: Jeroen Offerijns (@hieronx), Alina Sinelnikova (@ilinzweilin), Vikram Arun (@vikramarun), Joey Santoro (@joeysantoro), Farhaan Ali (@0xfarhaan) +author: Jeroen Offerijns (@hieronx), Alina Sinelnikova (@ilinzweilin), Vikram Arun (@vikramarun), Joey Santoro (@joeysantoro), Farhaan Ali (@0xfarhaan), João Martins (@0xTimepunk) discussions-to: https://ethereum-magicians.org/t/eip-7540-asynchronous-erc-4626-tokenized-vaults/16153 status: Review type: Standards Track From 2ae01704b2e5ce0f9648a99f3f99614b25f83563 Mon Sep 17 00:00:00 2001 From: "drCathieSo.eth" Date: Fri, 31 May 2024 05:08:35 +0800 Subject: [PATCH 4/4] Website: Intrinsic RevShare Token Merged by EIP-Bot. --- ERCS/erc-7641.md | 189 ++++++++++++++++++ assets/erc-7641/.gitignore | 14 ++ assets/erc-7641/ERC7641.js | 185 +++++++++++++++++ assets/erc-7641/ERC7641.sol | 145 ++++++++++++++ assets/erc-7641/IERC7641.sol | 43 ++++ assets/erc-7641/IERC7641AltRevToken.sol | 26 +++ assets/erc-7641/README.md | 3 + assets/erc-7641/contracts/ERC7641.sol | 145 ++++++++++++++ assets/erc-7641/contracts/IERC7641.sol | 43 ++++ .../contracts/IERC7641AltRevToken.sol | 26 +++ assets/erc-7641/hardhat.config.js | 6 + assets/erc-7641/package.json | 17 ++ assets/erc-7641/test/ERC7641.js | 185 +++++++++++++++++ 13 files changed, 1027 insertions(+) create mode 100644 ERCS/erc-7641.md create mode 100644 assets/erc-7641/.gitignore create mode 100644 assets/erc-7641/ERC7641.js create mode 100644 assets/erc-7641/ERC7641.sol create mode 100644 assets/erc-7641/IERC7641.sol create mode 100644 assets/erc-7641/IERC7641AltRevToken.sol create mode 100644 assets/erc-7641/README.md create mode 100644 assets/erc-7641/contracts/ERC7641.sol create mode 100644 assets/erc-7641/contracts/IERC7641.sol create mode 100644 assets/erc-7641/contracts/IERC7641AltRevToken.sol create mode 100644 assets/erc-7641/hardhat.config.js create mode 100644 assets/erc-7641/package.json create mode 100644 assets/erc-7641/test/ERC7641.js diff --git a/ERCS/erc-7641.md b/ERCS/erc-7641.md new file mode 100644 index 0000000000..b18dc1fd0c --- /dev/null +++ b/ERCS/erc-7641.md @@ -0,0 +1,189 @@ +--- +eip: 7641 +title: Intrinsic RevShare Token +description: An ERC-20 extension that integrates a revenue-sharing mechanism, ensuring tokens intrinsically represent a share of a communal revenue pool +author: Conway (@0x1cc), Cathie So (@socathie), Xiaohang Yu (@xhyumiracle), Suning Yao (@fewwwww), Kartin +discussions-to: https://ethereum-magicians.org/t/erc-7641-intrinsic-revshare-token/18999 +status: Draft +type: Standards Track +category: ERC +created: 2024-02-28 +requires: 20 +--- + +## Abstract + +This proposal outlines an extension of the prevailing [ERC-20](./eip-20.md) token standard, introducing a seamlessly integrated revenue-sharing mechanism. It incorporates a suite of interfaces designed to foster fair distribution of revenue among token holders while preserving the essential attributes of [ERC-20](./eip-20.md). Central to this design is the establishment of a communal revenue pool, aggregating revenues from diverse sources. The token, in essence, embodies shares, affording holders the ability to burn their tokens and redeem a proportionate share from the revenue pool. This innovative burning mechanism guarantees that, when the revenue pool is non-empty, the token's value remains at least commensurate with the share of the revenue pool. Additionally, in periodic intervals, token holders can claim a portion of the reward, enriching their engagement and further enhancing the token's utility. + +## Motivation + +### Revenue Sharing for Token Holders + +This proposal standardized an Intrinsic RevShare (revenue-sharing) model, allowing users to claim rewards periodically to ensure the efficiency of liquidity. This standard can inherently offer a clear path to long-term benefits for holders with revenue sharing, achieving a more sustainable token model by rewarding holders. + +With the inheritance of [ERC-20](./eip-20.md) functionalities, token holders enjoy flexibility in trading tokens on secondary markets, and an optional burning mechanism empowers them to actively contribute to a deflationary economic model while obtaining a proportional share of the revenue pool. + +This approach also encourages active participation in open-source initiatives with a sustainable and multifaceted revenue-sharing ecosystem for Intrinsic RevShare token holders. + +### Funding for Any Project + +This standard enables the tokenizing of all kinds of projects with revenue. This EIP introduces a new model for incentivizing contributions to open-source projects. It proposes the distribution of Intrinsic RevShare tokens to active contributors, creating a tangible asset reflecting project involvement. + +Notably, it introduces a use case known as Initial Model Offering (IMO). Many open-sourced AI models face a challenge in monetizing their contributions, leading to a lack of motivation for contributors and organizations alike. This proposal seeks to empower open-sourced AI models and organizations by introducing Intrinsic RevShare token. In leveraging the token for IMO, open-sourced AI organizations can conduct fundraisings for essential funds to incentivize the ongoing development of AI models. Moreover, any project utilizing these open-source models contributes to the sustainability of the ecosystem by paying a designated fee to the revenue pool. This fee forms the basis of a revenue-sharing mechanism, allowing Intrinsic RevShare token holders to claim a proportionate share, thereby establishing a systematic and fair distribution mechanism. Importantly, this revenue-sharing feature serves as a guarantee for token holders, fostering long-term revenue benefits and encouraging sustained engagement in the open-source AI community. + + +## 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. + +**Every compliant contract must implement the `IERC7641`, and [ERC-20](./eip-20.md) interfaces.** + +The Intrinsic RevShare Token standard includes the following interfaces: + +`IERC7641`: +- Defines a `claimableRevenue` view function to calculate the amount of ETH claimable by a token holder at a certain snapshot. +- Defines a `claim` function for token holder to claim ETH based on the token balance at certain snapshot. +- Defines a `snapshot` function to snapshot the token balance and the claimable revenue token balance. +- Defines a `redeemableOnBurn` view function to calculate the amount of ETH redeemable by a token holder upon burn. +- Defines a `burn` function for token holder to burn tokens and redeem the corresponding amount of revenue token. + +```solidity +pragma solidity ^0.8.24; + +/** + * @dev An interface for ERC-7641, an ERC-20 extension that integrates a revenue-sharing mechanism, ensuring tokens intrinsically represent a share of a communal revenue pool + */ +interface IERC7641 is IERC20 { + /** + * @dev A function to calculate the amount of ETH claimable by a token holder at certain snapshot. + * @param account The address of the token holder + * @param snapshotId The snapshot id + * @return The amount of revenue token claimable + */ + function claimableRevenue(address account, uint256 snapshotId) external view returns (uint256); + + /** + * @dev A function for token holder to claim ETH based on the token balance at certain snapshot. + * @param snapshotId The snapshot id + */ + function claim(uint256 snapshotId) external; + + /** + * @dev A function to snapshot the token balance and the claimable revenue token balance + * @return The snapshot id + * @notice Should have `require` to avoid ddos attack + */ + function snapshot() external returns (uint256); + + /** + * @dev A function to calculate the amount of ETH redeemable by a token holder upon burn + * @param amount The amount of token to burn + * @return The amount of revenue ETH redeemable + */ + function redeemableOnBurn(uint256 amount) external view returns (uint256); + + /** + * @dev A function to burn tokens and redeem the corresponding amount of revenue token + * @param amount The amount of token to burn + */ + function burn(uint256 amount) external; +} +``` + +### Optional Extension: AltRevToken + +The **AltRevToken extension** is OPTIONAL for this standard. This allows the contract to accept other [ERC-20](./eip-20.md) revenue tokens (more than ETH) into the revenue sharing pool. + +The AltRevToken extension +- Defines a `claimableERC20` function to calculate the amount of [ERC-20](./eip-20.md) claimable by a token holder at certain snapshot. +- Defines a `redeemableERC20OnBurn` function to calculate the amount of [ERC-20](./eip-20.md) redeemable by a token holder upon burn. + +```solidity +pragma solidity ^0.8.24; + +/** + * @dev An optional extension of the ERC-7641 standard that accepts other ERC-20 revenue tokens into the contract with corresponding claim function + */ +interface IERC7641AltRevToken is IERC7641 { + /** + * @dev A function to calculate the amount of ERC-20 claimable by a token holder at certain snapshot. + * @param account The address of the token holder + * @param snapshotId The snapshot id + * @param token The address of the revenue token + * @return The amount of revenue token claimable + */ + function claimableERC20(address account, uint256 snapshotId, address token) external view returns (uint256); + + /** + * @dev A function to calculate the amount of ERC-20 redeemable by a token holder upon burn + * @param amount The amount of token to burn + * @param token The address of the revenue token + * @return The amount of revenue token redeemable + */ + function redeemableERC20OnBurn(uint256 amount, address token) external view returns (uint256); +} +``` + +## Rationale + +### Revenue Sharing Mechanism + +We implement a revenue sharing mechanism wherein any token holder can claim a proportional share from the revenue pool. To ensure regular and transparent revenue distribution, we have incorporated the snapshot method, capturing both the token balance and the associated claimable revenue token balance. Periodic invocation of the snapshot method, corresponding to distinct revenue-sharing processes, is required. During each snapshot, token holders are empowered to claim a proportionate share from the revenue pool, creating a systematic and equitable distribution mechanism for participants. + +### `snapshot` interface + +We specify a `snapshot` interface to snapshot the token balance and the claimable revenue token balance. This functionality ensures correctness in tracking token holdings, facilitating a transparent record of each token portfolio. Regular invocation of the snapshot function is essential to maintain up-to-date records. The `snapshot` interface returns a unique `snapshotId`, allowing access to the corresponding token balance and claimable revenue token balance associated with that specific snapshot. This systematic approach enhances the correctness and reliability of historical data retrieval, providing users with comprehensive insights into their token and revenue token balances at different points in time. + +### `claimableRevenue` interface + +We specify a `claimableRevenue` interface to calculate the amount of ETH claimable by a token holder at a certain snapshot. We will share the revenue between two consecutive snapshots. As an example in our reference implementation, assuming that the revenue between two snapshots is `R`, we specify a revenue sharing ratio `p`, ranging from 0%-100%, and we share the revenue of `pR` to different token holders according to the token ratio. In this example, the amount of ETH claimable by a token holder with `amount` tokens at a certain snapshot is `pR * amount / totalAmount` , where `totalAmount` denotes the total amount of [ERC-7641](./eip-7641.md) token. Noted that the remaining revenue of `(1-p)R` will be retained in the revenue pool, and we can take out this part of revenue through burning. + +### `claim` interface + +We specify a `claim` interface for token holder to claim ETH based on the token balance at certain snapshot. Each token holder can only claim revenue at a certain snapshot once, ensuring a fair and transparent distribution mechanism. + +### Burning Mechanism + +We implement a burning mechanism wherein any token holder can burn their tokens to redeem a proportional share from the revenue pool. This mechanism serves as a guarantee, ensuring that the value of the token is consistently greater than or equal to the share of the revenue pool, promoting a fair and balanced system. + +### `redeemableOnBurn` interface + +We specify `redeemableOnBurn` interface to calculate the amount of ETH redeemable by a token holder upon burn. It is defined as a view function to reduce gas cost. As an example in our reference implementation, the amount of ETH redeemable, i.e., `redeemableETH` by a token holder with `amount` of token to burn is + +```solidity +redeemableETH = amount / totalSupply * totalRedeemableETH +``` + +where `totalSupply` denotes the total supply of [ERC-7641](./eip-7641.md) token, and `totalRedeemableETH` denotes the total amount of ETH in the burning pool. + +### `burn` interface: + +We specify `burn` interface for token holder to burn tokens and redeem the corresponding amount of revenue token. A token holder can burn at most all tokens it holds. This burning process leads to a reduction in the total token supply, establishing a deflationary economic model. Furthermore, it is important to note that tokens once burned are excluded from participating in any subsequent revenue sharing. + +## Backwards Compatibility + +This standard is backward compatible with the [ERC-20](./eip-20.md) as it extends the existing functionality with new interfaces. + +## Test Cases + +The reference implementation includes sample implementations of the interfaces in this standard under `contracts/` and corresponding unit tests under `test/`. + +## Reference Implementation + +- [ERC-7641](../assets/eip-7641/contracts/ERC7641.sol) + +## Security Considerations + +### Deflationary Economic Model + +The introduction of the burning mechanism in this standard signifies a shift towards a deflationary economic model, which introduces unique considerations regarding security. One prominent concern involves the potential impact on token liquidity and market dynamics. The continuous reduction in token supply through burning has the potential to affect liquidity levels, potentially leading to increased volatility and susceptibility to price manipulation. It is essential to conduct thorough stress testing and market simulations to assess the resilience of the system under various scenarios. + +### Spam Revenue Tokens + +The extension of AltRevToken with the ability to set up different revenue tokens introduces specific security considerations, primarily centered around the prevention of adding numerous, potentially worthless tokens. The addition of too many spam (worthless) tokens may lead to an increase in gas fees associated with burning and claiming processes. This can result in inefficiencies and higher transaction costs for users, potentially discouraging participation in revenue-sharing activities. + +A robust governance model is crucial for the approval and addition of new revenue tokens. Implementing a transparent and community-driven decision-making process ensures that only reputable and valuable tokens are introduced, preventing the inclusion of tokens with little to no utility. This governance process should involve community voting, security audits, and careful consideration of the potential impact on gas fees. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/erc-7641/.gitignore b/assets/erc-7641/.gitignore new file mode 100644 index 0000000000..9722dbfaf7 --- /dev/null +++ b/assets/erc-7641/.gitignore @@ -0,0 +1,14 @@ +node_modules +.env + +# Hardhat files +/cache +/artifacts + +# TypeChain files +/typechain +/typechain-types + +# solidity-coverage files +/coverage +/coverage.json diff --git a/assets/erc-7641/ERC7641.js b/assets/erc-7641/ERC7641.js new file mode 100644 index 0000000000..56e62f07f6 --- /dev/null +++ b/assets/erc-7641/ERC7641.js @@ -0,0 +1,185 @@ +const { expect } = require("chai"); +const { ethers, network } = require("hardhat"); + +describe("ERC7641", function () { + let erc7641; + let addr0; + let addr1; + let addr2; + let addrs; + let erc7641Address; + + const percentClaimable = 60; + const supply = 1000000; + const gas = ethers.parseEther("0.001"); + + beforeEach(async function () { + [addr0, addr1, addr2, ...addrs] = await ethers.getSigners(); + const ERC7641 = await ethers.getContractFactory("ERC7641"); + erc7641 = await ERC7641.deploy("ERC7641", "ERCX", supply, percentClaimable); + await erc7641.waitForDeployment(); + erc7641Address = await erc7641.getAddress(); + }); + + describe("Deployment", function () { + it("Should set the right name", async function () { + expect(await erc7641.name()).to.equal("ERC7641"); + }); + + it("Should set the right symbol", async function () { + expect(await erc7641.symbol()).to.equal("ERCX"); + }); + + it("Should set the right total supply", async function () { + expect(await erc7641.totalSupply()).to.equal(supply); + }); + + it("Should assign the total supply to the owner", async function () { + expect(await erc7641.balanceOf(await ethers.provider.getSigner(0))).to.equal(supply); + }); + }); + + describe("Deposit", function () { + it("Should deposit ETH to the contract", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: 1000 }); + expect(await ethers.provider.getBalance(erc7641Address)).to.equal(1000); + }); + }); + + describe("Snapshot", function () { + it("Should not snapshot if 1000 blocks have not passed", async function () { + await expect(erc7641.snapshot()).to.be.revertedWith("ERC7641: snapshot interval is too short"); + }); + + it("Should snapshot if > 1000 blocks have passed", async function () { + await network.provider.send("hardhat_mine", ["0x400"]); + expect(await erc7641.snapshot()).to.emit(erc7641, "Snapshot"); + }); + }); + + describe("Burn", function () { + it("Should burn tokens", async function () { + expect(await erc7641.redeemableOnBurn(10000)).to.equal(0); + await erc7641.burn(10000); + expect(await erc7641.balanceOf(await ethers.provider.getSigner(0))).to.equal(supply-10000); + }); + + it("Should burn tokens and receive ETH", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + expect(await ethers.provider.getBalance(erc7641Address)).to.equal(ethers.parseEther("1000")); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(ethers.parseEther("1000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.burn(10000); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("1000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)-gas); + }); + + it("Should snapshot and burn tokens", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(ethers.parseEther("1000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.burn(10000); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("1000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)-gas); + }); + + it("Should snapshot, deposit, and burn", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(ethers.parseEther("2000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.burn(10000); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("2000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)-gas); + }); + }); + + describe("Claim", function () { + it("Should not claim if no snapshot has been taken", async function () { + await expect(erc7641.claim(1)).to.be.revertedWith("ERC20Snapshot: nonexistent id"); + }); + + it("Should claim after snapshot", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + expect(await erc7641.claimableRevenue(addr0, 1)).to.equal(ethers.parseEther("1000")*BigInt(percentClaimable)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.claim(1); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("1000")*BigInt(percentClaimable)/BigInt(100)-gas); + }); + + it("Should claim after snapshot and deposit", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + expect(await erc7641.claimableRevenue(addr0, 1)).to.equal(ethers.parseEther("1000")*BigInt(percentClaimable)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.claim(1); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("1000")*BigInt(percentClaimable)/BigInt(100)-gas); + }); + + it("Should claim correctly after snapshot with two holders", async function () { + await erc7641.transfer(addr1.address, 100000); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + expect(await erc7641.claimableRevenue(addr0, 1)).to.equal(ethers.parseEther("1000")*BigInt(supply-100000)*BigInt(percentClaimable)/BigInt(100)/BigInt(supply)); + expect(await erc7641.claimableRevenue(addr1, 1)).to.equal(ethers.parseEther("1000")*BigInt(100000)*BigInt(percentClaimable)/BigInt(100)/BigInt(supply)); + const balanceBefore0 = await ethers.provider.getBalance(addr0.address); + const balanceBefore1 = await ethers.provider.getBalance(addr1.address); + await erc7641.claim(1); + await erc7641.connect(addr1).claim(1); + const balanceAfter0 = await ethers.provider.getBalance(addr0.address); + const balanceAfter1 = await ethers.provider.getBalance(addr1.address); + expect(balanceAfter0-balanceBefore0).to.greaterThan(ethers.parseEther("1000")*BigInt(supply-100000)*BigInt(percentClaimable)/BigInt(100)/BigInt(supply)-gas); + expect(balanceAfter1-balanceBefore1).to.greaterThan(ethers.parseEther("1000")*BigInt(100000)*BigInt(percentClaimable)/BigInt(100)/BigInt(supply)-gas); + }); + + it("Should claim multiple snapshots correctly", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("2000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + expect(await erc7641.claimableRevenue(addr0, 1)).to.equal(ethers.parseEther("1000")*BigInt(percentClaimable)/BigInt(100)); + expect(await erc7641.claimableRevenue(addr0, 2)).to.equal(ethers.parseEther("2000")*BigInt(percentClaimable)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.claimBatch([1, 2]); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("3000")*BigInt(percentClaimable)/BigInt(100)-gas); + }); + }); + + describe("Mixed operations", function () { + it("deposit -> snapshot -> deposit -> burn -> deposit -> burn -> snapshot -> claim -> burn", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("100") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("100") }); + let redeemed = ethers.parseEther("200")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(redeemed); + await erc7641.burn(10000); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("100") }); + redeemed += ethers.parseEther("100")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply-10000)/BigInt(100); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(redeemed); + await erc7641.burn(10000); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + expect(await erc7641.claimableRevenue(addr0, 2)).to.equal(ethers.parseEther("200")*BigInt(percentClaimable)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.claim(2); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("200")*BigInt(percentClaimable)/BigInt(100)-gas); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(redeemed); + }); + }); +}); diff --git a/assets/erc-7641/ERC7641.sol b/assets/erc-7641/ERC7641.sol new file mode 100644 index 0000000000..4a5d314f57 --- /dev/null +++ b/assets/erc-7641/ERC7641.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./IERC7641.sol"; + +contract ERC7641 is ERC20Snapshot, IERC7641 { + /** + * @dev last snapshotted block + */ + uint256 private _lastSnapshotBlock; + + /** + * @dev percentage claimable + */ + uint256 immutable public percentClaimable; + + /** + * @dev mapping from snapshot id to address to the amount of ETH claimable at the snapshot. + */ + mapping (uint256 => uint256) private _claimableAtSnapshot; + + /** + * @dev mapping from snapshot id to a boolean indicating whether the address has claimed the revenue. + */ + mapping (uint256 => mapping (address => bool)) private _claimedAtSnapshot; + + /** + * @dev claim pool + */ + uint256 private _claimPool; + + /** + * @dev burn pool + */ + uint256 private _burnPool; + + /** + * @dev burned from new revenue + */ + uint256 private _burned; + + /** + * @dev Constructor for the ERC7641 contract, premint the total supply to the contract creator. + * @param name The name of the token + * @param symbol The symbol of the token + * @param supply The total supply of the token + */ + constructor(string memory name, string memory symbol, uint256 supply, uint256 _percentClaimable) ERC20(name, symbol) { + require(_percentClaimable <= 100, "ERC7641: percentage claimable should be less than 100"); + percentClaimable = _percentClaimable; + _lastSnapshotBlock = block.number; + _mint(msg.sender, supply); + } + + /** + * @dev A function to calculate the amount of ETH claimable by a token holder at certain snapshot. + * @param account The address of the token holder + * @param snapshotId The snapshot id + * @return The amount of revenue token claimable + */ + function claimableRevenue(address account, uint256 snapshotId) public view returns (uint256) { + uint256 balance = balanceOfAt(account, snapshotId); + uint256 totalSupply = totalSupplyAt(snapshotId); + uint256 ethClaimable = _claimableAtSnapshot[snapshotId]; + return _claimedAtSnapshot[snapshotId][account] ? 0 : balance * ethClaimable / totalSupply; + } + + /** + * @dev A function for token holder to claim revenue token based on the token balance at certain snapshot. + * @param snapshotId The snapshot id + */ + function claim(uint256 snapshotId) public { + uint256 claimableETH = claimableRevenue(msg.sender, snapshotId); + require(claimableETH > 0, "ERC7641: no claimable ETH"); + + _claimedAtSnapshot[snapshotId][msg.sender] = true; + _claimPool -= claimableETH; + (bool success, ) = msg.sender.call{value: claimableETH}(""); + require(success, "ERC7641: claim failed"); + } + + /** + * @dev A function to claim by a list of snapshot ids. + * @param snapshotIds The list of snapshot ids + */ + function claimBatch(uint256[] memory snapshotIds) public { + for (uint256 i = 0; i < snapshotIds.length; i++) { + claim(snapshotIds[i]); + } + } + + /** + * @dev A snapshot function that also records the deposited ETH amount at the time of the snapshot. + * @return The snapshot id + * @notice example requirement: only 1000 blocks after the last snapshot + */ + function snapshot() public returns (uint256) { + require(block.number - _lastSnapshotBlock > 1000, "ERC7641: snapshot interval is too short"); + uint256 snapshotId = _snapshot(); + _lastSnapshotBlock = block.number; + + uint256 newRevenue = address(this).balance + _burned - _claimPool - _burnPool; + + uint256 claimableETH = newRevenue * percentClaimable / 100; + _claimableAtSnapshot[snapshotId] = claimableETH; + _claimPool += claimableETH; + _burnPool += newRevenue - claimableETH - _burned; + _burned = 0; + + return snapshotId; + } + + /** + * @dev A function to calculate the amount of ETH redeemable by a token holder upon burn + * @param amount The amount of token to burn + * @return The amount of revenue ETH redeemable + */ + function redeemableOnBurn(uint256 amount) public view returns (uint256) { + uint256 totalSupply = totalSupply(); + uint256 newRevenue = address(this).balance + _burned - _claimPool - _burnPool; + uint256 burnableFromNewRevenue = amount * (newRevenue * (100 - percentClaimable) - _burned * 100) / 100 / totalSupply; + uint256 burnableFromPool = amount * _burnPool / totalSupply; + return burnableFromNewRevenue + burnableFromPool; + } + + /** + * @dev A function to burn tokens and redeem the corresponding amount of revenue token + * @param amount The amount of token to burn + */ + function burn(uint256 amount) public { + uint256 totalSupply = totalSupply(); + uint256 newRevenue = address(this).balance + _burned - _claimPool - _burnPool; + uint256 burnableFromNewRevenue = amount * (newRevenue * (100 - percentClaimable) - _burned * 100) / 100 / totalSupply; + uint256 burnableFromPool = amount * _burnPool / totalSupply; + _burnPool -= burnableFromPool; + _burned += burnableFromNewRevenue; + _burn(msg.sender, amount); + (bool success, ) = msg.sender.call{value: burnableFromNewRevenue + burnableFromPool}(""); + require(success, "ERC7641: burn failed"); + } + + receive() external payable {} +} \ No newline at end of file diff --git a/assets/erc-7641/IERC7641.sol b/assets/erc-7641/IERC7641.sol new file mode 100644 index 0000000000..fc860df1f6 --- /dev/null +++ b/assets/erc-7641/IERC7641.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @dev An interface for ERC-7641, an ERC-20 extension that integrates a revenue-sharing mechanism, ensuring tokens intrinsically represent a share of a communal revenue pool + */ +interface IERC7641 is IERC20 { + /** + * @dev A function to calculate the amount of ETH claimable by a token holder at certain snapshot. + * @param account The address of the token holder + * @param snapshotId The snapshot id + * @return The amount of revenue token claimable + */ + function claimableRevenue(address account, uint256 snapshotId) external view returns (uint256); + + /** + * @dev A function for token holder to claim ETH based on the token balance at certain snapshot. + * @param snapshotId The snapshot id + */ + function claim(uint256 snapshotId) external; + + /** + * @dev A function to snapshot the token balance and the claimable revenue token balance + * @return The snapshot id + * @notice Should have `require` to avoid ddos attack + */ + function snapshot() external returns (uint256); + + /** + * @dev A function to calculate the amount of ETH redeemable by a token holder upon burn + * @param amount The amount of token to burn + * @return The amount of revenue ETH redeemable + */ + function redeemableOnBurn(uint256 amount) external view returns (uint256); + + /** + * @dev A function to burn tokens and redeem the corresponding amount of revenue token + * @param amount The amount of token to burn + */ + function burn(uint256 amount) external; +} diff --git a/assets/erc-7641/IERC7641AltRevToken.sol b/assets/erc-7641/IERC7641AltRevToken.sol new file mode 100644 index 0000000000..fcca290ba1 --- /dev/null +++ b/assets/erc-7641/IERC7641AltRevToken.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "./IERC7641.sol"; + +/** + * @dev An optional extension of the ERC-7641 standard that accepts other ERC-20 revenue tokens into the contract with corresponding claim function + */ +interface IERC7641AltRevToken is IERC7641 { + /** + * @dev A function to calculate the amount of ERC-20 claimable by a token holder at certain snapshot. + * @param account The address of the token holder + * @param snapshotId The snapshot id + * @param token The address of the revenue token + * @return The amount of revenue token claimable + */ + function claimableERC20(address account, uint256 snapshotId, address token) external view returns (uint256); + + /** + * @dev A function to calculate the amount of ERC-20 redeemable by a token holder upon burn + * @param amount The amount of token to burn + * @param token The address of the revenue token + * @return The amount of revenue token redeemable + */ + function redeemableERC20OnBurn(uint256 amount, address token) external view returns (uint256); +} \ No newline at end of file diff --git a/assets/erc-7641/README.md b/assets/erc-7641/README.md new file mode 100644 index 0000000000..96af201b9c --- /dev/null +++ b/assets/erc-7641/README.md @@ -0,0 +1,3 @@ +# ERC7641: Intrinsic RevShare Token + +An ERC-20 extension that integrates a revenue-sharing mechanism, ensuring tokens intrinsically represent a share of a communal revenue pool diff --git a/assets/erc-7641/contracts/ERC7641.sol b/assets/erc-7641/contracts/ERC7641.sol new file mode 100644 index 0000000000..4a5d314f57 --- /dev/null +++ b/assets/erc-7641/contracts/ERC7641.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./IERC7641.sol"; + +contract ERC7641 is ERC20Snapshot, IERC7641 { + /** + * @dev last snapshotted block + */ + uint256 private _lastSnapshotBlock; + + /** + * @dev percentage claimable + */ + uint256 immutable public percentClaimable; + + /** + * @dev mapping from snapshot id to address to the amount of ETH claimable at the snapshot. + */ + mapping (uint256 => uint256) private _claimableAtSnapshot; + + /** + * @dev mapping from snapshot id to a boolean indicating whether the address has claimed the revenue. + */ + mapping (uint256 => mapping (address => bool)) private _claimedAtSnapshot; + + /** + * @dev claim pool + */ + uint256 private _claimPool; + + /** + * @dev burn pool + */ + uint256 private _burnPool; + + /** + * @dev burned from new revenue + */ + uint256 private _burned; + + /** + * @dev Constructor for the ERC7641 contract, premint the total supply to the contract creator. + * @param name The name of the token + * @param symbol The symbol of the token + * @param supply The total supply of the token + */ + constructor(string memory name, string memory symbol, uint256 supply, uint256 _percentClaimable) ERC20(name, symbol) { + require(_percentClaimable <= 100, "ERC7641: percentage claimable should be less than 100"); + percentClaimable = _percentClaimable; + _lastSnapshotBlock = block.number; + _mint(msg.sender, supply); + } + + /** + * @dev A function to calculate the amount of ETH claimable by a token holder at certain snapshot. + * @param account The address of the token holder + * @param snapshotId The snapshot id + * @return The amount of revenue token claimable + */ + function claimableRevenue(address account, uint256 snapshotId) public view returns (uint256) { + uint256 balance = balanceOfAt(account, snapshotId); + uint256 totalSupply = totalSupplyAt(snapshotId); + uint256 ethClaimable = _claimableAtSnapshot[snapshotId]; + return _claimedAtSnapshot[snapshotId][account] ? 0 : balance * ethClaimable / totalSupply; + } + + /** + * @dev A function for token holder to claim revenue token based on the token balance at certain snapshot. + * @param snapshotId The snapshot id + */ + function claim(uint256 snapshotId) public { + uint256 claimableETH = claimableRevenue(msg.sender, snapshotId); + require(claimableETH > 0, "ERC7641: no claimable ETH"); + + _claimedAtSnapshot[snapshotId][msg.sender] = true; + _claimPool -= claimableETH; + (bool success, ) = msg.sender.call{value: claimableETH}(""); + require(success, "ERC7641: claim failed"); + } + + /** + * @dev A function to claim by a list of snapshot ids. + * @param snapshotIds The list of snapshot ids + */ + function claimBatch(uint256[] memory snapshotIds) public { + for (uint256 i = 0; i < snapshotIds.length; i++) { + claim(snapshotIds[i]); + } + } + + /** + * @dev A snapshot function that also records the deposited ETH amount at the time of the snapshot. + * @return The snapshot id + * @notice example requirement: only 1000 blocks after the last snapshot + */ + function snapshot() public returns (uint256) { + require(block.number - _lastSnapshotBlock > 1000, "ERC7641: snapshot interval is too short"); + uint256 snapshotId = _snapshot(); + _lastSnapshotBlock = block.number; + + uint256 newRevenue = address(this).balance + _burned - _claimPool - _burnPool; + + uint256 claimableETH = newRevenue * percentClaimable / 100; + _claimableAtSnapshot[snapshotId] = claimableETH; + _claimPool += claimableETH; + _burnPool += newRevenue - claimableETH - _burned; + _burned = 0; + + return snapshotId; + } + + /** + * @dev A function to calculate the amount of ETH redeemable by a token holder upon burn + * @param amount The amount of token to burn + * @return The amount of revenue ETH redeemable + */ + function redeemableOnBurn(uint256 amount) public view returns (uint256) { + uint256 totalSupply = totalSupply(); + uint256 newRevenue = address(this).balance + _burned - _claimPool - _burnPool; + uint256 burnableFromNewRevenue = amount * (newRevenue * (100 - percentClaimable) - _burned * 100) / 100 / totalSupply; + uint256 burnableFromPool = amount * _burnPool / totalSupply; + return burnableFromNewRevenue + burnableFromPool; + } + + /** + * @dev A function to burn tokens and redeem the corresponding amount of revenue token + * @param amount The amount of token to burn + */ + function burn(uint256 amount) public { + uint256 totalSupply = totalSupply(); + uint256 newRevenue = address(this).balance + _burned - _claimPool - _burnPool; + uint256 burnableFromNewRevenue = amount * (newRevenue * (100 - percentClaimable) - _burned * 100) / 100 / totalSupply; + uint256 burnableFromPool = amount * _burnPool / totalSupply; + _burnPool -= burnableFromPool; + _burned += burnableFromNewRevenue; + _burn(msg.sender, amount); + (bool success, ) = msg.sender.call{value: burnableFromNewRevenue + burnableFromPool}(""); + require(success, "ERC7641: burn failed"); + } + + receive() external payable {} +} \ No newline at end of file diff --git a/assets/erc-7641/contracts/IERC7641.sol b/assets/erc-7641/contracts/IERC7641.sol new file mode 100644 index 0000000000..fc860df1f6 --- /dev/null +++ b/assets/erc-7641/contracts/IERC7641.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @dev An interface for ERC-7641, an ERC-20 extension that integrates a revenue-sharing mechanism, ensuring tokens intrinsically represent a share of a communal revenue pool + */ +interface IERC7641 is IERC20 { + /** + * @dev A function to calculate the amount of ETH claimable by a token holder at certain snapshot. + * @param account The address of the token holder + * @param snapshotId The snapshot id + * @return The amount of revenue token claimable + */ + function claimableRevenue(address account, uint256 snapshotId) external view returns (uint256); + + /** + * @dev A function for token holder to claim ETH based on the token balance at certain snapshot. + * @param snapshotId The snapshot id + */ + function claim(uint256 snapshotId) external; + + /** + * @dev A function to snapshot the token balance and the claimable revenue token balance + * @return The snapshot id + * @notice Should have `require` to avoid ddos attack + */ + function snapshot() external returns (uint256); + + /** + * @dev A function to calculate the amount of ETH redeemable by a token holder upon burn + * @param amount The amount of token to burn + * @return The amount of revenue ETH redeemable + */ + function redeemableOnBurn(uint256 amount) external view returns (uint256); + + /** + * @dev A function to burn tokens and redeem the corresponding amount of revenue token + * @param amount The amount of token to burn + */ + function burn(uint256 amount) external; +} diff --git a/assets/erc-7641/contracts/IERC7641AltRevToken.sol b/assets/erc-7641/contracts/IERC7641AltRevToken.sol new file mode 100644 index 0000000000..fcca290ba1 --- /dev/null +++ b/assets/erc-7641/contracts/IERC7641AltRevToken.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "./IERC7641.sol"; + +/** + * @dev An optional extension of the ERC-7641 standard that accepts other ERC-20 revenue tokens into the contract with corresponding claim function + */ +interface IERC7641AltRevToken is IERC7641 { + /** + * @dev A function to calculate the amount of ERC-20 claimable by a token holder at certain snapshot. + * @param account The address of the token holder + * @param snapshotId The snapshot id + * @param token The address of the revenue token + * @return The amount of revenue token claimable + */ + function claimableERC20(address account, uint256 snapshotId, address token) external view returns (uint256); + + /** + * @dev A function to calculate the amount of ERC-20 redeemable by a token holder upon burn + * @param amount The amount of token to burn + * @param token The address of the revenue token + * @return The amount of revenue token redeemable + */ + function redeemableERC20OnBurn(uint256 amount, address token) external view returns (uint256); +} \ No newline at end of file diff --git a/assets/erc-7641/hardhat.config.js b/assets/erc-7641/hardhat.config.js new file mode 100644 index 0000000000..8ba99bc91c --- /dev/null +++ b/assets/erc-7641/hardhat.config.js @@ -0,0 +1,6 @@ +require("@nomicfoundation/hardhat-toolbox"); + +/** @type import('hardhat/config').HardhatUserConfig */ +module.exports = { + solidity: "0.8.24", +}; diff --git a/assets/erc-7641/package.json b/assets/erc-7641/package.json new file mode 100644 index 0000000000..6be5c6c9b4 --- /dev/null +++ b/assets/erc-7641/package.json @@ -0,0 +1,17 @@ +{ + "name": "erc7641", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "@openzeppelin/contracts": "^4.9.5", + "hardhat": "^2.20.1" + } +} diff --git a/assets/erc-7641/test/ERC7641.js b/assets/erc-7641/test/ERC7641.js new file mode 100644 index 0000000000..56e62f07f6 --- /dev/null +++ b/assets/erc-7641/test/ERC7641.js @@ -0,0 +1,185 @@ +const { expect } = require("chai"); +const { ethers, network } = require("hardhat"); + +describe("ERC7641", function () { + let erc7641; + let addr0; + let addr1; + let addr2; + let addrs; + let erc7641Address; + + const percentClaimable = 60; + const supply = 1000000; + const gas = ethers.parseEther("0.001"); + + beforeEach(async function () { + [addr0, addr1, addr2, ...addrs] = await ethers.getSigners(); + const ERC7641 = await ethers.getContractFactory("ERC7641"); + erc7641 = await ERC7641.deploy("ERC7641", "ERCX", supply, percentClaimable); + await erc7641.waitForDeployment(); + erc7641Address = await erc7641.getAddress(); + }); + + describe("Deployment", function () { + it("Should set the right name", async function () { + expect(await erc7641.name()).to.equal("ERC7641"); + }); + + it("Should set the right symbol", async function () { + expect(await erc7641.symbol()).to.equal("ERCX"); + }); + + it("Should set the right total supply", async function () { + expect(await erc7641.totalSupply()).to.equal(supply); + }); + + it("Should assign the total supply to the owner", async function () { + expect(await erc7641.balanceOf(await ethers.provider.getSigner(0))).to.equal(supply); + }); + }); + + describe("Deposit", function () { + it("Should deposit ETH to the contract", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: 1000 }); + expect(await ethers.provider.getBalance(erc7641Address)).to.equal(1000); + }); + }); + + describe("Snapshot", function () { + it("Should not snapshot if 1000 blocks have not passed", async function () { + await expect(erc7641.snapshot()).to.be.revertedWith("ERC7641: snapshot interval is too short"); + }); + + it("Should snapshot if > 1000 blocks have passed", async function () { + await network.provider.send("hardhat_mine", ["0x400"]); + expect(await erc7641.snapshot()).to.emit(erc7641, "Snapshot"); + }); + }); + + describe("Burn", function () { + it("Should burn tokens", async function () { + expect(await erc7641.redeemableOnBurn(10000)).to.equal(0); + await erc7641.burn(10000); + expect(await erc7641.balanceOf(await ethers.provider.getSigner(0))).to.equal(supply-10000); + }); + + it("Should burn tokens and receive ETH", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + expect(await ethers.provider.getBalance(erc7641Address)).to.equal(ethers.parseEther("1000")); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(ethers.parseEther("1000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.burn(10000); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("1000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)-gas); + }); + + it("Should snapshot and burn tokens", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(ethers.parseEther("1000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.burn(10000); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("1000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)-gas); + }); + + it("Should snapshot, deposit, and burn", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(ethers.parseEther("2000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.burn(10000); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("2000")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100)-gas); + }); + }); + + describe("Claim", function () { + it("Should not claim if no snapshot has been taken", async function () { + await expect(erc7641.claim(1)).to.be.revertedWith("ERC20Snapshot: nonexistent id"); + }); + + it("Should claim after snapshot", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + expect(await erc7641.claimableRevenue(addr0, 1)).to.equal(ethers.parseEther("1000")*BigInt(percentClaimable)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.claim(1); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("1000")*BigInt(percentClaimable)/BigInt(100)-gas); + }); + + it("Should claim after snapshot and deposit", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + expect(await erc7641.claimableRevenue(addr0, 1)).to.equal(ethers.parseEther("1000")*BigInt(percentClaimable)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.claim(1); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("1000")*BigInt(percentClaimable)/BigInt(100)-gas); + }); + + it("Should claim correctly after snapshot with two holders", async function () { + await erc7641.transfer(addr1.address, 100000); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + expect(await erc7641.claimableRevenue(addr0, 1)).to.equal(ethers.parseEther("1000")*BigInt(supply-100000)*BigInt(percentClaimable)/BigInt(100)/BigInt(supply)); + expect(await erc7641.claimableRevenue(addr1, 1)).to.equal(ethers.parseEther("1000")*BigInt(100000)*BigInt(percentClaimable)/BigInt(100)/BigInt(supply)); + const balanceBefore0 = await ethers.provider.getBalance(addr0.address); + const balanceBefore1 = await ethers.provider.getBalance(addr1.address); + await erc7641.claim(1); + await erc7641.connect(addr1).claim(1); + const balanceAfter0 = await ethers.provider.getBalance(addr0.address); + const balanceAfter1 = await ethers.provider.getBalance(addr1.address); + expect(balanceAfter0-balanceBefore0).to.greaterThan(ethers.parseEther("1000")*BigInt(supply-100000)*BigInt(percentClaimable)/BigInt(100)/BigInt(supply)-gas); + expect(balanceAfter1-balanceBefore1).to.greaterThan(ethers.parseEther("1000")*BigInt(100000)*BigInt(percentClaimable)/BigInt(100)/BigInt(supply)-gas); + }); + + it("Should claim multiple snapshots correctly", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("1000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("2000") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + expect(await erc7641.claimableRevenue(addr0, 1)).to.equal(ethers.parseEther("1000")*BigInt(percentClaimable)/BigInt(100)); + expect(await erc7641.claimableRevenue(addr0, 2)).to.equal(ethers.parseEther("2000")*BigInt(percentClaimable)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.claimBatch([1, 2]); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("3000")*BigInt(percentClaimable)/BigInt(100)-gas); + }); + }); + + describe("Mixed operations", function () { + it("deposit -> snapshot -> deposit -> burn -> deposit -> burn -> snapshot -> claim -> burn", async function () { + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("100") }); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("100") }); + let redeemed = ethers.parseEther("200")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply)/BigInt(100); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(redeemed); + await erc7641.burn(10000); + await addr0.sendTransaction({ to: erc7641Address, value: ethers.parseEther("100") }); + redeemed += ethers.parseEther("100")*BigInt(10000)*BigInt(100-percentClaimable)/BigInt(supply-10000)/BigInt(100); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(redeemed); + await erc7641.burn(10000); + await network.provider.send("hardhat_mine", ["0x400"]); + await erc7641.snapshot(); + expect(await erc7641.claimableRevenue(addr0, 2)).to.equal(ethers.parseEther("200")*BigInt(percentClaimable)/BigInt(100)); + const balanceBefore = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + await erc7641.claim(2); + const balanceAfter = await ethers.provider.getBalance(await ethers.provider.getSigner(0)); + expect(balanceAfter-balanceBefore).to.greaterThan(ethers.parseEther("200")*BigInt(percentClaimable)/BigInt(100)-gas); + expect(await erc7641.redeemableOnBurn(10000)).to.equal(redeemed); + }); + }); +});