feat: add revenue sharing specs#47
Conversation
|
|
||
| A new predeploy contract, `FeeSplitter`, is introduced to manage the distribution of all fees collected on L2. | ||
| Instead of having immutable recipients, the `RECIPIENT` address in each of the four `FeeVault` contracts will | ||
| be set to the `FeeSplitter` contract on new chains, while existing chains will be able to opt-in to use this new predeploy. |
There was a problem hiding this comment.
All chains should be able to opt-in
| #### `disburseFees` | ||
|
|
||
| Initiates the routing flow by withdrawing the fees that each of the fee vaults has collected and sends the shares | ||
| to the appropriate addresses according to the configured percentage. | ||
|
|
||
| ```solidity | ||
| function disburseFees() external | ||
| ``` | ||
|
|
||
| - MUST emit `FeesDisbursed` event upon successful execution. | ||
| - MUST emit `NoFeesCollected` event if there are no funds available at the time of the call. | ||
| - MUST revert if not enough time has passed since the last successful execution. | ||
| - MUST send the appropriate amounts to the recipients. | ||
| - The balance of the contract MUST be 0 after a successful execution. |
There was a problem hiding this comment.
Here should be an explanation on the behavior of this function when there are vaults that have a balance below their set setMinWithdrawalAmount, and what NoFeesCollected event means in this context (no fees collected because of minWithdrawalAmount reached or another condition)
|
|
||
| #### `NoFeesCollected` | ||
|
|
||
| Emitted when `disburseFees` is called but there are no funds available in the fee vaults at the time of execution. |
There was a problem hiding this comment.
I would suggest adding "no funds available to withdraw" to avoid confusion on the fact that the vaults have some funds most of the time (unless the specific fee mechanism is arbitrarily set, as is the case with the operatorFee in most of the chains)
| - Recipient A | ||
| - Recipient B |
There was a problem hiding this comment.
We need to rename these names everywhere. We should define something more specific to the usecase, for example: Revenue share recipient and revenue rest recipient
Or something similar
| The function MUST emit the `NoFeesCollected` event if the contract doesn't have any funds after the vaults have been withdrawn, | ||
| which can happen in scenarios where the vaults have a minimum withdrawal amount of 0. | ||
|
|
||
| On chains where any fee vault is not to be used and is not expected to maintain a balance in the contract, |
There was a problem hiding this comment.
I think this sentence is not clear enough
There was a problem hiding this comment.
I think it can be removed, since if there is no fees it won't revert but it just won't withdraw anything.
| Currently this is the case for `OperatorFeeVault` on some chains, so setting its minimum withdrawal amount to | ||
| 0 is needed on those chains. |
There was a problem hiding this comment.
Why is this needed? If the min amount is not met the vault is ignored, right?
|
|
||
| - MUST emit `FeesDisbursed` event upon successful execution. | ||
| - MUST emit `NoFeesCollected` event if there are no funds available in the contract after the vaults have been withdrawn. | ||
| - MUST revert if any vault has a balance below its minimum withdrawal amount. |
There was a problem hiding this comment.
This is wrong, we should not revert if there is some other vault with enough balance
agusduha
left a comment
There was a problem hiding this comment.
We should try to adapt the specs to the PoC namings and invariants
Joxess
left a comment
There was a problem hiding this comment.
These specs are quite good. You might want to review what we have in the PoC to maintain consistency between function types, variable declarations, naming, etc to ensure alignment
|
|
||
| | Name | Address | Introduced | Deprecated | Proxied | | ||
| | ----------- | ------------------------------------------ | ---------- | ---------- | ------- | | ||
| | FeeSplitter | 0x420000000000000000000000000000000000001C | Jovian | No | Yes | |
There was a problem hiding this comment.
Check with the PoC on what the correct address is
| - MUST revert if any vault has a recipient different from this contract. | ||
| - MUST revert if any vault has a withdrawal network different from `WithdrawalNetwork.L2`. | ||
| - MUST withdraw the vault's fees balance if the vault's balance is equal to or greater than the minimum withdrawal amount set. | ||
| - MUST set the `lastDisbursementTime` to the current block timestamp. |
There was a problem hiding this comment.
Maybe it is helpful to mention that this is updated only when disburseFees collects ETH
| ## Security Considerations | ||
|
|
||
| - Given that vault recipients can now be updated, it's important to ensure that this can only be done by the appropriate address, namely `ProxyAdmin.owner()`. | ||
| - Upgrading the vaults and making them compatible with the `FeeSplitter` incurs a process where you have to deploy the new implementations and properly configure the vaults, which introduces complexity and potential for errors. It is important to develop a solution, such as a contract to manage the entire upgrade process, simplifying the UX and reducing the risk of errors. |
There was a problem hiding this comment.
| - Upgrading the vaults and making them compatible with the `FeeSplitter` incurs a process where you have to deploy the new implementations and properly configure the vaults, which introduces complexity and potential for errors. It is important to develop a solution, such as a contract to manage the entire upgrade process, simplifying the UX and reducing the risk of errors. | |
| - Upgrading the vaults and making them compatible with the `FeeSplitter` incurs a process that requires deploying the new implementations and properly configuring the vaults, which introduces complexity and potential for errors. It is important to develop a solution, such as a contract to manage the entire upgrade process, simplifying the UX and reducing the risk of errors. |
|
Do you mind updating this with a mermaid diagram of the control flow so that its easier to visualize when explaining to people? |
| On deployment, the constructor will: | ||
|
|
||
| - Read and grab the current values for `recipient()`, `withdrawalNetwork()`, and `minWithdrawalAmount()` from each live vault (`SequencerFeeVault`, `L1FeeVault`, `BaseFeeVault`, `OperatorFeeVault`). Use a `try/catch` block to handle the case where the call fails because the vault is a legacy one, and query for the `RECIPIENT()` and `MIN_WITHDRAWAL_AMOUNT()` immutable values instead. | ||
| - Deploy a new implementation for each vault, passing the grabbed values per vault as constructor immutables, and also passing `L2` as the `WITHDRAWAL_NETWORK` constructor immutable. |
There was a problem hiding this comment.
Shouldn't we keep whatever this value is on the current fee vault? So if it says L1 deploy the new implementation with L1 as withdrawal network.
|
|
||
| - Read and grab the current values for `recipient()`, `withdrawalNetwork()`, and `minWithdrawalAmount()` from each live vault (`SequencerFeeVault`, `L1FeeVault`, `BaseFeeVault`, `OperatorFeeVault`). Use a `try/catch` block to handle the case where the call fails because the vault is a legacy one, and query for the `RECIPIENT()` and `MIN_WITHDRAWAL_AMOUNT()` immutable values instead. | ||
| - Deploy a new implementation for each vault, passing the grabbed values per vault as constructor immutables, and also passing `L2` as the `WITHDRAWAL_NETWORK` constructor immutable. | ||
| - Emit a single event containing the four deployed implementation addresses |
There was a problem hiding this comment.
Should we emit the old values or old implementation address for any reason?
|
|
||
| - All fee vaults MUST be correctly configured (`WithdrawalNetwork = L2` and `RECIPIENT = FeeSplitter`) or `disburseFees` MUST revert. | ||
| - `SharesCalculator` output MUST be valid: `sum(amounts) == totalCollected`, each `recipient != address(0)`, and each `withdrawalNetwork` is supported; otherwise `disburseFees` MUST revert. | ||
| - Disbursement MUST be atomic: if any individual payout fails (L2 transfer or L1 withdrawal), `disburseFees` MUST revert the entire transaction. |
There was a problem hiding this comment.
The splitter only does L2 transfers, L1 withdrawals are handled by the L1Withdrawer
| - All fee vaults MUST be correctly configured (`WithdrawalNetwork = L2` and `RECIPIENT = FeeSplitter`) or `disburseFees` MUST revert. | ||
| - `SharesCalculator` output MUST be valid: `sum(amounts) == totalCollected`, each `recipient != address(0)`, and each `withdrawalNetwork` is supported; otherwise `disburseFees` MUST revert. | ||
| - Disbursement MUST be atomic: if any individual payout fails (L2 transfer or L1 withdrawal), `disburseFees` MUST revert the entire transaction. | ||
| - If no funds are collected for the call, `disburseFees` MUST return early and MUST NOT consume the disbursement interval. |
There was a problem hiding this comment.
| - If no funds are collected for the call, `disburseFees` MUST return early and MUST NOT consume the disbursement interval. | |
| - If no funds are collected for the call, `disburseFees` MUST revert and MUST NOT consume the disbursement interval. |
Also I would remove the "must not consume disbursement interval" given that revering means that any state change is reverted but up to u
| ### Invariants | ||
|
|
||
| - All fee vaults MUST be correctly configured (`WithdrawalNetwork = L2` and `RECIPIENT = FeeSplitter`) or `disburseFees` MUST revert. | ||
| - `SharesCalculator` output MUST be valid: `sum(amounts) == totalCollected`, each `recipient != address(0)`, and each `withdrawalNetwork` is supported; otherwise `disburseFees` MUST revert. |
There was a problem hiding this comment.
Share info only consists of recipient and amount, withdrawal network does no longer exist in the ShareInfo struct
| - `SharesCalculator` output MUST be valid: `sum(amounts) == totalCollected`, each `recipient != address(0)`, and each `withdrawalNetwork` is supported; otherwise `disburseFees` MUST revert. | ||
| - Disbursement MUST be atomic: if any individual payout fails (L2 transfer or L1 withdrawal), `disburseFees` MUST revert the entire transaction. | ||
| - If no funds are collected for the call, `disburseFees` MUST return early and MUST NOT consume the disbursement interval. | ||
| - Cross‑function reentrancy MUST be prevented: `disburseFees` is non‑reentrant and `receive` MUST revert while a disbursement is in progress. |
There was a problem hiding this comment.
disburseFees is reentrant, and receive MUST NOT revert while a disbursement is in progress. What we actually do regarding receive + reentrancy:
receive reverts if:
- It's not in a fees disbursement context, meaning that
receive()is only opened when disbursing fees, so when callingFeeVault.withdraw()it doesn't revert - Caller is not one of the four fee vaults
Co-authored-by: Disco <131301107+0xDiscotech@users.noreply.github.com> Signed-off-by: IamFlux <175354924+0xiamflux@users.noreply.github.com>
Adds predeploy spec for Jovian HF.
Closes OPT-973