diff --git a/specs/protocol/jovian/fee-splitter.md b/specs/protocol/jovian/fee-splitter.md deleted file mode 100644 index a804a56ed..000000000 --- a/specs/protocol/jovian/fee-splitter.md +++ /dev/null @@ -1,140 +0,0 @@ -### FeeSplitter + Pluggable Revenue Share: Design Doc - -| | | -| ------------------ | -------------------- | -| Author | _0xDiscotech_ | -| Created at | _2025-08-21_ | -| Initial Reviewers | _Tynes, Agus, Joxes_ | -| Need Approval From | _Tynes, Agus, Joxes_ | -| Status | _Draft_ | - -## Purpose - -Make fee revenue sharing dynamic and per‑chain extensible. Allow any chain to plug in their own revenue share logic without forking the `FeeSplitter` but integrating with it. - -## Summary - -`FeeSplitter` aggregates fees from all the `FeeVault`s on L2, enforces the configured checks, and then disburses funds to recipients based on shares computed by a pluggable `SharesCalculator`. The calculator returns an array of disbursements—each with a `recipient`, `withdrawalNetwork`, `amount`, and optional `metadata`—and `FeeSplitter` iterates through this list to execute each payout as an L2 transfer or an L1 withdrawal. We ship with a `StandardSuperchainRevenueShare` calculator by default, and chains can swap in their own module as needed. Each disbursement emits a single aggregate event containing arrays of `recipients`, `networks`, and `amounts`. - -## Problem Statement + Context - -Fixed fee shares are too rigid: chains want custom splits, L1/L2 destinations, and richer policies. We need: - -- A safe, permissioned way to plug in chain‑specific logic. -- A stable, minimal surface in `FeeSplitter` to keep operations simple. -- Backwards‑compatible behavior by default (standard superchain revshare). - -## Proposed Solution - -High‑level flow: - -1. `FeeSplitter.disburseFees()` checks interval and vault config, pulls eligible funds from vaults, and computes both `gross` and `net` amounts (and per‑vault totals for future granularity). -2. `FeeSplitter` calls the chain‑configured `SharesCalculator` with the inputs to compute disbursements. -3. `FeeSplitter` validates outputs (basic invariants) and then pays each item: - - L2: native transfer - - L1: use `L2ToL1MessagePasser` to withdraw -4. Emit one aggregate event (arrays). Update internal accounting. - -Interfaces and ownership: - -- `SharesCalculator` naming: use `IRevenueShareCalculator` (short: `RevenueShareCalculator`). Method: `computeDisbursements(gross, net[, perVault])`. -- Admin (`ProxyAdmin.owner()`) can set the calculator via `setShareCalculator(address)` and update the disbursement interval. -- The default calculator is `StandardSuperchainRevenueShare` (our previous 2‑recipient, gross/net max behavior). - -Inputs to the calculator: - -- `gross` (sum of all vault withdrawals this call). -- `net` (sum of Sequencer/Base this call). -- Optionally per‑vault amounts (to enable more complex policies). We’ll pass them; calculators can ignore. - -Outputs from the calculator: - -- `Disbursement[]` where each item is `{recipient, withdrawalNetwork, amount}`. We also provision a `metadata` bytes field slot; see below. - -Metadata and recipients: - -- We’ll support an optional `metadata` bytes blob per disbursement. As agreed, we’ll `TSTORE` the metadata on `FeeSplitter` right before the corresponding send, so recipients that rely on it can read it in their payable fallback/receive code path. We keep `SafeCall.send()` to stay compatible with EOAs and multisigs (no new interface requirement). - -Invariants and validation: - -- Sum of `amounts` MUST equal total collected (rounding policy below). -- `recipients` MUST be non‑zero. -- If any payout fails (recipient reverts or bridge call fails), revert the whole transaction. -- If the calculator returns zero total, short‑circuit (no interval consumed). -- Misconfigured vault (wrong network or recipient) reverts (consistent with current behavior). - -Rounding: - -- Any last‑wei rounding delta goes to the last `recipient` returned by the calculator. - -Events: - -- One aggregate event on each disbursement with arrays: - - `recipients[]`, `networks[]`, `amounts[]`, `total`, `calculator` address. - -Extensibility: - -- Start with `StandardSuperchainRevenueShare` to preserve current semantics. -- RAAS providers can deploy their own calculators and the chain can point to them via `setShareCalculator`. - -### 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. -- 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. - -### Resource Usage - -- `disburseFees` does: - - Up to four vault withdrawals (each with eligibility checks). - - One external call to the calculator (view/pure). - - Iterates through the returned `disbursements` and performs transfers/bridge calls. - - Emits one aggregate event. -- Compared to “vault → L1 direct,” this adds an extra step at disbursement time (slightly higher gas). Acceptable on L2. -- Admin setters (`setShareCalculator`, `setFeeDisbursementInterval`) are trivial (single storage write + event). - -### Single Point of Failure and Multi Client Considerations - -- No client (op‑geth/op‑reth) changes needed. -- Opt‑in and per‑chain selectable calculator. The `FeeSplitter` surface remains stable. - -## Failure Mode Analysis - -- DoS on a very big recipients array returned by the calculator, leading to a large number of disbursements and OOG -- DoS due to very high gas consumption by a recipient ending in OOG -- Misconfigured recipients that don't allow receiving the fees from the splitter, leading to a failed disbursement. -- Buggy SharesCalculator that returns incorrect or malformed outputs, leading to a failed disbursement. -- - -## Impact on Developer Experience - -- Chains get a clean plug‑in point to customize revshare without forking `FeeSplitter`. -- Default behavior matches the current two‑recipient superchain revshare until a chain swaps calculators. -- Recipients don’t need to implement a new interface; we continue using `SafeCall.send()`. Advanced recipients can optionally read metadata via `TSTORE` if they want more context. - -## Alternatives Considered - -- Keep fixed two‑recipient split - - Simpler, but not flexible; each chain ends up forking or wrapping. -- Require a recipient interface and pass metadata via calldata - - Breaks EOAs/multisigs and adds constraints on recipients; not ideal for compatibility. -- Emit per‑recipient events instead of an aggregate event - - Noisy and more expensive; we prefer a single aggregate event with arrays. - -## Risks & Uncertainties - -- `TSTORE` availability/assumptions: we rely on transient storage being available on the target L2; if disabled, recipients can’t read metadata that way (fallback still works via plain ETH transfer). -- DoS via many recipients: we currently don’t cap outputs or enforce min payout amounts. Low risk (calculator is set by the permissioned chain op and can be updated quickly), but we’ll keep it in mind. If needed, we can add `MAX_PAYOUTS_PER_TX` and `MIN_PAYOUT_AMOUNT` as admin‑settable limits. -- Calculator bugs: if a calculator misbehaves (sum mismatch or downstream recipients revert), disbursements will revert. We’ll validate outputs and keep calculator swaps gated by `ProxyAdmin.owner()`. - -Open items we decided - -- `withdrawalNetwork` is part of the calculator output (per‑recipient L2/L1 destination). -- `metadata`: a bytes metadata field is supported; `FeeSplitter` `TSTORE`s it before each send so recipients that want it can read it. We keep `SafeCall.send()` to avoid breaking EOAs/multisigs. -- Inputs to calculator: we pass `gross`, `net`, and `perVault` totals (calculator can ignore what it doesn’t need). -- Failure policy: revert the whole disbursement if any payout fails. -- Rounding: last wei goes to the last `recipient`. -- Events: single aggregate event with arrays per disbursement. -- Gas forwarded: no special limit initially (set by chain op, can be updated quickly if needed). We can add a cap later if we see an issue. diff --git a/specs/protocol/jovian/fee-vault-initializer.md b/specs/protocol/jovian/fee-vault-initializer.md index 402831666..6bf36ca84 100644 --- a/specs/protocol/jovian/fee-vault-initializer.md +++ b/specs/protocol/jovian/fee-vault-initializer.md @@ -1,114 +1,57 @@ +# FeeVaultInitializer + -- [FeeVaultInitializer](#feevaultinitializer) - - [Constructor](#constructor) - - [Events](#events) - - [Upgrade Path](#upgrade-path) - - [Alternatives Considered](#alternatives-considered) - - [Pros & Cons Of Both Approaches](#pros--cons-of-both-approaches) - - [Getter-driven Approach (Current)](#getter-driven-approach-current) - - [Initializer-driven Approach](#initializer-driven-approach) - - [Considerations](#considerations) +- [Summary](#summary) +- [Functions](#functions) + - [`constructor`](#constructor) +- [Events](#events) + - [`FeeVaultDeployed`](#feevaultdeployed) -# FeeVaultInitializer - -This contract serves as a runbook for deploying new vault implementations while preserving the current vault configuration. It is intended to be used in a NUT series context to upgrade the vaults to the new implementation once they are deployed. +## Summary -### Constructor +A contract responsible for deploying new vault implementations while preserving the current vault configuration. +It is intended to be used in a Network Upgrade Transaction (NUT) series context to upgrade each vault to the +new implementation once deployed. -On deployment, the constructor will: +## Functions -- 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 +### `constructor` -Invariants: +When the initializer is deployed, it will: -- It MUST deploy implementations whose constructor immutables match the current configuration values. -- It MUST emit an event with the deployed implementation addresses. - -### Events - -Single event with all deployed implementation addresses: +- Read and retrieve the current values for `recipient()`, `withdrawalNetwork()`, and `minWithdrawalAmount()` + from each live vault (`SequencerFeeVault`, `L1FeeVault`, `BaseFeeVault`, `OperatorFeeVault`). +- Handle legacy vaults that may not have the `WITHDRAWAL_NETWORK` function by using a low-level staticcall and + assigning the default value of `WithdrawalNetwork.L2` when the configuration cannot be read. +- Deploy a new implementation for each vault, passing the retrieved values per vault as constructor + immutables, and passing `L2` as the `WITHDRAWAL_NETWORK` constructor immutable in case a legacy vault is + being upgraded. +- Emit the `FeeVaultDeployed` event for each newly deployed vault. ```solidity -event FeeVaultImplementationsDeployed( - address sequencerFeeVaultImplementation, - address l1FeeVaultImplementation, - address baseFeeVaultImplementation, - address operatorFeeVaultImplementation -); +constructor() ``` -## Upgrade Path - -These are the steps to deploy and activate new implementations through NUTs: +- MUST deploy implementations whose constructor immutables match the current configuration values. +- For legacy vaults, MUST assign a default `WithdrawalNetwork.L2` value to the network configuration on the new implementation. +- For each vault, MUST emit the `FeeVaultDeployed` event. -0. Precalculate: - a. `FeeVaultInitializer` address to be deployed - b. each vault implementation address to be deployed by the `FeeVaultInitializer` address -1. Deploy the `FeeVaultInitializer`. Its constructor deploys the new implementations, keeping the current configuration values. -2. For each vault proxy, call `upgradeTo` (or `upgradeToAndCall` if local policy requires) from the admin or via the `address(0)` NUT flow to point the proxy at the emitted implementation addresses (4 different NUTs). +## Events -Execute in order so that deployments exist before proxies are upgraded. +### `FeeVaultDeployed` -## Alternatives Considered - -**Initializer-driven storage init after upgrade**: Stores current configs as immutables on a `FeeVaultInitializer` and grant it permission to call `initialize()` on each vault after proxies are upgraded to the new implementation. - -- Flow: - 1. Deploy `FeeVaultInitializer`. Its constructor reads the current config from the live vaults and stores those values as immutables. - 2. Deploy a new implementation for each vault, passing the initializer’s address as the allowed initializer target (pre-calculated). - 3. From address(0) (NUT) or via admin, call `upgradeTo` for each vault proxy to point to the new implementation. - 4. Call `FeeVaultInitializer.initializeVaults()` so it pushes the stored config into each vault via `initialize()`. +Emitted when a new fee vault implementation has been deployed. ```solidity -address public immutable FEE_VAULT_INITIALIZER; -Config public config; - -constructor(address _feeVaultInitializer) { - FEE_VAULT_INITIALIZER = _feeVaultInitializer; -} - -function initialize(Config _config) external { - if (msg.sender != FEE_VAULT_INITIALIZER) revert NotAllowed(); - config = _config; -} +event FeeVaultDeployed( + string indexed vaultType, + address indexed newImplementation, + address recipient, + WithdrawalNetwork network, + uint256 minWithdrawalAmount +) ``` - -## Pros & Cons Of Both Approaches - -### Getter-driven Approach (Current) - -**Pros** - -- Simple and straightforward approach to deploy the new vault implementations while keeping the current configuration values. -- Fewer NUTs are needed on the series, making this process simpler. -- No need to give any permission to the `FeeVaultInitializer` contract to call `initialize()` on the vaults. -- Less code to maintain - -**Cons** - -- It requires a change to `FeeVaults` getter functions, which is not very clean, and also incurs a minor added gas cost while reading the values. - -### Initializer-driven Approach - -**Pros** - -- No need to change `FeeVaults` getter functions. -- No added gas cost while reading the values. - -**Cons** - -- More NUTs are needed on the series, making this process more complex. -- Requires permission to the `FeeVaultInitializer` contract to call `initialize()` on the vaults. -- More code to maintain: Vaults need to implement the `initialize()` function. - -With these pros and cons in mind, we opted for the getter-driven approach since we find it simpler, and no permissions nor `initialize()` function are needed to be implemented on the vaults. - -### Considerations - -For chains opting into `FeeSplitter`, ensure `WITHDRAWAL_NETWORK = L2` and `RECIPIENT = FeeSplitter` after upgrade (via admin setters). diff --git a/specs/protocol/jovian/fees-depositor.md b/specs/protocol/jovian/fees-depositor.md new file mode 100644 index 000000000..68da68cb5 --- /dev/null +++ b/specs/protocol/jovian/fees-depositor.md @@ -0,0 +1,117 @@ +# FeesDepositor + + + + +- [Summary](#summary) +- [Functions](#functions) + - [`receive`](#receive) + - [`setMinDepositAmount`](#setmindepositamount) + - [`setL2Recipient`](#setl2recipient) + - [`setGasLimit`](#setgaslimit) +- [Events](#events) + - [`FeesDeposited`](#feesdeposited) + - [`MinDepositAmountUpdated`](#mindepositamountupdated) + - [`L2RecipientUpdated`](#l2recipientupdated) + - [`FundsReceived`](#fundsreceived) + - [`GasLimitUpdated`](#gaslimitupdated) + + + +## Summary + +A periphery contract on L1 that acts as a recipient of fees sent by the `L1Withdrawer` contract on L2. Its purpose is to perform a deposit transaction to OP Mainnet with those fees via the `OptimismPortal` once it has received sufficient funds. + +It's a proxied contract with the owner of the `ProxyAdmin` as the address allowed to call the setter functions. + +## Functions + +### `receive` + +Initiates the deposit transaction process to OP Mainnet if and only if the contract holds funds equal to or above the `minDepositAmount` threshold. + +```solidity +receive() external payable +``` + +- MUST initiate a deposit transaction to the recipient on OP Mainnet if the `minDepositAmount` threshold is reached. +- MUST emit the `FundsReceived` event with the sender, amount received, and the current balance of the contract. +- MUST emit the `FeesDeposited` event only if the threshold is reached. + +### `setMinDepositAmount` + +Updates the minimum deposit amount the contract must hold before the deposit process can be initiated. + +```solidity +function setMinDepositAmount(uint256 _newMinDepositAmount) external +``` + +- MUST only be callable by `ProxyAdmin.owner()`. +- MUST emit the `MinDepositAmountUpdated` event. +- MUST update the `minDepositAmount` storage variable. + +### `setL2Recipient` + +Updates the address that will receive the funds on OP Mainnet during the deposit process. + +```solidity +function setL2Recipient(address _newRecipient) external +``` + +- MUST only be callable by `ProxyAdmin.owner()`. +- MUST emit the `RecipientUpdated` event. +- MUST update the `l2Recipient` storage variable. + +### `setGasLimit` + +Updates the gas limit used for the deposit transaction during the fees deposit process. + +```solidity +function setGasLimit(uint64 _gasLimit) external +``` + +- MUST only be callable by `ProxyAdmin.owner()`. +- MUST emit the `GasLimitUpdated` event. +- MUST update the `gasLimit` storage variable. + +## Events + +### `FeesDeposited` + +Emitted when a deposit to OP Mainnet is initiated. + +```solidity +event FeesDeposited(address indexed recipient, uint256 amount) +``` + +### `MinDepositAmountUpdated` + +Emitted when the minimum deposit amount before the deposit process can be initiated is updated. + +```solidity +event MinDepositAmountUpdated(uint256 oldMinDepositAmount, uint256 newMinDepositAmount) +``` + +### `L2RecipientUpdated` + +Emitted when the recipient of the funds on OP Mainnet is updated. + +```solidity +event L2RecipientUpdated(address oldL2Recipient, address newL2Recipient) +``` + +### `FundsReceived` + +Emitted whenever funds are received. + +```solidity +event FundsReceived(address indexed sender, uint256 amount, uint256 newBalance) +``` + +### `GasLimitUpdated` + +Emitted when the gas limit for the deposit transaction is updated. + +```solidity +event GasLimitUpdated(uint64 oldGasLimit, uint64 newGasLimit) +``` diff --git a/specs/protocol/jovian/l1-withdrawer.md b/specs/protocol/jovian/l1-withdrawer.md new file mode 100644 index 000000000..6a04a70f7 --- /dev/null +++ b/specs/protocol/jovian/l1-withdrawer.md @@ -0,0 +1,119 @@ +# L1Withdrawer + + + + +- [Summary](#summary) +- [Functions](#functions) + - [`receive`](#receive) + - [`setMinWithdrawalAmount`](#setminwithdrawalamount) + - [`setRecipient`](#setrecipient) + - [`setWithdrawalGasLimit`](#setwithdrawalgaslimit) +- [Events](#events) + - [`WithdrawalInitiated`](#withdrawalinitiated) + - [`MinWithdrawalAmountUpdated`](#minwithdrawalamountupdated) + - [`RecipientUpdated`](#recipientupdated) + - [`WithdrawalGasLimitUpdated`](#withdrawalgaslimitupdated) + - [`FundsReceived`](#fundsreceived) + + + +## Summary + +An optional periphery contract designed to be used as a recipient for a portion of the shares sent +by the `FeeSplitter`. Its sole purpose is to initiate a withdrawal to L1 via `L2ToL1MessagePasser.initiateWithdrawal` +once it has received enough funds. + +## Functions + +### `receive` + +Initiates the withdrawal process to L1 if and only if the contract holds funds equal to or above the +`minWithdrawalAmount` threshold. + +```solidity +receive() external payable +``` + +- MUST initiate a withdrawal to the set recipient if and only if the `minWithdrawalAmount` threshold is reached, + passing the `withdrawalGasLimit` variable to `initiateWithdrawal`. +- MUST emit the `FundsReceived` event with the sender, amount received and balance. +- MUST emit the `WithdrawalInitiated` event only if the threshold is reached. + +### `setMinWithdrawalAmount` + +Updates the minimum withdrawal amount the contract must hold before the withdrawal process can be initiated. + +```solidity +function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external +``` + +- MUST only be callable by `ProxyAdmin.owner()`. +- MUST emit the `MinWithdrawalAmountUpdated` event. +- MUST update the `minWithdrawalAmount` storage variable. + +### `setRecipient` + +Updates the address that will receive the funds on L1 during the withdrawal process. + +```solidity +function setRecipient(address _newRecipient) external +``` + +- MUST only be callable by `ProxyAdmin.owner()`. +- MUST emit the `RecipientUpdated` event. +- MUST update the `recipient` storage variable. + +### `setWithdrawalGasLimit` + +Updates the gas limit for the withdrawal on L1. + +```solidity +function setWithdrawalGasLimit(uint256 _newWithdrawalGasLimit) external +``` + +- MUST only be callable by `ProxyAdmin.owner()`. +- MUST emit the `WithdrawalGasLimitUpdated` event. +- MUST update the `withdrawalGasLimit` storage variable. + +## Events + +### `WithdrawalInitiated` + +Emitted when a withdrawal to L1 is initiated. + +```solidity +event WithdrawalInitiated(address indexed recipient, uint256 amount) +``` + +### `MinWithdrawalAmountUpdated` + +Emitted when the minimum withdrawal amount before the withdrawal can be initiated is updated. + +```solidity +event MinWithdrawalAmountUpdated(uint256 oldMinWithdrawalAmount, uint256 newMinWithdrawalAmount) +``` + +### `RecipientUpdated` + +Emitted when the recipient of the funds on L1 is updated. + +```solidity +event RecipientUpdated(address oldRecipient, address newRecipient) +``` + +### `WithdrawalGasLimitUpdated` + +Emitted when the withdrawal gas limit on L1 is updated. + +```solidity +event WithdrawalGasLimitUpdated(uint256 oldWithdrawalGasLimit, uint256 newWithdrawalGasLimit) +``` + +### `FundsReceived` + +Emitted whenever funds are received. + +```solidity +event FundsReceived(address indexed sender, uint256 amount, uint256 newBalance) +``` diff --git a/specs/protocol/jovian/predeploys.md b/specs/protocol/jovian/predeploys.md index 0c1f9f9da..cd370a0bc 100644 --- a/specs/protocol/jovian/predeploys.md +++ b/specs/protocol/jovian/predeploys.md @@ -20,24 +20,18 @@ - [Invariants](#invariants) - [Fee Vaults (SequencerFeeVault, L1FeeVault, BaseFeeVault, OperatorFeeVault)](#fee-vaults-sequencerfeevault-l1feevault-basefeevault-operatorfeevault) - [FeeSplitter](#feesplitter) - - [Constants](#constants) - [Functions](#functions-1) - [`initialize`](#initialize) - [`disburseFees`](#disbursefees) - [`receive`](#receive) - - [`setRevenueShareRecipient`](#setrevenuesharerecipient) - - [`setRevenueRemainderRecipient`](#setrevenueremainderrecipient) + - [`setSharesCalculator`](#setsharescalculator) - [`setFeeDisbursementInterval`](#setfeedisbursementinterval) - [Events](#events-1) - [`FeesDisbursed`](#feesdisbursed) - - [`NoFeesCollected`](#nofeescollected) - [`FeesReceived`](#feesreceived) - - [`Initialized`](#initialized) - - [`RevenueShareRecipientUpdated`](#revenuesharerecipientupdated) - - [`RevenueRemainderRecipientUpdated`](#revenueremainderrecipientupdated) - [`FeeDisbursementIntervalUpdated`](#feedisbursementintervalupdated) + - [`SharesCalculatorUpdated`](#sharescalculatorupdated) - [Security Considerations](#security-considerations) -- [Open Questions](#open-questions) @@ -45,14 +39,14 @@ | Name | Address | Introduced | Deprecated | Proxied | | ----------- | ------------------------------------------ | ---------- | ---------- | ------- | -| FeeSplitter | 0x4200000000000000000000000000000000000029 | Jovian | No | Yes | +| FeeSplitter | 0x420000000000000000000000000000000000002B | Jovian | No | Yes | The `FeeSplitter` predeploy manages the distribution of all L2 fees. Fee vault contracts (`OperatorFeeVault`, `BaseFeeVault`, `L1FeeVault`, and `SequencerFeeVault`) update their configuration via setter functions for minimum withdrawal amounts, withdrawal networks, and recipients without requiring new deployments. Using the `FeeSplitter` requires vaults to use `WithdrawalNetwork.L2` and set the `FeeSplitter` as their -fee recipient. Chains MAY opt in at any time. +fee recipient. Chain operators may opt-in at any time or they can continue using other solutions. ### Disburse Fees Flow @@ -65,10 +59,8 @@ sequenceDiagram participant SequencerFeeVault participant FeeSplitter actor Caller - participant RevenueShareRecipient - participant RevenueRemainderRecipient - - + participant ISharesCalculator + participant Recipient Caller ->> FeeSplitter: 1) disburseFees() @@ -88,13 +80,21 @@ sequenceDiagram Note over FeeSplitter: If any fees were collected, calculate the share and
remaining amounts to transfer based on the rates. - FeeSplitter ->> RevenueShareRecipient: 6) send(share) - FeeSplitter ->> RevenueRemainderRecipient: 7) send(total - share) + FeeSplitter ->> ISharesCalculator: 6) getRecipientsAndAmounts(per-vault revenue) + ISharesCalculator -->> FeeSplitter: ShareInfo[] + + loop For each ShareInfo + FeeSplitter ->> Recipient: 7) send(shareInfo.amount) + end ``` ## FeeVault -Legacy immutables are preserved for network-specific config, and storage-based overrides are enabled via getters. Each getter returns the storage value if set; otherwise, it falls back to the immutable. Setters write the storage value to opt-in to overrides. There will be a flag to track whether the storage variable was set or not. +Legacy immutables are preserved for network-specific config, and storage-based overrides are +enabled via getters. Each getter returns the storage value if set; otherwise, it falls back +to the immutable. Setters write to storage to opt into overrides. A flag tracks whether the +storage variable was set. This allows the [`FeeVaultInitializer`](./fee-vault-initializer.md) to set legacy (immutable) values when deploying the new fee vault implementation and enables the `ProxyAdmin.owner` +to override what is returned by the getters once the new configuration has been set. ### Functions @@ -108,6 +108,7 @@ function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external - MUST only be callable by `ProxyAdmin.owner()` - MUST emit the `MinWithdrawalAmountUpdated` event +- MUST update the `_minWithdrawalAmount` storage variable #### `setRecipient` @@ -119,6 +120,7 @@ function setRecipient(address _newRecipient) external - MUST only be callable by `ProxyAdmin.owner()` - MUST emit the `RecipientUpdated` event +- MUST update the `_recipient` storage variable #### `setWithdrawalNetwork` @@ -132,10 +134,12 @@ function setWithdrawalNetwork(WithdrawalNetwork _newWithdrawalNetwork) external - MUST only be callable by `ProxyAdmin.owner()` - MUST emit the `WithdrawalNetworkUpdated` event +- MUST update the `_withdrawalNetwork` storage variable #### `recipient` -Returns the current recipient address, preferring the storage override if set; otherwise falls back to the legacy immutable value. +Returns the current recipient address, preferring the storage override if set; otherwise falls back to the +legacy immutable value. ```solidity function recipient() external view returns (address) @@ -147,7 +151,8 @@ function recipient() external view returns (address) #### `minWithdrawalAmount` -Returns the current minimum withdrawal amount, preferring the storage override if set; otherwise falls back to the legacy immutable value. +Returns the current minimum withdrawal amount, preferring the storage override if set; otherwise falls back to +the legacy immutable value. ```solidity function minWithdrawalAmount() external view returns (uint256) @@ -159,7 +164,8 @@ function minWithdrawalAmount() external view returns (uint256) #### `withdrawalNetwork` -Returns the current withdrawal network, preferring the storage override if set; otherwise falls back to the legacy immutable value. +Returns the current withdrawal network, preferring the storage override if set; otherwise falls back to the +legacy immutable value. ```solidity function withdrawalNetwork() external view returns (WithdrawalNetwork) @@ -201,49 +207,42 @@ event WithdrawalNetworkUpdated(WithdrawalNetwork oldWithdrawalNetwork, Withdrawa - If using the `FeeSplitter`, the withdrawal network MUST be set to `WithdrawalNetwork.L2` and the recipient MUST be set to the `FeeSplitter` predeploy address. - The balance of the vault MUST be preserved between implementation upgrades. +- On successful `withdraw()` execution, it MUST withdraw the entire balance. ## Fee Vaults (SequencerFeeVault, L1FeeVault, BaseFeeVault, OperatorFeeVault) -These contracts will inherit the changes made to the `FeeVault` contract, meaning that they will have storage variables and setters instead of constants for the configuration values, and they will be initializable. +These contracts inherit the changes made to the `FeeVault` contract, meaning that they have storage +variables and setters instead of constants for the configuration values, and they are initializable. -Their configuration includes the withdrawal network and the recipient to which the fees will be sent: +Their configuration includes the withdrawal network and the recipient to which the fees are sent: - **WithdrawalNetwork.L1**: Funds are withdrawn to an L1 address (default behavior) - **WithdrawalNetwork.L2**: Funds are withdrawn to an L2 address -For those chains that choose to use the `FeeSplitter` predeploy, `WithdrawalNetwork.L2` as the withdrawal network, and the `FeeSplitter` as the recipient MUST be set using the setter functions. +For those chains that choose to use the `FeeSplitter` predeploy, `WithdrawalNetwork.L2` as the withdrawal +network, and the `FeeSplitter` as the recipient MUST be set using the setter functions. ## FeeSplitter -This contract splits the ETH it receives and sends the correct amounts to two designated addresses. It integrates with the fee vault system by configuring each Fee Vault to use `WithdrawalNetwork.L2` and setting this predeploy as the -recipient in EVERY fee vault. - -The contract manages two recipients: - -- Revenue share recipient -- Remainder recipient +This contract splits the funds it receives from the vaults using a configured `ISharesCalculator` compatible revenue shares calculator to determine which addresses should receive funds and in what amounts by querying `ISharesCalculator.getRecipientsAndAmounts`: -And it has two ways of dividing the revenue (considered as the total amount of ETH received by the contract since the last disbursement): +```solidity +function getRecipientsAndAmounts( + uint256 _sequencerFeeVaultBalance, + uint256 _baseFeeVaultBalance, + uint256 _operatorFeeVaultBalance, + uint256 _l1FeeVaultBalance) + external + view + returns (ShareInfo[] memory shareInfo); +``` -- `grossRevenue`: The whole balance received by the contract. -- `netRevenue`: Only the fees collected by the `SequencerFeeVault` and `BaseFeeVault`. +The `ShareInfo[]` array returned represents pairs: a `recipient` address to receive the funds and an `amount` of funds it should receive; a default [`SuperchainRevSharesCalculator`](./superchain-revshares-calc.md) implementation of this interface is provided. -Their percentages for each kind of revenue are managed separately. -The contract will send the maximum amount between calculating the `grossRevenueShare` and `netRevenueShare` with their respective percentages to the `revenueShareRecipient`, and the remaining amount will be sent to the `revenueRemainderRecipient`. +The `FeeSplitter` integrates with the fee vault system by configuring each Fee Vault to use `WithdrawalNetwork.L2` and setting this predeploy as the recipient in every fee vault. The `FeeSplitter` MUST be proxied and initializable only by the `ProxyAdmin.owner()`. -### Constants - -The gross and net revenue fee shares rates will be defined as 2.5% of the gross revenue and 15% of the net revenue. - -| Name | Value | -| ------------------------------- | ----- | -| `MIN_FEE_DISBURSEMENT_INTERVAL` | 1 day | -| `BASIS_POINT_SCALE` | 10000 | -| `NET_FEE_SHARE_BP` | 250 | -| `GROSS_FEE_SHARE_BP` | 1500 | - ### Functions #### `initialize` @@ -252,32 +251,25 @@ Initializes the contract with the initial recipients and disbursement interval. ```solidity function initialize( - address payable _revenueShareRecipient, - address payable _revenueRemainderRecipient, - uint40 _feeDisbursementInterval + ISharesCalculator _sharesCalculator, + uint128 _feeDisbursementInterval ) external ``` - MUST only be callable once. -- MUST only be callable by `ProxyAdmin.owner()`. -- MUST revert if `_revenueShareRecipient` is the zero address. -- MUST revert if `_revenueRemainderRecipient` is the zero address. -- MUST revert if `_feeDisbursementInterval` is less than `MIN_FEE_DISBURSEMENT_INTERVAL`. -- MUST set `revenueShareRecipient` to `_revenueShareRecipient`. -- MUST set `revenueRemainderRecipient` to `_revenueRemainderRecipient`. - MUST set `feeDisbursementInterval` to `_feeDisbursementInterval`. - MUST emit an `Initialized` event with the provided parameters. #### `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. -The function MUST revert if the withdrawal is not set to `WithdrawalNetwork.L2`, or if the recipient set is not the `FeeSplitter`. -The function MUST withdraw only if the vault balance is greater than or equal to its minimum withdrawal amount. +to the appropriate addresses according to the amounts returned by the set shares calculator. -When attempting to withdraw from the vaults, it will check that the withdrawal network is set to `WithdrawalNetwork.L2`, and that the recipient of the vault is the `FeeSplitter`. It MUST revert if any of these conditions are not met. +When attempting to withdraw from the vaults, it checks that the withdrawal network is set to `WithdrawalNetwork.L2`, +and that the recipient of the vault is the `FeeSplitter`. It MUST revert if any of these conditions is not met. It MUST only withdraw if the vault balance is greater than or equal to its minimum withdrawal amount. -In addition, it will follow a `nonReentrant` pattern, to avoid receiving balance back once the fees are being disbursed. +In addition, it follows a `nonReentrant` pattern using `TSORE`d flags, to avoid receiving balance back +once the fees are being disbursed. ```solidity function disburseFees() external @@ -286,54 +278,38 @@ function disburseFees() external - MUST revert if not enough time has passed since the last successful execution. - 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. -- If any fees were collected, MUST set the `lastDisbursementTime` to the current block timestamp. -- MUST reset the `netRevenueShare` state variable. -- MUST send the max between `grossRevenueShare` and `netRevenueShare` to the `revenueShareRecipient`. -- MUST send the `grossRevenue` minus the amount sent to the `revenueShareRecipient` to the `revenueRemainderRecipient`. -- MUST emit `NoFeesCollected` event if there are no funds available in the contract after the vaults have been withdrawn. +- MUST revert if total fees collected are 0. + withdrawal amount set. +- It MUST set the `lastDisbursementTime` to the current block timestamp. - MUST emit `FeesDisbursed` event if the funds were disbursed. -- The balance of the contract MUST be 0 after a successful execution. #### `receive` -Receives ETH from any sender, but only accounts for `netRevenueShare` if the sender is either the `SequencerFeeVault` or `BaseFeeVault`. +Receives funds from any of the `FeeVault`s if and only if the disbursing process is in progress, and reverts +otherwise. This is enforced using transient storage flags. ```solidity function receive() external payable ``` -- MUST revert if on a reentrant call after `disburseFees` has been called. -- MUST add the received amount to the `netRevenueShare` balance if the sender is either the `SequencerFeeVault` or `BaseFeeVault`. -- MUST accept ETH from any sender. +- MUST revert if the disbursing process is not in progress. +- MUST accept funds from the `BaseFeeVault`, `L1FeeVault`, `SequencerFeeVault` and `OperatorFeeVault` only. - MUST emit a `FeesReceived` event upon successful execution. -#### `setRevenueShareRecipient` +#### `setSharesCalculator` -Sets the address that should receive the configured share of fees. +Sets the address of the calculator used to partion the fees. ```solidity -function setRevenueShareRecipient(address _newRevenueShareRecipient) external +function setSharesCalculator(ISharesCalculator _newSharesCalculator) external ``` -- MUST revert if `_newRevenueShareRecipient` is the zero address. - MUST only be callable by `ProxyAdmin.owner()` -- MUST emit a `RevenueShareRecipientUpdated` event upon successful execution. +- MUST emit a `SharesCalculatorUpdated` event upon successful execution. +- MUST update the `sharesCalculator` storage variable. -#### `setRevenueRemainderRecipient` - -Sets the address that should receive the remaining fees. - -```solidity -function setRevenueRemainderRecipient(address _newRevenueRemainderRecipient) external -``` - -- MUST only be callable by `ProxyAdmin.owner()` -- MUST revert if `_newRevenueRemainderRecipient` is the zero address -- MUST emit a `RevenueRemainderRecipientUpdated` event upon successful execution. - #### `setFeeDisbursementInterval` Sets the minimum time, in seconds, that must pass between consecutive calls to `disburseFees`. @@ -342,9 +318,9 @@ Sets the minimum time, in seconds, that must pass between consecutive calls to ` function setFeeDisbursementInterval(uint40 _newInterval) external ``` -- MUST revert if `_newInterval` is less than `MIN_FEE_DISBURSEMENT_INTERVAL`. - MUST only be callable by `ProxyAdmin.owner()` - MUST emit a `FeeDisbursementIntervalUpdated` event upon successful execution. +- MUST update the `feeDisbursementInterval` storage variable. ### Events @@ -353,71 +329,39 @@ function setFeeDisbursementInterval(uint40 _newInterval) external Emitted when fees are successfully withdrawn from fee vaults and distributed to recipients. ```solidity -event FeesDisbursed( - address indexed revenueShareRecipient, - address indexed remainderRecipient, - uint256 revenueShareRecipientAmount, - uint256 revenueRemainderRecipientAmount - ); -``` - -#### `NoFeesCollected` - -Emitted when `disburseFees` is called and, after attempting eligible vault withdrawals, there are no funds available to withdraw or distribute. This can occur when all vaults lack withdrawable funds or when the contract holds no ETH from any other sender. - -```solidity -event NoFeesCollected() +event FeesDisbursed(ShareInfo[] shareInfo, uint256 grossRevenue) ``` #### `FeesReceived` -Emitted when the contract receives balance. +Emitted when the contract receives funds. ```solidity event FeesReceived(address indexed sender, uint256 amount) ``` -#### `Initialized` - -Emitted when the contract is initialized with its initial configuration. - -```solidity -event Initialized( - address revenueShareRecipient, - address revenueRemainderRecipient, - uint40 feeDisbursementInterval - ) -``` - -#### `RevenueShareRecipientUpdated` - -Emitted when the revenue share recipient is successfully updated. - -```solidity -event RevenueShareRecipientUpdated(address indexed oldRevenueShareRecipient, address indexed newRevenueShareRecipient) -``` - -#### `RevenueRemainderRecipientUpdated` +#### `FeeDisbursementIntervalUpdated` -Emitted when the revenue remainder recipient is successfully updated. +Emitted when the minimum time interval between consecutive fee disbursements is successfully updated. ```solidity -event RevenueRemainderRecipientUpdated(address indexed oldRevenueRemainderRecipient, address indexed newRevenueRemainderRecipient) +event FeeDisbursementIntervalUpdated(uint128 oldFeeDisbursementInterval, uint128 newFeeDisbursementInterval) ``` -#### `FeeDisbursementIntervalUpdated` +#### `SharesCalculatorUpdated` -Emitted when the minimum time interval between consecutive fee disbursements is successfully updated. +Emitted when the shares calculator is updated. ```solidity -event FeeDisbursementIntervalUpdated(uint256 oldFeeDisbursementInterval, uint256 newFeeDisbursementInterval) +event SharesCalculatorUpdated(address oldSharesCalculator, address newSharesCalculator) ``` ## 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 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. - -## Open Questions - -- Should we block zero-address recipients during initialization? +- 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 + that requires deploying. We provide a [`FeeVaultInitializer`](./fee-vault-initializer.md) that performs this upgrade. + 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. diff --git a/specs/protocol/jovian/superchain-revshares-calc.md b/specs/protocol/jovian/superchain-revshares-calc.md new file mode 100644 index 000000000..77334371c --- /dev/null +++ b/specs/protocol/jovian/superchain-revshares-calc.md @@ -0,0 +1,88 @@ +# SuperchainRevSharesCalculator + + + + +- [Summary](#summary) +- [Functions](#functions) + - [`getRecipientsAndAmounts`](#getrecipientsandamounts) + - [`setShareRecipient`](#setsharerecipient) + - [`setRemainderRecipient`](#setremainderrecipient) +- [Events](#events) + - [`ShareRecipientUpdated`](#sharerecipientupdated) + - [`RemainderRecipientUpdated`](#remainderrecipientupdated) + + + +## Summary + +A Superchain implementation is provided for the `ISharesCalculator` interface. It pays the greater amount +between 2.5% of gross revenue or 15% of net revenue (gross minus L1 fees) to the configured share recipient. +The second configured recipient receives the full remainder via `FeeSplitter`'s remainder send. + +It allows the `ProxyAdmin.owner` to configure the recipient address of the Superchain revenue share and the +recipient of the remainder. + +## Functions + +### `getRecipientsAndAmounts` + +Calculates the share for each of the two recipients based on the following formula: + +```solidity +GrossRevenue = Sum of all vault fee revenues +GrossShare = GrossRevenue × 2.5% +NetShare = (GrossRevenue - L1FeeRevenue) × 15% + +ShareRecipientAmount = max(GrossShare, NetShare) +RemainderRecipientAmount = GrossRevenue - ShareRecipientAmount +``` + +```solidity +function getRecipientsAndAmounts(uint256 _sequencerFeeRevenue, uint256 _baseFeeRevenue, uint256 _operatorFeeRevenue, uint256 _l1FeeRevenue) external view returns (ShareInfo[] memory shareInfo) +``` + +- MUST return the correct partition of shares for each of the `shareRecipient` and `remainderRecipient` addresses + based on the above formula. + +### `setShareRecipient` + +Sets the recipient of the shares calculated by the fee sharing formula. + +```solidity +function setShareRecipient(address payable _shareRecipient) external +``` + +- MUST only be callable by `ProxyAdmin.owner`. +- MUST emit `ShareRecipientUpdated`. +- MUST update the `shareRecipient` storage variable. + +### `setRemainderRecipient` + +Sets the recipient of the remainder of the fees once the shares for the share recipient have been calculated. + +```solidity +function setRemainderRecipient(address payable _remainderRecipient) external +``` + +- MUST only be callable by `ProxyAdmin.owner`. +- MUST emit `RemainderRecipientUpdated`. +- MUST update the `remainderRecipient` storage variable. + +## Events + +### `ShareRecipientUpdated` + +Emitted when the recipient for the calculated share of the fees is updated. + +```solidity +event ShareRecipientUpdated(address indexed oldShareRecipient, address indexed newShareRecipient); +``` + +### `RemainderRecipientUpdated` + +Emitted when the recipient for the remainder of the fees is updated. + +```solidity +event RemainderRecipientUpdated(address indexed oldRemainderRecipient, address indexed newRemainderRecipient); +```