diff --git a/.circleci/config.yml b/.circleci/config.yml index 9f9fbba750faf..e4e3bdcfd6958 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -562,7 +562,7 @@ jobs: contracts-bedrock-build: docker: - image: <> - resource_class: xlarge + resource_class: 2xlarge parameters: build_args: description: Forge build arguments @@ -862,7 +862,7 @@ jobs: circleci_ip_ranges: true docker: - image: <> - resource_class: xlarge + resource_class: 2xlarge parameters: test_list: description: List of test files to run diff --git a/.gitignore b/.gitignore index bf22c3755f50c..4031cb1d276f9 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,4 @@ __pycache__ crytic-export # ignore local asdf config -.tool-versions +.tool-versions \ No newline at end of file diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index bcd5616170a5c..bdfebd4f2d7ab 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -194,6 +194,7 @@ rules: paths: exclude: - packages/contracts-bedrock/src/legacy/L1ChugSplashProxy.sol + - packages/contracts-bedrock/test/invariants/FeeSplit.t.sol - id: sol-style-enforce-require-msg languages: [solidity] diff --git a/devnet-sdk/contracts/constants/constants.go b/devnet-sdk/contracts/constants/constants.go index ba494a1721d9d..f0d76bcde53ba 100644 --- a/devnet-sdk/contracts/constants/constants.go +++ b/devnet-sdk/contracts/constants/constants.go @@ -22,6 +22,7 @@ var ( ProxyAdmin types.Address = common.HexToAddress("0x4200000000000000000000000000000000000018") BaseFeeVault types.Address = common.HexToAddress("0x4200000000000000000000000000000000000019") L1FeeVault types.Address = common.HexToAddress("0x420000000000000000000000000000000000001a") + OperatorFeeVault types.Address = common.HexToAddress("0x420000000000000000000000000000000000001B") SchemaRegistry types.Address = common.HexToAddress("0x4200000000000000000000000000000000000020") EAS types.Address = common.HexToAddress("0x4200000000000000000000000000000000000021") CrossL2Inbox types.Address = common.HexToAddress("0x4200000000000000000000000000000000000022") @@ -29,6 +30,7 @@ var ( SuperchainETHBridge types.Address = common.HexToAddress("0x4200000000000000000000000000000000000024") ETHLiquidity types.Address = common.HexToAddress("0x4200000000000000000000000000000000000025") SuperchainTokenBridge types.Address = common.HexToAddress("0x4200000000000000000000000000000000000028") + FeeSplitter types.Address = common.HexToAddress("0x420000000000000000000000000000000000002B") GovernanceToken types.Address = common.HexToAddress("0x4200000000000000000000000000000000000042") Create2Deployer types.Address = common.HexToAddress("0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2") MultiCall3 types.Address = common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11") diff --git a/op-chain-ops/devkeys/devkeys.go b/op-chain-ops/devkeys/devkeys.go index fdb151e4e6fcf..9c7541126212c 100644 --- a/op-chain-ops/devkeys/devkeys.go +++ b/op-chain-ops/devkeys/devkeys.go @@ -158,6 +158,10 @@ const ( SequencerFeeVaultRecipientRole ChainOperatorRole = 9 // SystemConfigOwner is the key that can make SystemConfig changes. SystemConfigOwner ChainOperatorRole = 10 + // OperatorFeeVaultRecipientRole is the key that receives from the OperatorFeeVault predeploy + OperatorFeeVaultRecipientRole ChainOperatorRole = 11 + // ChainFeesRecipientRole is the key that receives the chain's share from the FeeSplitter + ChainFeesRecipientRole ChainOperatorRole = 12 ) func (role ChainOperatorRole) String() string { @@ -182,8 +186,12 @@ func (role ChainOperatorRole) String() string { return "l1-fee-vault-recipient" case SequencerFeeVaultRecipientRole: return "sequencer-fee-vault-recipient" + case OperatorFeeVaultRecipientRole: + return "operator-fee-vault-recipient" case SystemConfigOwner: return "system-config-owner" + case ChainFeesRecipientRole: + return "chain-fees-recipient" default: return fmt.Sprintf("unknown-operator-%d", uint64(role)) } diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index 99b07787624fd..514647c7a19ed 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -86,6 +86,22 @@ type DevDeployConfig struct { FundDevAccounts bool `json:"fundDevAccounts"` } +type RevenueShareDeployConfig struct { + UseRevenueShare bool `json:"useRevenueShare"` + ChainFeesRecipient common.Address `json:"chainFeesRecipient"` +} + +var _ ConfigChecker = (*RevenueShareDeployConfig)(nil) + +func (d *RevenueShareDeployConfig) Check(log log.Logger) error { + if d.UseRevenueShare { + if d.ChainFeesRecipient == (common.Address{}) { + return fmt.Errorf("%w: ChainFeesRecipient cannot be address(0)", ErrInvalidDeployConfig) + } + } + return nil +} + type L2GenesisBlockDeployConfig struct { L2GenesisBlockNonce hexutil.Uint64 `json:"l2GenesisBlockNonce"` L2GenesisBlockGasLimit hexutil.Uint64 `json:"l2GenesisBlockGasLimit"` @@ -150,18 +166,24 @@ type L2VaultsDeployConfig struct { // SequencerFeeVaultRecipient represents the recipient of fees accumulated in the SequencerFeeVault. // Can be an account on L1 or L2, depending on the SequencerFeeVaultWithdrawalNetwork value. SequencerFeeVaultRecipient common.Address `json:"sequencerFeeVaultRecipient"` + // OperatorFeeVaultRecipient represents the recipient of fees accumulated in the OperatorFeeVault. + OperatorFeeVaultRecipient common.Address `json:"operatorFeeVaultRecipient"` // BaseFeeVaultMinimumWithdrawalAmount represents the minimum withdrawal amount for the BaseFeeVault. BaseFeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"baseFeeVaultMinimumWithdrawalAmount"` // L1FeeVaultMinimumWithdrawalAmount represents the minimum withdrawal amount for the L1FeeVault. L1FeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"l1FeeVaultMinimumWithdrawalAmount"` // SequencerFeeVaultMinimumWithdrawalAmount represents the minimum withdrawal amount for the SequencerFeeVault. SequencerFeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"sequencerFeeVaultMinimumWithdrawalAmount"` + // OperatorFeeVaultMinimumWithdrawalAmount represents the minimum withdrawal amount for the OperatorFeeVault. + OperatorFeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"operatorFeeVaultMinimumWithdrawalAmount"` // BaseFeeVaultWithdrawalNetwork represents the withdrawal network for the BaseFeeVault. BaseFeeVaultWithdrawalNetwork WithdrawalNetwork `json:"baseFeeVaultWithdrawalNetwork"` // L1FeeVaultWithdrawalNetwork represents the withdrawal network for the L1FeeVault. L1FeeVaultWithdrawalNetwork WithdrawalNetwork `json:"l1FeeVaultWithdrawalNetwork"` // SequencerFeeVaultWithdrawalNetwork represents the withdrawal network for the SequencerFeeVault. SequencerFeeVaultWithdrawalNetwork WithdrawalNetwork `json:"sequencerFeeVaultWithdrawalNetwork"` + // OperatorFeeVaultWithdrawalNetwork represents the withdrawal network for the OperatorFeeVault. + OperatorFeeVaultWithdrawalNetwork WithdrawalNetwork `json:"operatorFeeVaultWithdrawalNetwork"` } var _ ConfigChecker = (*L2VaultsDeployConfig)(nil) @@ -176,6 +198,9 @@ func (d *L2VaultsDeployConfig) Check(log log.Logger) error { if d.SequencerFeeVaultRecipient == (common.Address{}) { return fmt.Errorf("%w: SequencerFeeVaultRecipient cannot be address(0)", ErrInvalidDeployConfig) } + if d.OperatorFeeVaultRecipient == (common.Address{}) { + return fmt.Errorf("%w: OperatorFeeVaultRecipient cannot be address(0)", ErrInvalidDeployConfig) + } if !d.BaseFeeVaultWithdrawalNetwork.Valid() { return fmt.Errorf("%w: BaseFeeVaultWithdrawalNetwork can only be 0 (L1) or 1 (L2)", ErrInvalidDeployConfig) } @@ -185,6 +210,9 @@ func (d *L2VaultsDeployConfig) Check(log log.Logger) error { if !d.SequencerFeeVaultWithdrawalNetwork.Valid() { return fmt.Errorf("%w: SequencerFeeVaultWithdrawalNetwork can only be 0 (L1) or 1 (L2)", ErrInvalidDeployConfig) } + if !d.OperatorFeeVaultWithdrawalNetwork.Valid() { + return fmt.Errorf("%w: OperatorFeeVaultWithdrawalNetwork can only be 0 (L1) or 1 (L2)", ErrInvalidDeployConfig) + } return nil } @@ -743,6 +771,7 @@ type L2InitializationConfig struct { L2CoreDeployConfig FeeMarketConfig AltDADeployConfig + RevenueShareDeployConfig } func (d *L2InitializationConfig) Check(log log.Logger) error { diff --git a/op-chain-ops/genesis/testdata/test-deploy-config-full.json b/op-chain-ops/genesis/testdata/test-deploy-config-full.json index ae2bd1dcaa42c..e1dcf41bc1844 100644 --- a/op-chain-ops/genesis/testdata/test-deploy-config-full.json +++ b/op-chain-ops/genesis/testdata/test-deploy-config-full.json @@ -40,12 +40,15 @@ "baseFeeVaultRecipient": "0x42000000000000000000000000000000000000f5", "l1FeeVaultRecipient": "0x42000000000000000000000000000000000000f6", "sequencerFeeVaultRecipient": "0x42000000000000000000000000000000000000f7", + "operatorFeeVaultRecipient": "0x4200000000000000000000000000000000000063", "baseFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", + "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "baseFeeVaultWithdrawalNetwork": 0, "l1FeeVaultWithdrawalNetwork": 1, "sequencerFeeVaultWithdrawalNetwork": 1, + "operatorFeeVaultWithdrawalNetwork": 1, "l1StandardBridgeProxy": "0x42000000000000000000000000000000000000f8", "l1CrossDomainMessengerProxy": "0x42000000000000000000000000000000000000f9", "l1ERC721BridgeProxy": "0x4200000000000000000000000000000000000060", @@ -94,5 +97,7 @@ "daChallengeProxy": "0x0000000000000000000000000000000000000000", "daChallengeWindow": 0, "daResolveWindow": 0, - "daResolverRefundPercentage": 0 + "daResolverRefundPercentage": 0, + "useRevenueShare": true, + "chainFeesRecipient": "0x0000000000000000000000000000000000000444" } diff --git a/op-chain-ops/genesis/withdrawal_network_test.go b/op-chain-ops/genesis/withdrawal_network_test.go index 624cbc0efe72c..018b20117e017 100644 --- a/op-chain-ops/genesis/withdrawal_network_test.go +++ b/op-chain-ops/genesis/withdrawal_network_test.go @@ -83,18 +83,21 @@ func TestWithdrawalNetworkInlineJSON(t *testing.T) { BaseFeeVaultWithdrawalNetwork WithdrawalNetwork `json:"baseFeeVaultWithdrawalNetwork"` L1FeeVaultWithdrawalNetwork WithdrawalNetwork `json:"l1FeeVaultWithdrawalNetwork"` SequencerFeeVaultWithdrawalNetwork WithdrawalNetwork `json:"sequencerFeeVaultWithdrawalNetwork"` + OperatorFeeVaultWithdrawalNetwork WithdrawalNetwork `json:"operatorFeeVaultWithdrawalNetwork"` } jsonString := `{ "baseFeeVaultWithdrawalNetwork": "remote", "l1FeeVaultWithdrawalNetwork": "local", - "sequencerFeeVaultWithdrawalNetwork": "local" + "sequencerFeeVaultWithdrawalNetwork": "local", + "operatorFeeVaultWithdrawalNetwork": "local" }` intJsonString := `{ "baseFeeVaultWithdrawalNetwork": 0, "l1FeeVaultWithdrawalNetwork": 1, - "sequencerFeeVaultWithdrawalNetwork": 1 + "sequencerFeeVaultWithdrawalNetwork": 1, + "operatorFeeVaultWithdrawalNetwork": 1 }` t.Run("StringMarshaling", func(t *testing.T) { @@ -104,6 +107,7 @@ func TestWithdrawalNetworkInlineJSON(t *testing.T) { require.Equal(t, WithdrawalNetwork("remote"), decoded.BaseFeeVaultWithdrawalNetwork) require.Equal(t, WithdrawalNetwork("local"), decoded.L1FeeVaultWithdrawalNetwork) require.Equal(t, WithdrawalNetwork("local"), decoded.SequencerFeeVaultWithdrawalNetwork) + require.Equal(t, WithdrawalNetwork("local"), decoded.OperatorFeeVaultWithdrawalNetwork) encoded, err := json.Marshal(decoded) require.NoError(t, err) @@ -118,6 +122,7 @@ func TestWithdrawalNetworkInlineJSON(t *testing.T) { require.Equal(t, WithdrawalNetwork("remote"), decoded.BaseFeeVaultWithdrawalNetwork) require.Equal(t, WithdrawalNetwork("local"), decoded.L1FeeVaultWithdrawalNetwork) require.Equal(t, WithdrawalNetwork("local"), decoded.SequencerFeeVaultWithdrawalNetwork) + require.Equal(t, WithdrawalNetwork("local"), decoded.OperatorFeeVaultWithdrawalNetwork) encoded, err := json.Marshal(decoded) require.NoError(t, err) diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index d18d78a29c236..a5dd780617927 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/manage" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" "github.com/ethereum-optimism/optimism/op-service/eth" ) @@ -329,11 +330,17 @@ func GenesisL2(l2Host *script.Host, cfg *L2Config, deployment *L2Deployment, mul L1FeeVaultRecipient: cfg.L1FeeVaultRecipient, L1FeeVaultMinimumWithdrawalAmount: cfg.L1FeeVaultMinimumWithdrawalAmount.ToInt(), L1FeeVaultWithdrawalNetwork: big.NewInt(int64(cfg.L1FeeVaultWithdrawalNetwork.ToUint8())), + OperatorFeeVaultRecipient: cfg.OperatorFeeVaultRecipient, + OperatorFeeVaultMinimumWithdrawalAmount: cfg.OperatorFeeVaultMinimumWithdrawalAmount.ToInt(), + OperatorFeeVaultWithdrawalNetwork: big.NewInt(int64(cfg.OperatorFeeVaultWithdrawalNetwork.ToUint8())), GovernanceTokenOwner: cfg.GovernanceTokenOwner, Fork: big.NewInt(cfg.SolidityForkNumber(1)), DeployCrossL2Inbox: multichainDepSet, EnableGovernance: cfg.EnableGovernance, FundDevAccounts: cfg.FundDevAccounts, + UseRevenueShare: cfg.UseRevenueShare, + ChainFeesRecipient: cfg.ChainFeesRecipient, + L1FeesDepositor: standard.L1FeesDepositor, }); err != nil { return fmt.Errorf("failed L2 genesis: %w", err) } diff --git a/op-chain-ops/interopgen/recipe.go b/op-chain-ops/interopgen/recipe.go index 2d40c4451c94d..587b83cf46a4a 100644 --- a/op-chain-ops/interopgen/recipe.go +++ b/op-chain-ops/interopgen/recipe.go @@ -186,6 +186,10 @@ func (r *InteropDevL2Recipe) build(l1ChainID uint64, addrs devkeys.Addresses) (* if err != nil { return nil, err } + operatorFeeVaultRecipient, err := addrs.Address(chainOps(devkeys.OperatorFeeVaultRecipientRole)) + if err != nil { + return nil, err + } sequencerP2P, err := addrs.Address(chainOps(devkeys.SequencerP2PRole)) if err != nil { return nil, err @@ -228,12 +232,15 @@ func (r *InteropDevL2Recipe) build(l1ChainID uint64, addrs devkeys.Addresses) (* BaseFeeVaultRecipient: baseFeeVaultRecipient, L1FeeVaultRecipient: l1FeeVaultRecipient, SequencerFeeVaultRecipient: sequencerFeeVaultRecipient, + OperatorFeeVaultRecipient: operatorFeeVaultRecipient, BaseFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(Ether(10)), L1FeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(Ether(10)), SequencerFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(Ether(10)), + OperatorFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(Ether(10)), BaseFeeVaultWithdrawalNetwork: "remote", L1FeeVaultWithdrawalNetwork: "remote", SequencerFeeVaultWithdrawalNetwork: "remote", + OperatorFeeVaultWithdrawalNetwork: "remote", }, GovernanceDeployConfig: genesis.GovernanceDeployConfig{ EnableGovernance: false, @@ -282,6 +289,10 @@ func (r *InteropDevL2Recipe) build(l1ChainID uint64, addrs devkeys.Addresses) (* AltDADeployConfig: genesis.AltDADeployConfig{ UseAltDA: false, }, + RevenueShareDeployConfig: genesis.RevenueShareDeployConfig{ + UseRevenueShare: false, + ChainFeesRecipient: common.Address{}, + }, }, Prefund: make(map[common.Address]*big.Int), SaltMixer: "", diff --git a/op-core/predeploys/addresses.go b/op-core/predeploys/addresses.go index e461f0c86faaf..78bcc68c8cd8c 100644 --- a/op-core/predeploys/addresses.go +++ b/op-core/predeploys/addresses.go @@ -31,6 +31,7 @@ const ( SuperchainETHBridge = "0x4200000000000000000000000000000000000024" ETHLiquidity = "0x4200000000000000000000000000000000000025" SuperchainTokenBridge = "0x4200000000000000000000000000000000000028" + FeeSplitter = "0x420000000000000000000000000000000000002b" Create2Deployer = "0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2" MultiCall3 = "0xcA11bde05977b3631167028862bE2a173976CA11" Safe_v130 = "0x69f4D1788e39c87893C980c06EdF4b7f686e2938" @@ -72,6 +73,7 @@ var ( SuperchainETHBridgeAddr = common.HexToAddress(SuperchainETHBridge) ETHLiquidityAddr = common.HexToAddress(ETHLiquidity) SuperchainTokenBridgeAddr = common.HexToAddress(SuperchainTokenBridge) + FeeSplitterAddr = common.HexToAddress(FeeSplitter) Create2DeployerAddr = common.HexToAddress(Create2Deployer) MultiCall3Addr = common.HexToAddress(MultiCall3) Safe_v130Addr = common.HexToAddress(Safe_v130) @@ -106,6 +108,7 @@ func init() { Predeploys["SuperchainETHBridge"] = &Predeploy{Address: SuperchainETHBridgeAddr} Predeploys["ETHLiquidity"] = &Predeploy{Address: ETHLiquidityAddr} Predeploys["SuperchainTokenBridge"] = &Predeploy{Address: SuperchainTokenBridgeAddr} + Predeploys["FeeSplitter"] = &Predeploy{Address: FeeSplitterAddr} Predeploys["GovernanceToken"] = &Predeploy{ Address: GovernanceTokenAddr, ProxyDisabled: true, diff --git a/op-deployer/book/src/user-guide/init.md b/op-deployer/book/src/user-guide/init.md index a82f917652e0f..03842eb909810 100644 --- a/op-deployer/book/src/user-guide/init.md +++ b/op-deployer/book/src/user-guide/init.md @@ -44,9 +44,15 @@ l2ContractsLocator = "tag://op-contracts/v1.7.0-beta.1+l2-contracts" baseFeeVaultRecipient = "0x0000000000000000000000000000000000000000" l1FeeVaultRecipient = "0x0000000000000000000000000000000000000000" sequencerFeeVaultRecipient = "0x0000000000000000000000000000000000000000" + operatorFeeVaultRecipient = "0x0000000000000000000000000000000000000000" eip1559DenominatorCanyon = 250 eip1559Denominator = 50 eip1559Elasticity = 6 + + # Revenue Sharing Configuration + useRevenueShare = true + chainFeesRecipient = "0x0000000000000000000000000000000000000000" + [chains.roles] l1ProxyAdminOwner = "0x0000000000000000000000000000000000000000" l2ProxyAdminOwner = "0x0000000000000000000000000000000000000000" @@ -60,20 +66,38 @@ l2ContractsLocator = "tag://op-contracts/v1.7.0-beta.1+l2-contracts" Before you can use your intent file for a deployment, you will need to update all zero values to whatever is appropriate for your chain. For dev environments, it is ok to use all EOAs/hot-wallets. +## Revenue Sharing Configuration + +The `useRevenueShare` field controls whether your chain enables the revenue sharing system feature: + +- **`useRevenueShare = true`** (default for standard configurations): `FeeVault`s are upgraded and configured to use the `FeeSplitter` contract as the recipient, L2 as withdrawal network and `0` as the minimum withdrawal amount. The split logic is calculated using the `SuperchainRevSharesCalculator` contract. The `L1Withdrawer` contract is set to withdraw the OP portion of fees automatically. + +- **`useRevenueShare = false`**: `FeeSplitter` is deployed but initialized with zero address for the `sharesCalculator` field. No deployment is made for the `SuperchainRevSharesCalculator` and `L1Withdrawer` contracts. `FeeVault`s are upgraded but initialized using the custom configuration you provide. + +### Configuration Fields + +- `useRevenueShare` (optional): Enables or disables the revenue sharing system. Defaults to `true` for standard configurations, `false` for custom. +- `chainFeesRecipient` (required when `useRevenueShare = true`): Address that receives the chain operator's portion of fee revenue on L2. Must be able to receive ETH. + +> **Note**: Since `useRevenueShare` defaults to `true` for standard configurations, you must either provide a `chainFeesRecipient` address OR explicitly set `useRevenueShare = false` to opt out. The deployment will fail validation if revenue sharing is enabled without a recipient. + ## Production Setup + In production environments, you should use a more secure setup with cold-wallet multisigs (e.g. Gnosis Safes) for the following: -* `baseFeeVaultRecipient` -* `l1FeeVaultRecipient` -* `sequencerFeeVaultRecipient` -* `l1ProxyAdminOwner` -* `l2ProxyAdminOwner` -* `systemConfigOwner` + +- `baseFeeVaultRecipient` +- `l1FeeVaultRecipient` +- `sequencerFeeVaultRecipient` +- `operatorFeeVaultRecipient` +- `l1ProxyAdminOwner` +- `l2ProxyAdminOwner` +- `systemConfigOwner` HSMs (hardware security modules) are recommended for the following hot-wallets: -* `unsafeBlockSigner` -* `batcher` -* `proposer` -* `challenger` +- `unsafeBlockSigner` +- `batcher` +- `proposer` +- `challenger` [stages]: ../architecture/pipeline.md diff --git a/op-deployer/pkg/deployer/inspect/semvers.go b/op-deployer/pkg/deployer/inspect/semvers.go index fa3cd31fa8c37..3f9a17fa21a26 100644 --- a/op-deployer/pkg/deployer/inspect/semvers.go +++ b/op-deployer/pkg/deployer/inspect/semvers.go @@ -102,8 +102,10 @@ type L2PredeploySemvers struct { OptimismMintableERC721Factory string BaseFeeVault string L1FeeVault string + OperatorFeeVault string SchemaRegistry string EAS string + FeeSplitter string CrossL2Inbox string L2toL2CrossDomainMessenger string SuperchainETHBridge string @@ -153,8 +155,10 @@ func L2Semvers(cfg L2SemversConfig) (*L2PredeploySemvers, error) { {predeploys.OptimismMintableERC721FactoryAddr, &ps.OptimismMintableERC721Factory, "OptimismMintableERC721Factory"}, {predeploys.BaseFeeVaultAddr, &ps.BaseFeeVault, "BaseFeeVault"}, {predeploys.L1FeeVaultAddr, &ps.L1FeeVault, "L1FeeVault"}, + {predeploys.OperatorFeeVaultAddr, &ps.OperatorFeeVault, "OperatorFeeVault"}, {predeploys.SchemaRegistryAddr, &ps.SchemaRegistry, "SchemaRegistry"}, {predeploys.EASAddr, &ps.EAS, "EAS"}, + {predeploys.FeeSplitterAddr, &ps.FeeSplitter, "FeeSplitter"}, } for _, contract := range contracts { semver, err := ReadSemver(host, contract.Address) diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index dcb5e10056c26..3d56200db751e 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -315,6 +315,7 @@ func TestGlobalOverrides(t *testing.T) { expectedBaseFeeVaultRecipient := common.HexToAddress("0x0000000000000000000000000000000000000001") expectedL1FeeVaultRecipient := common.HexToAddress("0x0000000000000000000000000000000000000002") expectedSequencerFeeVaultRecipient := common.HexToAddress("0x0000000000000000000000000000000000000003") + expectedOperatorFeeVaultRecipient := common.HexToAddress("0x0000000000000000000000000000000000000004") expectedBaseFeeVaultMinimumWithdrawalAmount := strings.ToLower("0x1BC16D674EC80000") expectedBaseFeeVaultWithdrawalNetwork := genesis.FromUint8(0) expectedEnableGovernance := false @@ -326,6 +327,7 @@ func TestGlobalOverrides(t *testing.T) { "baseFeeVaultRecipient": expectedBaseFeeVaultRecipient, "l1FeeVaultRecipient": expectedL1FeeVaultRecipient, "sequencerFeeVaultRecipient": expectedSequencerFeeVaultRecipient, + "operatorFeeVaultRecipient": expectedOperatorFeeVaultRecipient, "baseFeeVaultMinimumWithdrawalAmount": expectedBaseFeeVaultMinimumWithdrawalAmount, "baseFeeVaultWithdrawalNetwork": expectedBaseFeeVaultWithdrawalNetwork, "enableGovernance": expectedEnableGovernance, @@ -579,6 +581,12 @@ func TestInvalidL2Genesis(t *testing.T) { "sequencerFeeVaultRecipient": nil, }, }, + { + name: "operator fee vault recipient not set", + overrides: map[string]any{ + "operatorFeeVaultRecipient": nil, + }, + }, { name: "l1 chain ID not set", overrides: map[string]any{ @@ -908,9 +916,6 @@ func validateOPChainDeployment(t *testing.T, cg codeGetter, st *state.State, int alloc := chainState.Allocs.Data.Accounts chainIntent := intent.Chains[i] - checkImmutableBehindProxy(t, alloc, predeploys.BaseFeeVaultAddr, chainIntent.BaseFeeVaultRecipient) - checkImmutableBehindProxy(t, alloc, predeploys.L1FeeVaultAddr, chainIntent.L1FeeVaultRecipient) - checkImmutableBehindProxy(t, alloc, predeploys.SequencerFeeVaultAddr, chainIntent.SequencerFeeVaultRecipient) checkImmutableBehindProxy(t, alloc, predeploys.OptimismMintableERC721FactoryAddr, common.BigToHash(new(big.Int).SetUint64(intent.L1ChainID))) // ownership slots diff --git a/op-deployer/pkg/deployer/integration_test/cli/e2e_apply_test.go b/op-deployer/pkg/deployer/integration_test/cli/e2e_apply_test.go index 49485751fa2e8..ebf006331f1ac 100644 --- a/op-deployer/pkg/deployer/integration_test/cli/e2e_apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/cli/e2e_apply_test.go @@ -55,6 +55,7 @@ func TestCLIEndToEndApply(t *testing.T) { chain.BaseFeeVaultRecipient = shared.AddrFor(t, dk, devkeys.BaseFeeVaultRecipientRole.Key(l1ChainIDBig)) chain.L1FeeVaultRecipient = shared.AddrFor(t, dk, devkeys.L1FeeVaultRecipientRole.Key(l1ChainIDBig)) chain.SequencerFeeVaultRecipient = shared.AddrFor(t, dk, devkeys.SequencerFeeVaultRecipientRole.Key(l1ChainIDBig)) + chain.OperatorFeeVaultRecipient = shared.AddrFor(t, dk, devkeys.OperatorFeeVaultRecipientRole.Key(l1ChainIDBig)) chain.Eip1559DenominatorCanyon = standard.Eip1559DenominatorCanyon chain.Eip1559Denominator = standard.Eip1559Denominator diff --git a/op-deployer/pkg/deployer/integration_test/cli/noop_test.go b/op-deployer/pkg/deployer/integration_test/cli/noop_test.go index 5aee016f02351..c8385bc657230 100644 --- a/op-deployer/pkg/deployer/integration_test/cli/noop_test.go +++ b/op-deployer/pkg/deployer/integration_test/cli/noop_test.go @@ -50,6 +50,7 @@ func TestCLIApplyNoOp(t *testing.T) { chain.BaseFeeVaultRecipient = shared.AddrFor(t, dk, devkeys.BaseFeeVaultRecipientRole.Key(l1ChainIDBig)) chain.L1FeeVaultRecipient = shared.AddrFor(t, dk, devkeys.L1FeeVaultRecipientRole.Key(l1ChainIDBig)) chain.SequencerFeeVaultRecipient = shared.AddrFor(t, dk, devkeys.SequencerFeeVaultRecipientRole.Key(l1ChainIDBig)) + chain.OperatorFeeVaultRecipient = shared.AddrFor(t, dk, devkeys.OperatorFeeVaultRecipientRole.Key(l1ChainIDBig)) chain.Eip1559DenominatorCanyon = standard.Eip1559DenominatorCanyon chain.Eip1559Denominator = standard.Eip1559Denominator diff --git a/op-deployer/pkg/deployer/integration_test/shared/shared.go b/op-deployer/pkg/deployer/integration_test/shared/shared.go index 296d56104a717..4484b2e13484d 100644 --- a/op-deployer/pkg/deployer/integration_test/shared/shared.go +++ b/op-deployer/pkg/deployer/integration_test/shared/shared.go @@ -29,6 +29,7 @@ func NewChainIntent(t *testing.T, dk *devkeys.MnemonicDevKeys, l1ChainID *big.In BaseFeeVaultRecipient: AddrFor(t, dk, devkeys.BaseFeeVaultRecipientRole.Key(l1ChainID)), L1FeeVaultRecipient: AddrFor(t, dk, devkeys.L1FeeVaultRecipientRole.Key(l1ChainID)), SequencerFeeVaultRecipient: AddrFor(t, dk, devkeys.SequencerFeeVaultRecipientRole.Key(l1ChainID)), + OperatorFeeVaultRecipient: AddrFor(t, dk, devkeys.OperatorFeeVaultRecipientRole.Key(l1ChainID)), Eip1559DenominatorCanyon: standard.Eip1559DenominatorCanyon, Eip1559Denominator: standard.Eip1559Denominator, Eip1559Elasticity: standard.Eip1559Elasticity, @@ -42,6 +43,8 @@ func NewChainIntent(t *testing.T, dk *devkeys.MnemonicDevKeys, l1ChainID *big.In Proposer: AddrFor(t, dk, devkeys.ProposerRole.Key(l1ChainID)), Challenger: AddrFor(t, dk, devkeys.ChallengerRole.Key(l1ChainID)), }, + UseRevenueShare: false, + ChainFeesRecipient: common.Address{}, } } diff --git a/op-deployer/pkg/deployer/manage/testdata/state.json b/op-deployer/pkg/deployer/manage/testdata/state.json index c99554e1e9731..9b0e0c606086a 100755 --- a/op-deployer/pkg/deployer/manage/testdata/state.json +++ b/op-deployer/pkg/deployer/manage/testdata/state.json @@ -20,6 +20,9 @@ "baseFeeVaultRecipient": "0x0000000000000000000000000000000000000001", "l1FeeVaultRecipient": "0x0000000000000000000000000000000000000002", "sequencerFeeVaultRecipient": "0x0000000000000000000000000000000000000003", + "operatorFeeVaultRecipient": "0x0000000000000000000000000000000000000004", + "useRevenueShare": true, + "chainFeesRecipient": "0x0000000000000000000000000000000000000005", "eip1559DenominatorCanyon": 250, "eip1559Denominator": 50, "eip1559Elasticity": 6, @@ -57,4 +60,3 @@ "l1StateDump": null, "DeploymentCalldata": null } - diff --git a/op-deployer/pkg/deployer/opcm/l2genesis.go b/op-deployer/pkg/deployer/opcm/l2genesis.go index 3f91196cc85fc..4c10573436fa2 100644 --- a/op-deployer/pkg/deployer/opcm/l2genesis.go +++ b/op-deployer/pkg/deployer/opcm/l2genesis.go @@ -23,11 +23,17 @@ type L2GenesisInput struct { L1FeeVaultRecipient common.Address L1FeeVaultMinimumWithdrawalAmount *big.Int L1FeeVaultWithdrawalNetwork *big.Int + OperatorFeeVaultRecipient common.Address + OperatorFeeVaultMinimumWithdrawalAmount *big.Int + OperatorFeeVaultWithdrawalNetwork *big.Int GovernanceTokenOwner common.Address Fork *big.Int DeployCrossL2Inbox bool EnableGovernance bool FundDevAccounts bool + UseRevenueShare bool + ChainFeesRecipient common.Address + L1FeesDepositor common.Address } type L2GenesisScript script.DeployScriptWithoutOutput[L2GenesisInput] diff --git a/op-deployer/pkg/deployer/pipeline/l2genesis.go b/op-deployer/pkg/deployer/pipeline/l2genesis.go index bb3f514358bce..19dec48dc3ca3 100644 --- a/op-deployer/pkg/deployer/pipeline/l2genesis.go +++ b/op-deployer/pkg/deployer/pipeline/l2genesis.go @@ -26,9 +26,11 @@ type l2GenesisOverrides struct { BaseFeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"baseFeeVaultMinimumWithdrawalAmount"` L1FeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"l1FeeVaultMinimumWithdrawalAmount"` SequencerFeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"sequencerFeeVaultMinimumWithdrawalAmount"` + OperatorFeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"operatorFeeVaultMinimumWithdrawalAmount"` BaseFeeVaultWithdrawalNetwork genesis.WithdrawalNetwork `json:"baseFeeVaultWithdrawalNetwork"` L1FeeVaultWithdrawalNetwork genesis.WithdrawalNetwork `json:"l1FeeVaultWithdrawalNetwork"` SequencerFeeVaultWithdrawalNetwork genesis.WithdrawalNetwork `json:"sequencerFeeVaultWithdrawalNetwork"` + OperatorFeeVaultWithdrawalNetwork genesis.WithdrawalNetwork `json:"operatorFeeVaultWithdrawalNetwork"` EnableGovernance bool `json:"enableGovernance"` GovernanceTokenOwner common.Address `json:"governanceTokenOwner"` } @@ -83,17 +85,23 @@ func GenerateL2Genesis(pEnv *Env, intent *state.Intent, bundle ArtifactsBundle, BaseFeeVaultWithdrawalNetwork: wdNetworkToBig(overrides.BaseFeeVaultWithdrawalNetwork), L1FeeVaultWithdrawalNetwork: wdNetworkToBig(overrides.L1FeeVaultWithdrawalNetwork), SequencerFeeVaultWithdrawalNetwork: wdNetworkToBig(overrides.SequencerFeeVaultWithdrawalNetwork), + OperatorFeeVaultWithdrawalNetwork: wdNetworkToBig(overrides.OperatorFeeVaultWithdrawalNetwork), SequencerFeeVaultMinimumWithdrawalAmount: overrides.SequencerFeeVaultMinimumWithdrawalAmount.ToInt(), BaseFeeVaultMinimumWithdrawalAmount: overrides.BaseFeeVaultMinimumWithdrawalAmount.ToInt(), L1FeeVaultMinimumWithdrawalAmount: overrides.L1FeeVaultMinimumWithdrawalAmount.ToInt(), + OperatorFeeVaultMinimumWithdrawalAmount: overrides.OperatorFeeVaultMinimumWithdrawalAmount.ToInt(), BaseFeeVaultRecipient: thisIntent.BaseFeeVaultRecipient, L1FeeVaultRecipient: thisIntent.L1FeeVaultRecipient, SequencerFeeVaultRecipient: thisIntent.SequencerFeeVaultRecipient, + OperatorFeeVaultRecipient: thisIntent.OperatorFeeVaultRecipient, GovernanceTokenOwner: overrides.GovernanceTokenOwner, Fork: big.NewInt(schedule.SolidityForkNumber(1)), DeployCrossL2Inbox: len(intent.Chains) > 1, EnableGovernance: overrides.EnableGovernance, FundDevAccounts: overrides.FundDevAccounts, + UseRevenueShare: thisIntent.UseRevenueShare, + ChainFeesRecipient: thisIntent.ChainFeesRecipient, + L1FeesDepositor: standard.L1FeesDepositor, }); err != nil { return fmt.Errorf("failed to call L2Genesis script: %w", err) } @@ -160,9 +168,11 @@ func defaultOverrides() l2GenesisOverrides { BaseFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, L1FeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, SequencerFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, + OperatorFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, BaseFeeVaultWithdrawalNetwork: "local", L1FeeVaultWithdrawalNetwork: "local", SequencerFeeVaultWithdrawalNetwork: "local", + OperatorFeeVaultWithdrawalNetwork: "local", EnableGovernance: false, GovernanceTokenOwner: standard.GovernanceTokenOwner, } diff --git a/op-deployer/pkg/deployer/pipeline/l2genesis_test.go b/op-deployer/pkg/deployer/pipeline/l2genesis_test.go index aed2a1e782390..0b7e683915119 100644 --- a/op-deployer/pkg/deployer/pipeline/l2genesis_test.go +++ b/op-deployer/pkg/deployer/pipeline/l2genesis_test.go @@ -48,9 +48,11 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { BaseFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, L1FeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, SequencerFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, + OperatorFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, BaseFeeVaultWithdrawalNetwork: "local", L1FeeVaultWithdrawalNetwork: "local", SequencerFeeVaultWithdrawalNetwork: "local", + OperatorFeeVaultWithdrawalNetwork: "local", EnableGovernance: false, GovernanceTokenOwner: standard.GovernanceTokenOwner, }, @@ -67,12 +69,15 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { "baseFeeVaultMinimumWithdrawalAmount": "0x1234", "l1FeeVaultMinimumWithdrawalAmount": "0x2345", "sequencerFeeVaultMinimumWithdrawalAmount": "0x3456", + "operatorFeeVaultMinimumWithdrawalAmount": "0x4567", "baseFeeVaultWithdrawalNetwork": "remote", "l1FeeVaultWithdrawalNetwork": "remote", "sequencerFeeVaultWithdrawalNetwork": "remote", + "operatorFeeVaultWithdrawalNetwork": "remote", "enableGovernance": true, "governanceTokenOwner": "0x1111111111111111111111111111111111111111", "l2GenesisInteropTimeOffset": "0x1234", + "chainFeesRecipient": "0x0000000000000000000000000000000000005678", }, }, chainIntent: &state.ChainIntent{}, @@ -82,9 +87,11 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { BaseFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x1234")), L1FeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x2345")), SequencerFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x3456")), + OperatorFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x4567")), BaseFeeVaultWithdrawalNetwork: "remote", L1FeeVaultWithdrawalNetwork: "remote", SequencerFeeVaultWithdrawalNetwork: "remote", + OperatorFeeVaultWithdrawalNetwork: "remote", EnableGovernance: true, GovernanceTokenOwner: common.HexToAddress("0x1111111111111111111111111111111111111111"), }, @@ -111,6 +118,7 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { "baseFeeVaultWithdrawalNetwork": "remote", "l1FeeVaultWithdrawalNetwork": "remote", "sequencerFeeVaultWithdrawalNetwork": "remote", + "operatorFeeVaultWithdrawalNetwork": "remote", "enableGovernance": true, "governanceTokenOwner": "0x1111111111111111111111111111111111111111", "l2GenesisInteropTimeOffset": "0x1234", @@ -122,9 +130,11 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { BaseFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x1234")), L1FeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x2345")), SequencerFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x3456")), + OperatorFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, BaseFeeVaultWithdrawalNetwork: "remote", L1FeeVaultWithdrawalNetwork: "remote", SequencerFeeVaultWithdrawalNetwork: "remote", + OperatorFeeVaultWithdrawalNetwork: "remote", EnableGovernance: true, GovernanceTokenOwner: common.HexToAddress("0x1111111111111111111111111111111111111111"), }, diff --git a/op-deployer/pkg/deployer/standard/standard.go b/op-deployer/pkg/deployer/standard/standard.go index 6500558dc6703..5ec49224b8611 100644 --- a/op-deployer/pkg/deployer/standard/standard.go +++ b/op-deployer/pkg/deployer/standard/standard.go @@ -33,6 +33,8 @@ const ( Eip1559Denominator uint64 = 50 Eip1559Elasticity uint64 = 6 + UseRevenueShare = true + ContractsV160Tag = "op-contracts/v1.6.0" ContractsV180Tag = "op-contracts/v1.8.0-rc.4" ContractsV170Beta1L2Tag = "op-contracts/v1.7.0-beta.1+l2-contracts" @@ -43,6 +45,9 @@ const ( CurrentTag = ContractsV410Tag ) +// TODO(#17505): This address should be updated to the actual address once deployed +var L1FeesDepositor = common.HexToAddress("0x81c01427DFA9A2512b4EBf1462868856BA4aA91a") + var DisputeAbsolutePrestate = common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") var VaultMinWithdrawalAmount = mustHexBigFromHex("0x8ac7230489e80000") diff --git a/op-deployer/pkg/deployer/state/chain_intent.go b/op-deployer/pkg/deployer/state/chain_intent.go index 3ca48014d313d..bc489729edbfb 100644 --- a/op-deployer/pkg/deployer/state/chain_intent.go +++ b/op-deployer/pkg/deployer/state/chain_intent.go @@ -61,6 +61,7 @@ type ChainIntent struct { BaseFeeVaultRecipient common.Address `json:"baseFeeVaultRecipient" toml:"baseFeeVaultRecipient"` L1FeeVaultRecipient common.Address `json:"l1FeeVaultRecipient" toml:"l1FeeVaultRecipient"` SequencerFeeVaultRecipient common.Address `json:"sequencerFeeVaultRecipient" toml:"sequencerFeeVaultRecipient"` + OperatorFeeVaultRecipient common.Address `json:"operatorFeeVaultRecipient" toml:"operatorFeeVaultRecipient"` Eip1559DenominatorCanyon uint64 `json:"eip1559DenominatorCanyon" toml:"eip1559DenominatorCanyon"` Eip1559Denominator uint64 `json:"eip1559Denominator" toml:"eip1559Denominator"` Eip1559Elasticity uint64 `json:"eip1559Elasticity" toml:"eip1559Elasticity"` @@ -72,6 +73,8 @@ type ChainIntent struct { OperatorFeeScalar uint32 `json:"operatorFeeScalar,omitempty" toml:"operatorFeeScalar,omitempty"` OperatorFeeConstant uint64 `json:"operatorFeeConstant,omitempty" toml:"operatorFeeConstant,omitempty"` L1StartBlockHash *common.Hash `json:"l1StartBlockHash,omitempty" toml:"l1StartBlockHash,omitempty"` + UseRevenueShare bool `json:"useRevenueShare,omitempty" toml:"useRevenueShare,omitempty"` + ChainFeesRecipient common.Address `json:"chainFeesRecipient,omitempty" toml:"chainFeesRecipient,omitempty"` MinBaseFee uint64 `json:"minBaseFee,omitempty" toml:"minBaseFee,omitempty"` DAFootprintGasScalar uint16 `json:"daFootprintGasScalar,omitempty" toml:"daFootprintGasScalar,omitempty"` @@ -94,6 +97,7 @@ var ErrGasLimitZeroValue = fmt.Errorf("chain has a gas limit set to zero value") var ErrNonStandardValue = fmt.Errorf("chain contains non-standard config value") var ErrEip1559ZeroValue = fmt.Errorf("eip1559 param is set to zero value") var ErrIncompatibleValue = fmt.Errorf("chain contains incompatible config value") +var ErrRevenueShareZeroAddress = fmt.Errorf("chain has enabled revenue share but recipient is set to zero address") func (c *ChainIntent) Check() error { if c.ID == emptyHash { @@ -116,7 +120,8 @@ func (c *ChainIntent) Check() error { if c.BaseFeeVaultRecipient == emptyAddress || c.L1FeeVaultRecipient == emptyAddress || - c.SequencerFeeVaultRecipient == emptyAddress { + c.SequencerFeeVaultRecipient == emptyAddress || + c.OperatorFeeVaultRecipient == emptyAddress { return fmt.Errorf("%w: chainId=%s", ErrFeeVaultZeroAddress, c.ID) } @@ -124,5 +129,11 @@ func (c *ChainIntent) Check() error { return c.DangerousAltDAConfig.Check(nil) } + if c.UseRevenueShare { + if c.ChainFeesRecipient == emptyAddress { + return fmt.Errorf("%w: chainId=%s", ErrRevenueShareZeroAddress, c.ID) + } + } + return nil } diff --git a/op-deployer/pkg/deployer/state/deploy_config.go b/op-deployer/pkg/deployer/state/deploy_config.go index 158892782f4ec..3dfea6d93a72c 100644 --- a/op-deployer/pkg/deployer/state/deploy_config.go +++ b/op-deployer/pkg/deployer/state/deploy_config.go @@ -46,12 +46,15 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, BaseFeeVaultWithdrawalNetwork: "local", L1FeeVaultWithdrawalNetwork: "local", SequencerFeeVaultWithdrawalNetwork: "local", + OperatorFeeVaultWithdrawalNetwork: "local", SequencerFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, BaseFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, L1FeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, + OperatorFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, BaseFeeVaultRecipient: chainIntent.BaseFeeVaultRecipient, L1FeeVaultRecipient: chainIntent.L1FeeVaultRecipient, SequencerFeeVaultRecipient: chainIntent.SequencerFeeVaultRecipient, + OperatorFeeVaultRecipient: chainIntent.OperatorFeeVaultRecipient, }, GovernanceDeployConfig: genesis.GovernanceDeployConfig{ EnableGovernance: false, @@ -70,6 +73,10 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, EIP1559DenominatorCanyon: 250, EIP1559Elasticity: chainIntent.Eip1559Elasticity, }, + RevenueShareDeployConfig: genesis.RevenueShareDeployConfig{ + UseRevenueShare: chainIntent.UseRevenueShare, + ChainFeesRecipient: chainIntent.ChainFeesRecipient, + }, // STOP! This struct sets the _default_ upgrade schedule for all chains. // Any upgrades you enable here will be enabled for all new deployments. diff --git a/op-deployer/pkg/deployer/state/deploy_config_test.go b/op-deployer/pkg/deployer/state/deploy_config_test.go index a91a90a8cbcb8..0dd35928ac981 100644 --- a/op-deployer/pkg/deployer/state/deploy_config_test.go +++ b/op-deployer/pkg/deployer/state/deploy_config_test.go @@ -26,6 +26,7 @@ func TestCombineDeployConfig(t *testing.T) { BaseFeeVaultRecipient: common.HexToAddress("0x123"), L1FeeVaultRecipient: common.HexToAddress("0x456"), SequencerFeeVaultRecipient: common.HexToAddress("0x789"), + OperatorFeeVaultRecipient: common.HexToAddress("0xabc"), Roles: ChainRoles{ SystemConfigOwner: common.HexToAddress("0x123"), L1ProxyAdminOwner: common.HexToAddress("0x456"), @@ -33,6 +34,8 @@ func TestCombineDeployConfig(t *testing.T) { UnsafeBlockSigner: common.HexToAddress("0xabc"), Batcher: common.HexToAddress("0xdef"), }, + UseRevenueShare: true, + ChainFeesRecipient: common.HexToAddress("0x123"), } state := State{ SuperchainDeployment: &addresses.SuperchainContracts{ProtocolVersionsProxy: common.HexToAddress("0x123")}, diff --git a/op-deployer/pkg/deployer/state/intent.go b/op-deployer/pkg/deployer/state/intent.go index d860a7049a688..8b1f0979d5a80 100644 --- a/op-deployer/pkg/deployer/state/intent.go +++ b/op-deployer/pkg/deployer/state/intent.go @@ -173,6 +173,11 @@ func (c *Intent) validateStandardValues() error { if len(chain.AdditionalDisputeGames) > 0 { return fmt.Errorf("%w: chainId=%s additionalDisputeGames must be nil", ErrNonStandardValue, chain.ID) } + if chain.UseRevenueShare { + if chain.ChainFeesRecipient == emptyAddress { + return fmt.Errorf("%w: chainId=%s", ErrRevenueShareZeroAddress, chain.ID) + } + } } challenger, _ := standard.ChallengerAddressFor(c.L1ChainID) @@ -358,6 +363,7 @@ func NewIntentStandard(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, erro L1ProxyAdminOwner: l1ProxyAdminOwner, L2ProxyAdminOwner: l2ProxyAdminOwner, }, + UseRevenueShare: standard.UseRevenueShare, }) } return intent, nil diff --git a/op-deployer/pkg/deployer/state/intent_test.go b/op-deployer/pkg/deployer/state/intent_test.go index 8ffb0060db3c5..852e31d312fb0 100644 --- a/op-deployer/pkg/deployer/state/intent_test.go +++ b/op-deployer/pkg/deployer/state/intent_test.go @@ -23,7 +23,8 @@ func TestValidateStandardValues(t *testing.T) { setFeeAddresses(&intent) err = intent.Check() - require.NoError(t, err) + require.Error(t, err) + require.ErrorIs(t, err, ErrRevenueShareZeroAddress) tests := []struct { name string @@ -87,6 +88,14 @@ func TestValidateStandardValues(t *testing.T) { }, ErrIncompatibleValue, }, + { + "RevenueShare", + func(intent *Intent) { + intent.Chains[0].UseRevenueShare = true + intent.Chains[0].ChainFeesRecipient = common.Address{} + }, + ErrRevenueShareZeroAddress, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -94,6 +103,7 @@ func TestValidateStandardValues(t *testing.T) { require.NoError(t, err) setChainRoles(&intent) setFeeAddresses(&intent) + setRevenueShare(&intent) tt.mutator(&intent) @@ -131,6 +141,10 @@ func TestValidateCustomValues(t *testing.T) { err = intent.Check() require.NoError(t, err) + setRevenueShare(&intent) + err = intent.Check() + require.NoError(t, err) + tests := []struct { name string mutator func(intent *Intent) @@ -155,6 +169,14 @@ func TestValidateCustomValues(t *testing.T) { }, ErrIncompatibleValue, }, + { + "zero address for revenue share chain fees recipient when enabled", + func(intent *Intent) { + intent.Chains[0].UseRevenueShare = true + intent.Chains[0].ChainFeesRecipient = common.Address{} + }, + ErrRevenueShareZeroAddress, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -210,4 +232,10 @@ func setFeeAddresses(intent *Intent) { intent.Chains[0].BaseFeeVaultRecipient = common.HexToAddress("0x08") intent.Chains[0].L1FeeVaultRecipient = common.HexToAddress("0x09") intent.Chains[0].SequencerFeeVaultRecipient = common.HexToAddress("0x0A") + intent.Chains[0].OperatorFeeVaultRecipient = common.HexToAddress("0x0B") +} + +func setRevenueShare(intent *Intent) { + intent.Chains[0].UseRevenueShare = true + intent.Chains[0].ChainFeesRecipient = common.HexToAddress("0x0C") } diff --git a/op-devstack/sysext/l2.go b/op-devstack/sysext/l2.go index 3ceda37a9e3ef..5ea7c19842092 100644 --- a/op-devstack/sysext/l2.go +++ b/op-devstack/sysext/l2.go @@ -441,6 +441,7 @@ func (o *Orchestrator) getWalletMappings(l1Wallets descriptors.WalletMap) map[st "sequencerFeeVaultRecipient": devkeys.SequencerFeeVaultRecipientRole, "baseFeeVaultRecipient": devkeys.BaseFeeVaultRecipientRole, "l1FeeVaultRecipient": devkeys.L1FeeVaultRecipientRole, + "operatorFeeVaultRecipient": devkeys.OperatorFeeVaultRecipientRole, } for walletRole, devkeyRole := range systemRoles { diff --git a/op-devstack/sysgo/deployer.go b/op-devstack/sysgo/deployer.go index 2db79a8857598..a16f0e5cadb97 100644 --- a/op-devstack/sysgo/deployer.go +++ b/op-devstack/sysgo/deployer.go @@ -414,6 +414,14 @@ func (wb *worldBuilder) buildL2DeploymentOutputs() { } } +func WithRevenueShare(enabled bool, chainFeesRecipient common.Address) DeployerOption { + return func(p devtest.P, keys devkeys.Keys, builder intentbuilder.Builder) { + for _, l2Cfg := range builder.L2s() { + l2Cfg.WithRevenueShare(enabled, chainFeesRecipient) + } + } +} + func (wb *worldBuilder) buildFullConfigSet() { // If no chain has interop active, the dep set will be nil here, // so we should skip building the full config set. diff --git a/op-e2e/bindings/feesplitter.go b/op-e2e/bindings/feesplitter.go new file mode 100644 index 0000000000000..f1f9d9734101f --- /dev/null +++ b/op-e2e/bindings/feesplitter.go @@ -0,0 +1,1154 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// ISharesCalculatorShareInfo is an auto generated low-level Go binding around an user-defined struct. +type ISharesCalculatorShareInfo struct { + Recipient common.Address + Amount *big.Int +} + +// FeeSplitterMetaData contains all meta data concerning the FeeSplitter contract. +var FeeSplitterMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"receive\",\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"MAX_DISBURSEMENT_INTERVAL\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint128\",\"internalType\":\"uint128\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"disburseFees\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"feeDisbursementInterval\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint128\",\"internalType\":\"uint128\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[{\"name\":\"_sharesCalculator\",\"type\":\"address\",\"internalType\":\"contractISharesCalculator\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"lastDisbursementTime\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint128\",\"internalType\":\"uint128\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"setFeeDisbursementInterval\",\"inputs\":[{\"name\":\"_newFeeDisbursementInterval\",\"type\":\"uint128\",\"internalType\":\"uint128\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setSharesCalculator\",\"inputs\":[{\"name\":\"_newSharesCalculator\",\"type\":\"address\",\"internalType\":\"contractISharesCalculator\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"sharesCalculator\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"contractISharesCalculator\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"version\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"FeeDisbursementIntervalUpdated\",\"inputs\":[{\"name\":\"oldFeeDisbursementInterval\",\"type\":\"uint128\",\"indexed\":false,\"internalType\":\"uint128\"},{\"name\":\"newFeeDisbursementInterval\",\"type\":\"uint128\",\"indexed\":false,\"internalType\":\"uint128\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"FeesDisbursed\",\"inputs\":[{\"name\":\"shareInfo\",\"type\":\"tuple[]\",\"indexed\":false,\"internalType\":\"structISharesCalculator.ShareInfo[]\",\"components\":[{\"name\":\"recipient\",\"type\":\"address\",\"internalType\":\"addresspayable\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"name\":\"grossRevenue\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"FeesReceived\",\"inputs\":[{\"name\":\"sender\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"newBalance\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Initialized\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint8\",\"indexed\":false,\"internalType\":\"uint8\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SharesCalculatorUpdated\",\"inputs\":[{\"name\":\"oldSharesCalculator\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"newSharesCalculator\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"FeeSplitter_DisbursementIntervalNotReached\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_ExceedsMaxFeeDisbursementTime\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_FailedToSendToRevenueShareRecipient\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_FeeDisbursementIntervalCannotBeZero\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_FeeShareInfoEmpty\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_FeeVaultMustWithdrawToFeeSplitter\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_FeeVaultMustWithdrawToL2\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_NoFeesCollected\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_OnlyProxyAdminOwner\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_ReceiveWindowClosed\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_SenderNotApprovedVault\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_SharesCalculatorCannotBeZero\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"FeeSplitter_SharesCalculatorMalformedOutput\",\"inputs\":[]}]", + Bin: "0x6080604052348015600e575f80fd5b5060156019565b60d4565b5f54610100900460ff161560835760405162461bcd60e51b815260206004820152602760248201527f496e697469616c697a61626c653a20636f6e747261637420697320696e697469604482015266616c697a696e6760c81b606482015260840160405180910390fd5b5f5460ff908116101560d2575f805460ff191660ff9081179091556040519081527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b565b611344806100e15f395ff3fe608060405260043610610096575f3560e01c80637dfbd04911610066578063b87ea8d41161004c578063b87ea8d41461031b578063c4d66de81461032f578063d61a398b1461034e575f80fd5b80637dfbd049146102e55780637fc81bb7146102fc575f80fd5b80630a7617b3146101e55780630c0544a314610206578063394d27311461026857806354fd4d5014610290575f80fd5b366101e1577fe3007e9730850b5618eacb0537bef0cf0f1600267ae8549e472449d77b731e455c6100f3576040517f17617f6100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b337342000000000000000000000000000000000000111480159061012b57503373420000000000000000000000000000000000001914155b801561014b57503373420000000000000000000000000000000000001a14155b801561016b57503373420000000000000000000000000000000000001b14155b156101a2576040517f9dcde10900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6040805134815247602082018190529133917f213e72af0d3613bd643cff3059f872c1015e6541624e37872bf95eefbaf220a8910160405180910390a2005b5f80fd5b3480156101f0575f80fd5b506102046101ff366004610f9e565b6103a4565b005b348015610211575f80fd5b506001546102429070010000000000000000000000000000000090046fffffffffffffffffffffffffffffffff1681565b6040516fffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b348015610273575f80fd5b50600154610242906fffffffffffffffffffffffffffffffff1681565b34801561029b575f80fd5b506102d86040518060400160405280600581526020017f312e302e3000000000000000000000000000000000000000000000000000000081525081565b60405161025f9190610fb9565b3480156102f0575f80fd5b506102426301e1338081565b348015610307575f80fd5b5061020461031636600461100c565b610566565b348015610326575f80fd5b50610204610759565b34801561033a575f80fd5b50610204610349366004610f9e565b610b3d565b348015610359575f80fd5b505f5461037f9062010000900473ffffffffffffffffffffffffffffffffffffffff1681565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200161025f565b73420000000000000000000000000000000000001873ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610401573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610425919061103b565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610489576040517f38bac74200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff81166104d6576040517f99c6ec0800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f805473ffffffffffffffffffffffffffffffffffffffff838116620100008181027fffffffffffffffffffff0000000000000000000000000000000000000000ffff85161790945560408051949093049091168084526020840191909152917f16417cc372deec0caee5f52e2ad77a5f07b4591fd56b4ff31b6e20f817d4daeb91015b60405180910390a15050565b73420000000000000000000000000000000000001873ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156105c3573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906105e7919061103b565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461064b576040517f38bac74200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b806fffffffffffffffffffffffffffffffff165f03610696576040517fcf85916100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6301e133806fffffffffffffffffffffffffffffffff821611156106e6576040517f30b9f35e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600180546fffffffffffffffffffffffffffffffff8381167001000000000000000000000000000000008181028385161790945560408051949093049091168084526020840191909152917f4492086b630ed3846eec0979dd87a71c814ceb1c6dab80ab81e3450b21e4de28910161055a565b60015461078e906fffffffffffffffffffffffffffffffff700100000000000000000000000000000000820481169116611083565b6fffffffffffffffffffffffffffffffff164210156107d9576040517f1e4a9f3a00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600180547fffffffffffffffffffffffffffffffff0000000000000000000000000000000016426fffffffffffffffffffffffffffffffff1617815561081e90610d33565b5f61083c734200000000000000000000000000000000000011610d59565b90505f61085c734200000000000000000000000000000000000019610d59565b90505f61087c73420000000000000000000000000000000000001a610d59565b90505f61089c73420000000000000000000000000000000000001b610d59565b90506108a75f610d33565b5f82826108b486886110b3565b6108be91906110b3565b6108c891906110b3565b9050805f03610903576040517fc8972e5200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f80546040517f54e7f42d000000000000000000000000000000000000000000000000000000008152600481018890526024810187905260448101859052606481018690526201000090910473ffffffffffffffffffffffffffffffffffffffff16906354e7f42d906084015f60405180830381865afa158015610989573d5f803e3d5ffd5b505050506040513d5f823e601f3d9081017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01682016040526109ce919081019061116b565b905080515f03610a0a576040517f763970d600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f805b8251811015610ac1575f838281518110610a2957610a2961123a565b60200260200101515f015190505f848381518110610a4957610a4961123a565b6020026020010151602001519050805f03610a65575050610ab9565b5f610a708383610f56565b905080610aa9576040517fd68d1b1800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610ab382866110b3565b94505050505b600101610a0d565b50828114610afb576040517f9c01eac000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7f73f9a13241a1848ec157967f3a85601709353e616f1f2605d818c0f2d21774df8284604051610b2c929190611267565b60405180910390a150505050505050565b5f54610100900460ff1615808015610b5b57505f54600160ff909116105b80610b745750303b158015610b7457505f5460ff166001145b610c04576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a6564000000000000000000000000000000000000606482015260840160405180910390fd5b5f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660011790558015610c60575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790555b5f80547fffffffffffffffffffff0000000000000000000000000000000000000000ffff166201000073ffffffffffffffffffffffffffffffffffffffff851602179055600180546fffffffffffffffffffffffffffffffff1672015180000000000000000000000000000000001790558015610d2f575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200161055a565b5050565b807fe3007e9730850b5618eacb0537bef0cf0f1600267ae8549e472449d77b731e455d50565b5f60018273ffffffffffffffffffffffffffffffffffffffff166382356d8a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610da5573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610dc99190611302565b6001811115610dda57610dda6112d5565b14610e11576040517fb4726cbe00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b3073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff166366d003ac6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610e71573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610e95919061103b565b73ffffffffffffffffffffffffffffffffffffffff1614610ee2576040517fc3380cef00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8173ffffffffffffffffffffffffffffffffffffffff16633ccfd60b6040518163ffffffff1660e01b81526004016020604051808303815f875af1158015610f2c573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610f509190611320565b92915050565b5f610f62835a84610f69565b9392505050565b5f805f805f858888f1949350505050565b73ffffffffffffffffffffffffffffffffffffffff81168114610f9b575f80fd5b50565b5f60208284031215610fae575f80fd5b8135610f6281610f7a565b602081525f82518060208401528060208501604085015e5f6040828501015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011684010191505092915050565b5f6020828403121561101c575f80fd5b81356fffffffffffffffffffffffffffffffff81168114610f62575f80fd5b5f6020828403121561104b575f80fd5b8151610f6281610f7a565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b6fffffffffffffffffffffffffffffffff8181168382160190808211156110ac576110ac611056565b5092915050565b80820180821115610f5057610f50611056565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040805190810167ffffffffffffffff81118282101715611116576111166110c6565b60405290565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016810167ffffffffffffffff81118282101715611163576111636110c6565b604052919050565b5f602080838503121561117c575f80fd5b825167ffffffffffffffff80821115611193575f80fd5b818501915085601f8301126111a6575f80fd5b8151818111156111b8576111b86110c6565b6111c6848260051b0161111c565b818152848101925060069190911b8301840190878211156111e5575f80fd5b928401925b8184101561122f5760408489031215611201575f80fd5b6112096110f3565b845161121481610f7a565b815284860151868201528352604090930192918401916111ea565b979650505050505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b604080825283518282018190525f91906020906060850190828801855b828110156112bf578151805173ffffffffffffffffffffffffffffffffffffffff168552850151858501529285019290840190600101611284565b5050508093505050508260208301529392505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b5f60208284031215611312575f80fd5b815160028110610f62575f80fd5b5f60208284031215611330575f80fd5b505191905056fea164736f6c6343000819000a", +} + +// FeeSplitterABI is the input ABI used to generate the binding from. +// Deprecated: Use FeeSplitterMetaData.ABI instead. +var FeeSplitterABI = FeeSplitterMetaData.ABI + +// FeeSplitterBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use FeeSplitterMetaData.Bin instead. +var FeeSplitterBin = FeeSplitterMetaData.Bin + +// DeployFeeSplitter deploys a new Ethereum contract, binding an instance of FeeSplitter to it. +func DeployFeeSplitter(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *FeeSplitter, error) { + parsed, err := FeeSplitterMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(FeeSplitterBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &FeeSplitter{FeeSplitterCaller: FeeSplitterCaller{contract: contract}, FeeSplitterTransactor: FeeSplitterTransactor{contract: contract}, FeeSplitterFilterer: FeeSplitterFilterer{contract: contract}}, nil +} + +// FeeSplitter is an auto generated Go binding around an Ethereum contract. +type FeeSplitter struct { + FeeSplitterCaller // Read-only binding to the contract + FeeSplitterTransactor // Write-only binding to the contract + FeeSplitterFilterer // Log filterer for contract events +} + +// FeeSplitterCaller is an auto generated read-only Go binding around an Ethereum contract. +type FeeSplitterCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FeeSplitterTransactor is an auto generated write-only Go binding around an Ethereum contract. +type FeeSplitterTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FeeSplitterFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type FeeSplitterFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FeeSplitterSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type FeeSplitterSession struct { + Contract *FeeSplitter // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// FeeSplitterCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type FeeSplitterCallerSession struct { + Contract *FeeSplitterCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// FeeSplitterTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type FeeSplitterTransactorSession struct { + Contract *FeeSplitterTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// FeeSplitterRaw is an auto generated low-level Go binding around an Ethereum contract. +type FeeSplitterRaw struct { + Contract *FeeSplitter // Generic contract binding to access the raw methods on +} + +// FeeSplitterCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type FeeSplitterCallerRaw struct { + Contract *FeeSplitterCaller // Generic read-only contract binding to access the raw methods on +} + +// FeeSplitterTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type FeeSplitterTransactorRaw struct { + Contract *FeeSplitterTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewFeeSplitter creates a new instance of FeeSplitter, bound to a specific deployed contract. +func NewFeeSplitter(address common.Address, backend bind.ContractBackend) (*FeeSplitter, error) { + contract, err := bindFeeSplitter(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &FeeSplitter{FeeSplitterCaller: FeeSplitterCaller{contract: contract}, FeeSplitterTransactor: FeeSplitterTransactor{contract: contract}, FeeSplitterFilterer: FeeSplitterFilterer{contract: contract}}, nil +} + +// NewFeeSplitterCaller creates a new read-only instance of FeeSplitter, bound to a specific deployed contract. +func NewFeeSplitterCaller(address common.Address, caller bind.ContractCaller) (*FeeSplitterCaller, error) { + contract, err := bindFeeSplitter(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &FeeSplitterCaller{contract: contract}, nil +} + +// NewFeeSplitterTransactor creates a new write-only instance of FeeSplitter, bound to a specific deployed contract. +func NewFeeSplitterTransactor(address common.Address, transactor bind.ContractTransactor) (*FeeSplitterTransactor, error) { + contract, err := bindFeeSplitter(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &FeeSplitterTransactor{contract: contract}, nil +} + +// NewFeeSplitterFilterer creates a new log filterer instance of FeeSplitter, bound to a specific deployed contract. +func NewFeeSplitterFilterer(address common.Address, filterer bind.ContractFilterer) (*FeeSplitterFilterer, error) { + contract, err := bindFeeSplitter(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &FeeSplitterFilterer{contract: contract}, nil +} + +// bindFeeSplitter binds a generic wrapper to an already deployed contract. +func bindFeeSplitter(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := FeeSplitterMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_FeeSplitter *FeeSplitterRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _FeeSplitter.Contract.FeeSplitterCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_FeeSplitter *FeeSplitterRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FeeSplitter.Contract.FeeSplitterTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_FeeSplitter *FeeSplitterRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _FeeSplitter.Contract.FeeSplitterTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_FeeSplitter *FeeSplitterCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _FeeSplitter.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_FeeSplitter *FeeSplitterTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FeeSplitter.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_FeeSplitter *FeeSplitterTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _FeeSplitter.Contract.contract.Transact(opts, method, params...) +} + +// MAXDISBURSEMENTINTERVAL is a free data retrieval call binding the contract method 0x7dfbd049. +// +// Solidity: function MAX_DISBURSEMENT_INTERVAL() view returns(uint128) +func (_FeeSplitter *FeeSplitterCaller) MAXDISBURSEMENTINTERVAL(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _FeeSplitter.contract.Call(opts, &out, "MAX_DISBURSEMENT_INTERVAL") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// MAXDISBURSEMENTINTERVAL is a free data retrieval call binding the contract method 0x7dfbd049. +// +// Solidity: function MAX_DISBURSEMENT_INTERVAL() view returns(uint128) +func (_FeeSplitter *FeeSplitterSession) MAXDISBURSEMENTINTERVAL() (*big.Int, error) { + return _FeeSplitter.Contract.MAXDISBURSEMENTINTERVAL(&_FeeSplitter.CallOpts) +} + +// MAXDISBURSEMENTINTERVAL is a free data retrieval call binding the contract method 0x7dfbd049. +// +// Solidity: function MAX_DISBURSEMENT_INTERVAL() view returns(uint128) +func (_FeeSplitter *FeeSplitterCallerSession) MAXDISBURSEMENTINTERVAL() (*big.Int, error) { + return _FeeSplitter.Contract.MAXDISBURSEMENTINTERVAL(&_FeeSplitter.CallOpts) +} + +// FeeDisbursementInterval is a free data retrieval call binding the contract method 0x0c0544a3. +// +// Solidity: function feeDisbursementInterval() view returns(uint128) +func (_FeeSplitter *FeeSplitterCaller) FeeDisbursementInterval(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _FeeSplitter.contract.Call(opts, &out, "feeDisbursementInterval") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// FeeDisbursementInterval is a free data retrieval call binding the contract method 0x0c0544a3. +// +// Solidity: function feeDisbursementInterval() view returns(uint128) +func (_FeeSplitter *FeeSplitterSession) FeeDisbursementInterval() (*big.Int, error) { + return _FeeSplitter.Contract.FeeDisbursementInterval(&_FeeSplitter.CallOpts) +} + +// FeeDisbursementInterval is a free data retrieval call binding the contract method 0x0c0544a3. +// +// Solidity: function feeDisbursementInterval() view returns(uint128) +func (_FeeSplitter *FeeSplitterCallerSession) FeeDisbursementInterval() (*big.Int, error) { + return _FeeSplitter.Contract.FeeDisbursementInterval(&_FeeSplitter.CallOpts) +} + +// LastDisbursementTime is a free data retrieval call binding the contract method 0x394d2731. +// +// Solidity: function lastDisbursementTime() view returns(uint128) +func (_FeeSplitter *FeeSplitterCaller) LastDisbursementTime(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _FeeSplitter.contract.Call(opts, &out, "lastDisbursementTime") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// LastDisbursementTime is a free data retrieval call binding the contract method 0x394d2731. +// +// Solidity: function lastDisbursementTime() view returns(uint128) +func (_FeeSplitter *FeeSplitterSession) LastDisbursementTime() (*big.Int, error) { + return _FeeSplitter.Contract.LastDisbursementTime(&_FeeSplitter.CallOpts) +} + +// LastDisbursementTime is a free data retrieval call binding the contract method 0x394d2731. +// +// Solidity: function lastDisbursementTime() view returns(uint128) +func (_FeeSplitter *FeeSplitterCallerSession) LastDisbursementTime() (*big.Int, error) { + return _FeeSplitter.Contract.LastDisbursementTime(&_FeeSplitter.CallOpts) +} + +// SharesCalculator is a free data retrieval call binding the contract method 0xd61a398b. +// +// Solidity: function sharesCalculator() view returns(address) +func (_FeeSplitter *FeeSplitterCaller) SharesCalculator(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _FeeSplitter.contract.Call(opts, &out, "sharesCalculator") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// SharesCalculator is a free data retrieval call binding the contract method 0xd61a398b. +// +// Solidity: function sharesCalculator() view returns(address) +func (_FeeSplitter *FeeSplitterSession) SharesCalculator() (common.Address, error) { + return _FeeSplitter.Contract.SharesCalculator(&_FeeSplitter.CallOpts) +} + +// SharesCalculator is a free data retrieval call binding the contract method 0xd61a398b. +// +// Solidity: function sharesCalculator() view returns(address) +func (_FeeSplitter *FeeSplitterCallerSession) SharesCalculator() (common.Address, error) { + return _FeeSplitter.Contract.SharesCalculator(&_FeeSplitter.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_FeeSplitter *FeeSplitterCaller) Version(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _FeeSplitter.contract.Call(opts, &out, "version") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_FeeSplitter *FeeSplitterSession) Version() (string, error) { + return _FeeSplitter.Contract.Version(&_FeeSplitter.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_FeeSplitter *FeeSplitterCallerSession) Version() (string, error) { + return _FeeSplitter.Contract.Version(&_FeeSplitter.CallOpts) +} + +// DisburseFees is a paid mutator transaction binding the contract method 0xb87ea8d4. +// +// Solidity: function disburseFees() returns() +func (_FeeSplitter *FeeSplitterTransactor) DisburseFees(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FeeSplitter.contract.Transact(opts, "disburseFees") +} + +// DisburseFees is a paid mutator transaction binding the contract method 0xb87ea8d4. +// +// Solidity: function disburseFees() returns() +func (_FeeSplitter *FeeSplitterSession) DisburseFees() (*types.Transaction, error) { + return _FeeSplitter.Contract.DisburseFees(&_FeeSplitter.TransactOpts) +} + +// DisburseFees is a paid mutator transaction binding the contract method 0xb87ea8d4. +// +// Solidity: function disburseFees() returns() +func (_FeeSplitter *FeeSplitterTransactorSession) DisburseFees() (*types.Transaction, error) { + return _FeeSplitter.Contract.DisburseFees(&_FeeSplitter.TransactOpts) +} + +// Initialize is a paid mutator transaction binding the contract method 0xc4d66de8. +// +// Solidity: function initialize(address _sharesCalculator) returns() +func (_FeeSplitter *FeeSplitterTransactor) Initialize(opts *bind.TransactOpts, _sharesCalculator common.Address) (*types.Transaction, error) { + return _FeeSplitter.contract.Transact(opts, "initialize", _sharesCalculator) +} + +// Initialize is a paid mutator transaction binding the contract method 0xc4d66de8. +// +// Solidity: function initialize(address _sharesCalculator) returns() +func (_FeeSplitter *FeeSplitterSession) Initialize(_sharesCalculator common.Address) (*types.Transaction, error) { + return _FeeSplitter.Contract.Initialize(&_FeeSplitter.TransactOpts, _sharesCalculator) +} + +// Initialize is a paid mutator transaction binding the contract method 0xc4d66de8. +// +// Solidity: function initialize(address _sharesCalculator) returns() +func (_FeeSplitter *FeeSplitterTransactorSession) Initialize(_sharesCalculator common.Address) (*types.Transaction, error) { + return _FeeSplitter.Contract.Initialize(&_FeeSplitter.TransactOpts, _sharesCalculator) +} + +// SetFeeDisbursementInterval is a paid mutator transaction binding the contract method 0x7fc81bb7. +// +// Solidity: function setFeeDisbursementInterval(uint128 _newFeeDisbursementInterval) returns() +func (_FeeSplitter *FeeSplitterTransactor) SetFeeDisbursementInterval(opts *bind.TransactOpts, _newFeeDisbursementInterval *big.Int) (*types.Transaction, error) { + return _FeeSplitter.contract.Transact(opts, "setFeeDisbursementInterval", _newFeeDisbursementInterval) +} + +// SetFeeDisbursementInterval is a paid mutator transaction binding the contract method 0x7fc81bb7. +// +// Solidity: function setFeeDisbursementInterval(uint128 _newFeeDisbursementInterval) returns() +func (_FeeSplitter *FeeSplitterSession) SetFeeDisbursementInterval(_newFeeDisbursementInterval *big.Int) (*types.Transaction, error) { + return _FeeSplitter.Contract.SetFeeDisbursementInterval(&_FeeSplitter.TransactOpts, _newFeeDisbursementInterval) +} + +// SetFeeDisbursementInterval is a paid mutator transaction binding the contract method 0x7fc81bb7. +// +// Solidity: function setFeeDisbursementInterval(uint128 _newFeeDisbursementInterval) returns() +func (_FeeSplitter *FeeSplitterTransactorSession) SetFeeDisbursementInterval(_newFeeDisbursementInterval *big.Int) (*types.Transaction, error) { + return _FeeSplitter.Contract.SetFeeDisbursementInterval(&_FeeSplitter.TransactOpts, _newFeeDisbursementInterval) +} + +// SetSharesCalculator is a paid mutator transaction binding the contract method 0x0a7617b3. +// +// Solidity: function setSharesCalculator(address _newSharesCalculator) returns() +func (_FeeSplitter *FeeSplitterTransactor) SetSharesCalculator(opts *bind.TransactOpts, _newSharesCalculator common.Address) (*types.Transaction, error) { + return _FeeSplitter.contract.Transact(opts, "setSharesCalculator", _newSharesCalculator) +} + +// SetSharesCalculator is a paid mutator transaction binding the contract method 0x0a7617b3. +// +// Solidity: function setSharesCalculator(address _newSharesCalculator) returns() +func (_FeeSplitter *FeeSplitterSession) SetSharesCalculator(_newSharesCalculator common.Address) (*types.Transaction, error) { + return _FeeSplitter.Contract.SetSharesCalculator(&_FeeSplitter.TransactOpts, _newSharesCalculator) +} + +// SetSharesCalculator is a paid mutator transaction binding the contract method 0x0a7617b3. +// +// Solidity: function setSharesCalculator(address _newSharesCalculator) returns() +func (_FeeSplitter *FeeSplitterTransactorSession) SetSharesCalculator(_newSharesCalculator common.Address) (*types.Transaction, error) { + return _FeeSplitter.Contract.SetSharesCalculator(&_FeeSplitter.TransactOpts, _newSharesCalculator) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_FeeSplitter *FeeSplitterTransactor) Receive(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FeeSplitter.contract.RawTransact(opts, nil) // calldata is disallowed for receive function +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_FeeSplitter *FeeSplitterSession) Receive() (*types.Transaction, error) { + return _FeeSplitter.Contract.Receive(&_FeeSplitter.TransactOpts) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_FeeSplitter *FeeSplitterTransactorSession) Receive() (*types.Transaction, error) { + return _FeeSplitter.Contract.Receive(&_FeeSplitter.TransactOpts) +} + +// FeeSplitterFeeDisbursementIntervalUpdatedIterator is returned from FilterFeeDisbursementIntervalUpdated and is used to iterate over the raw logs and unpacked data for FeeDisbursementIntervalUpdated events raised by the FeeSplitter contract. +type FeeSplitterFeeDisbursementIntervalUpdatedIterator struct { + Event *FeeSplitterFeeDisbursementIntervalUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *FeeSplitterFeeDisbursementIntervalUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(FeeSplitterFeeDisbursementIntervalUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(FeeSplitterFeeDisbursementIntervalUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *FeeSplitterFeeDisbursementIntervalUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FeeSplitterFeeDisbursementIntervalUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FeeSplitterFeeDisbursementIntervalUpdated represents a FeeDisbursementIntervalUpdated event raised by the FeeSplitter contract. +type FeeSplitterFeeDisbursementIntervalUpdated struct { + OldFeeDisbursementInterval *big.Int + NewFeeDisbursementInterval *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterFeeDisbursementIntervalUpdated is a free log retrieval operation binding the contract event 0x4492086b630ed3846eec0979dd87a71c814ceb1c6dab80ab81e3450b21e4de28. +// +// Solidity: event FeeDisbursementIntervalUpdated(uint128 oldFeeDisbursementInterval, uint128 newFeeDisbursementInterval) +func (_FeeSplitter *FeeSplitterFilterer) FilterFeeDisbursementIntervalUpdated(opts *bind.FilterOpts) (*FeeSplitterFeeDisbursementIntervalUpdatedIterator, error) { + + logs, sub, err := _FeeSplitter.contract.FilterLogs(opts, "FeeDisbursementIntervalUpdated") + if err != nil { + return nil, err + } + return &FeeSplitterFeeDisbursementIntervalUpdatedIterator{contract: _FeeSplitter.contract, event: "FeeDisbursementIntervalUpdated", logs: logs, sub: sub}, nil +} + +// WatchFeeDisbursementIntervalUpdated is a free log subscription operation binding the contract event 0x4492086b630ed3846eec0979dd87a71c814ceb1c6dab80ab81e3450b21e4de28. +// +// Solidity: event FeeDisbursementIntervalUpdated(uint128 oldFeeDisbursementInterval, uint128 newFeeDisbursementInterval) +func (_FeeSplitter *FeeSplitterFilterer) WatchFeeDisbursementIntervalUpdated(opts *bind.WatchOpts, sink chan<- *FeeSplitterFeeDisbursementIntervalUpdated) (event.Subscription, error) { + + logs, sub, err := _FeeSplitter.contract.WatchLogs(opts, "FeeDisbursementIntervalUpdated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(FeeSplitterFeeDisbursementIntervalUpdated) + if err := _FeeSplitter.contract.UnpackLog(event, "FeeDisbursementIntervalUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseFeeDisbursementIntervalUpdated is a log parse operation binding the contract event 0x4492086b630ed3846eec0979dd87a71c814ceb1c6dab80ab81e3450b21e4de28. +// +// Solidity: event FeeDisbursementIntervalUpdated(uint128 oldFeeDisbursementInterval, uint128 newFeeDisbursementInterval) +func (_FeeSplitter *FeeSplitterFilterer) ParseFeeDisbursementIntervalUpdated(log types.Log) (*FeeSplitterFeeDisbursementIntervalUpdated, error) { + event := new(FeeSplitterFeeDisbursementIntervalUpdated) + if err := _FeeSplitter.contract.UnpackLog(event, "FeeDisbursementIntervalUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FeeSplitterFeesDisbursedIterator is returned from FilterFeesDisbursed and is used to iterate over the raw logs and unpacked data for FeesDisbursed events raised by the FeeSplitter contract. +type FeeSplitterFeesDisbursedIterator struct { + Event *FeeSplitterFeesDisbursed // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *FeeSplitterFeesDisbursedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(FeeSplitterFeesDisbursed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(FeeSplitterFeesDisbursed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *FeeSplitterFeesDisbursedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FeeSplitterFeesDisbursedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FeeSplitterFeesDisbursed represents a FeesDisbursed event raised by the FeeSplitter contract. +type FeeSplitterFeesDisbursed struct { + ShareInfo []ISharesCalculatorShareInfo + GrossRevenue *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterFeesDisbursed is a free log retrieval operation binding the contract event 0x73f9a13241a1848ec157967f3a85601709353e616f1f2605d818c0f2d21774df. +// +// Solidity: event FeesDisbursed((address,uint256)[] shareInfo, uint256 grossRevenue) +func (_FeeSplitter *FeeSplitterFilterer) FilterFeesDisbursed(opts *bind.FilterOpts) (*FeeSplitterFeesDisbursedIterator, error) { + + logs, sub, err := _FeeSplitter.contract.FilterLogs(opts, "FeesDisbursed") + if err != nil { + return nil, err + } + return &FeeSplitterFeesDisbursedIterator{contract: _FeeSplitter.contract, event: "FeesDisbursed", logs: logs, sub: sub}, nil +} + +// WatchFeesDisbursed is a free log subscription operation binding the contract event 0x73f9a13241a1848ec157967f3a85601709353e616f1f2605d818c0f2d21774df. +// +// Solidity: event FeesDisbursed((address,uint256)[] shareInfo, uint256 grossRevenue) +func (_FeeSplitter *FeeSplitterFilterer) WatchFeesDisbursed(opts *bind.WatchOpts, sink chan<- *FeeSplitterFeesDisbursed) (event.Subscription, error) { + + logs, sub, err := _FeeSplitter.contract.WatchLogs(opts, "FeesDisbursed") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(FeeSplitterFeesDisbursed) + if err := _FeeSplitter.contract.UnpackLog(event, "FeesDisbursed", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseFeesDisbursed is a log parse operation binding the contract event 0x73f9a13241a1848ec157967f3a85601709353e616f1f2605d818c0f2d21774df. +// +// Solidity: event FeesDisbursed((address,uint256)[] shareInfo, uint256 grossRevenue) +func (_FeeSplitter *FeeSplitterFilterer) ParseFeesDisbursed(log types.Log) (*FeeSplitterFeesDisbursed, error) { + event := new(FeeSplitterFeesDisbursed) + if err := _FeeSplitter.contract.UnpackLog(event, "FeesDisbursed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FeeSplitterFeesReceivedIterator is returned from FilterFeesReceived and is used to iterate over the raw logs and unpacked data for FeesReceived events raised by the FeeSplitter contract. +type FeeSplitterFeesReceivedIterator struct { + Event *FeeSplitterFeesReceived // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *FeeSplitterFeesReceivedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(FeeSplitterFeesReceived) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(FeeSplitterFeesReceived) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *FeeSplitterFeesReceivedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FeeSplitterFeesReceivedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FeeSplitterFeesReceived represents a FeesReceived event raised by the FeeSplitter contract. +type FeeSplitterFeesReceived struct { + Sender common.Address + Amount *big.Int + NewBalance *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterFeesReceived is a free log retrieval operation binding the contract event 0x213e72af0d3613bd643cff3059f872c1015e6541624e37872bf95eefbaf220a8. +// +// Solidity: event FeesReceived(address indexed sender, uint256 amount, uint256 newBalance) +func (_FeeSplitter *FeeSplitterFilterer) FilterFeesReceived(opts *bind.FilterOpts, sender []common.Address) (*FeeSplitterFeesReceivedIterator, error) { + + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + + logs, sub, err := _FeeSplitter.contract.FilterLogs(opts, "FeesReceived", senderRule) + if err != nil { + return nil, err + } + return &FeeSplitterFeesReceivedIterator{contract: _FeeSplitter.contract, event: "FeesReceived", logs: logs, sub: sub}, nil +} + +// WatchFeesReceived is a free log subscription operation binding the contract event 0x213e72af0d3613bd643cff3059f872c1015e6541624e37872bf95eefbaf220a8. +// +// Solidity: event FeesReceived(address indexed sender, uint256 amount, uint256 newBalance) +func (_FeeSplitter *FeeSplitterFilterer) WatchFeesReceived(opts *bind.WatchOpts, sink chan<- *FeeSplitterFeesReceived, sender []common.Address) (event.Subscription, error) { + + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + + logs, sub, err := _FeeSplitter.contract.WatchLogs(opts, "FeesReceived", senderRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(FeeSplitterFeesReceived) + if err := _FeeSplitter.contract.UnpackLog(event, "FeesReceived", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseFeesReceived is a log parse operation binding the contract event 0x213e72af0d3613bd643cff3059f872c1015e6541624e37872bf95eefbaf220a8. +// +// Solidity: event FeesReceived(address indexed sender, uint256 amount, uint256 newBalance) +func (_FeeSplitter *FeeSplitterFilterer) ParseFeesReceived(log types.Log) (*FeeSplitterFeesReceived, error) { + event := new(FeeSplitterFeesReceived) + if err := _FeeSplitter.contract.UnpackLog(event, "FeesReceived", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FeeSplitterInitializedIterator is returned from FilterInitialized and is used to iterate over the raw logs and unpacked data for Initialized events raised by the FeeSplitter contract. +type FeeSplitterInitializedIterator struct { + Event *FeeSplitterInitialized // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *FeeSplitterInitializedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(FeeSplitterInitialized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(FeeSplitterInitialized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *FeeSplitterInitializedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FeeSplitterInitializedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FeeSplitterInitialized represents a Initialized event raised by the FeeSplitter contract. +type FeeSplitterInitialized struct { + Version uint8 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInitialized is a free log retrieval operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_FeeSplitter *FeeSplitterFilterer) FilterInitialized(opts *bind.FilterOpts) (*FeeSplitterInitializedIterator, error) { + + logs, sub, err := _FeeSplitter.contract.FilterLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return &FeeSplitterInitializedIterator{contract: _FeeSplitter.contract, event: "Initialized", logs: logs, sub: sub}, nil +} + +// WatchInitialized is a free log subscription operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_FeeSplitter *FeeSplitterFilterer) WatchInitialized(opts *bind.WatchOpts, sink chan<- *FeeSplitterInitialized) (event.Subscription, error) { + + logs, sub, err := _FeeSplitter.contract.WatchLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(FeeSplitterInitialized) + if err := _FeeSplitter.contract.UnpackLog(event, "Initialized", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseInitialized is a log parse operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_FeeSplitter *FeeSplitterFilterer) ParseInitialized(log types.Log) (*FeeSplitterInitialized, error) { + event := new(FeeSplitterInitialized) + if err := _FeeSplitter.contract.UnpackLog(event, "Initialized", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FeeSplitterSharesCalculatorUpdatedIterator is returned from FilterSharesCalculatorUpdated and is used to iterate over the raw logs and unpacked data for SharesCalculatorUpdated events raised by the FeeSplitter contract. +type FeeSplitterSharesCalculatorUpdatedIterator struct { + Event *FeeSplitterSharesCalculatorUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *FeeSplitterSharesCalculatorUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(FeeSplitterSharesCalculatorUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(FeeSplitterSharesCalculatorUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *FeeSplitterSharesCalculatorUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FeeSplitterSharesCalculatorUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FeeSplitterSharesCalculatorUpdated represents a SharesCalculatorUpdated event raised by the FeeSplitter contract. +type FeeSplitterSharesCalculatorUpdated struct { + OldSharesCalculator common.Address + NewSharesCalculator common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSharesCalculatorUpdated is a free log retrieval operation binding the contract event 0x16417cc372deec0caee5f52e2ad77a5f07b4591fd56b4ff31b6e20f817d4daeb. +// +// Solidity: event SharesCalculatorUpdated(address oldSharesCalculator, address newSharesCalculator) +func (_FeeSplitter *FeeSplitterFilterer) FilterSharesCalculatorUpdated(opts *bind.FilterOpts) (*FeeSplitterSharesCalculatorUpdatedIterator, error) { + + logs, sub, err := _FeeSplitter.contract.FilterLogs(opts, "SharesCalculatorUpdated") + if err != nil { + return nil, err + } + return &FeeSplitterSharesCalculatorUpdatedIterator{contract: _FeeSplitter.contract, event: "SharesCalculatorUpdated", logs: logs, sub: sub}, nil +} + +// WatchSharesCalculatorUpdated is a free log subscription operation binding the contract event 0x16417cc372deec0caee5f52e2ad77a5f07b4591fd56b4ff31b6e20f817d4daeb. +// +// Solidity: event SharesCalculatorUpdated(address oldSharesCalculator, address newSharesCalculator) +func (_FeeSplitter *FeeSplitterFilterer) WatchSharesCalculatorUpdated(opts *bind.WatchOpts, sink chan<- *FeeSplitterSharesCalculatorUpdated) (event.Subscription, error) { + + logs, sub, err := _FeeSplitter.contract.WatchLogs(opts, "SharesCalculatorUpdated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(FeeSplitterSharesCalculatorUpdated) + if err := _FeeSplitter.contract.UnpackLog(event, "SharesCalculatorUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSharesCalculatorUpdated is a log parse operation binding the contract event 0x16417cc372deec0caee5f52e2ad77a5f07b4591fd56b4ff31b6e20f817d4daeb. +// +// Solidity: event SharesCalculatorUpdated(address oldSharesCalculator, address newSharesCalculator) +func (_FeeSplitter *FeeSplitterFilterer) ParseSharesCalculatorUpdated(log types.Log) (*FeeSplitterSharesCalculatorUpdated, error) { + event := new(FeeSplitterSharesCalculatorUpdated) + if err := _FeeSplitter.contract.UnpackLog(event, "SharesCalculatorUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/op-e2e/bindings/operatorfeevault.go b/op-e2e/bindings/operatorfeevault.go new file mode 100644 index 0000000000000..83485a6ff4830 --- /dev/null +++ b/op-e2e/bindings/operatorfeevault.go @@ -0,0 +1,1389 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// OperatorFeeVaultMetaData contains all meta data concerning the OperatorFeeVault contract. +var OperatorFeeVaultMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"receive\",\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"MIN_WITHDRAWAL_AMOUNT\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"RECIPIENT\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"WITHDRAWAL_NETWORK\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint8\",\"internalType\":\"enumTypes.WithdrawalNetwork\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[{\"name\":\"_recipient\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"_minWithdrawalAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"_withdrawalNetwork\",\"type\":\"uint8\",\"internalType\":\"enumTypes.WithdrawalNetwork\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"minWithdrawalAmount\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"recipient\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"setMinWithdrawalAmount\",\"inputs\":[{\"name\":\"_newMinWithdrawalAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setRecipient\",\"inputs\":[{\"name\":\"_newRecipient\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setWithdrawalNetwork\",\"inputs\":[{\"name\":\"_newWithdrawalNetwork\",\"type\":\"uint8\",\"internalType\":\"enumTypes.WithdrawalNetwork\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"totalProcessed\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"version\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"withdraw\",\"inputs\":[],\"outputs\":[{\"name\":\"value_\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"withdrawalNetwork\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint8\",\"internalType\":\"enumTypes.WithdrawalNetwork\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"Initialized\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint64\",\"indexed\":false,\"internalType\":\"uint64\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"MinWithdrawalAmountUpdated\",\"inputs\":[{\"name\":\"oldWithdrawalAmount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"newWithdrawalAmount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"RecipientUpdated\",\"inputs\":[{\"name\":\"oldRecipient\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"newRecipient\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Withdrawal\",\"inputs\":[{\"name\":\"value\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"to\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"from\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Withdrawal\",\"inputs\":[{\"name\":\"value\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"to\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"from\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"withdrawalNetwork\",\"type\":\"uint8\",\"indexed\":false,\"internalType\":\"enumTypes.WithdrawalNetwork\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"WithdrawalNetworkUpdated\",\"inputs\":[{\"name\":\"oldWithdrawalNetwork\",\"type\":\"uint8\",\"indexed\":false,\"internalType\":\"enumTypes.WithdrawalNetwork\"},{\"name\":\"newWithdrawalNetwork\",\"type\":\"uint8\",\"indexed\":false,\"internalType\":\"enumTypes.WithdrawalNetwork\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"FeeVault_OnlyProxyAdminOwner\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InvalidInitialization\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NotInitializing\",\"inputs\":[]}]", + Bin: "0x6080604052348015600e575f80fd5b5060156019565b60c9565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff161560685760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b039081161460c65780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b610eb9806100d65f395ff3fe6080604052600436106100d1575f3560e01c806382356d8a1161007c57806385b5b14d1161005757806385b5b14d14610276578063b49dc74114610295578063d0e12f90146102b4578063d3e5792b146102e3575f80fd5b806382356d8a1461020f5780638312f1491461024d57806384411d6514610262575f80fd5b80633ccfd60b116100ac5780633ccfd60b1461016c57806354fd4d501461018e57806366d003ac146101e3575f80fd5b80630d9019e1146100dc578063307f29621461012c5780633bbed4a01461014d575f80fd5b366100d857005b5f80fd5b3480156100e7575f80fd5b5060025473ffffffffffffffffffffffffffffffffffffffff165b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b348015610137575f80fd5b5061014b610146366004610c86565b6102f7565b005b348015610158575f80fd5b5061014b610167366004610cc3565b61047a565b348015610177575f80fd5b506101806105de565b604051908152602001610123565b348015610199575f80fd5b506101d66040518060400160405280600581526020017f312e312e3000000000000000000000000000000000000000000000000000000081525081565b6040516101239190610cde565b3480156101ee575f80fd5b506002546101029073ffffffffffffffffffffffffffffffffffffffff1681565b34801561021a575f80fd5b506002546102409074010000000000000000000000000000000000000000900460ff1681565b6040516101239190610d97565b348015610258575f80fd5b5061018060015481565b34801561026d575f80fd5b506101805f5481565b348015610281575f80fd5b5061014b610290366004610dab565b610918565b3480156102a0575f80fd5b5061014b6102af366004610dc2565b610a3b565b3480156102bf575f80fd5b5060025474010000000000000000000000000000000000000000900460ff16610240565b3480156102ee575f80fd5b50600154610180565b73420000000000000000000000000000000000001873ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610354573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906103789190610dfd565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146103dc576040517f7cd7e09f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600280547401000000000000000000000000000000000000000080820460ff1692849290917fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff9091169083600181111561043857610438610d31565b02179055507ff2ec44eb1c3b3acd547b76333eb2c4b27eee311860c57a9fdb04c95f62398fc8818360405161046e929190610e18565b60405180910390a15050565b73420000000000000000000000000000000000001873ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156104d7573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906104fb9190610dfd565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461055f576040517f7cd7e09f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6002805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff000000000000000000000000000000000000000083168117909355604080519190921680825260208201939093527f62e69886a5df0ba8ffcacbfc1388754e7abd9bde24b036354c561f1acd4e4593910161046e565b5f60015447101561069c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604a60248201527f4665655661756c743a207769746864726177616c20616d6f756e74206d75737460448201527f2062652067726561746572207468616e206d696e696d756d207769746864726160648201527f77616c20616d6f756e7400000000000000000000000000000000000000000000608482015260a4015b60405180910390fd5b479050805f808282546106af9190610e33565b90915550506002546040805183815273ffffffffffffffffffffffffffffffffffffffff90921660208301523382820152517fc8a211cc64b6ed1b50595a9fcb1932b6d1e5a6e8ef15b60e5b1f988ea9086bba9181900360600190a16002546040517f38e04cbeb8c10f8f568618aa75be0f10b6729b8b4237743b4de20cbcde2839ee9161077491849173ffffffffffffffffffffffffffffffffffffffff811691339174010000000000000000000000000000000000000000900460ff1690610e6b565b60405180910390a1600160025474010000000000000000000000000000000000000000900460ff1660018111156107ad576107ad610d31565b0361086a576002545f906107d79073ffffffffffffffffffffffffffffffffffffffff1683610c4f565b905080610866576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f4665655661756c743a206661696c656420746f2073656e642045544820746f2060448201527f4c322066656520726563697069656e74000000000000000000000000000000006064820152608401610693565b5090565b6002546040517fc2b3e5ac00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff909116600482015262061a806024820152606060448201525f60648201527342000000000000000000000000000000000000169063c2b3e5ac9083906084015f604051808303818588803b1580156108fe575f80fd5b505af1158015610910573d5f803e3d5ffd5b505050505090565b73420000000000000000000000000000000000001873ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610975573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906109999190610dfd565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146109fd576040517f7cd7e09f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600180549082905560408051828152602081018490527f895a067c78583e800418fabf3da26a9496aab2ff3429cebdf7fefa642b2e4203910161046e565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000810460ff16159067ffffffffffffffff165f81158015610a855750825b90505f8267ffffffffffffffff166001148015610aa15750303b155b905081158015610aaf575080155b15610ae6576040517ff92ee8a900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b84547fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000001660011785558315610b475784547fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff16680100000000000000001785555b6002805473ffffffffffffffffffffffffffffffffffffffff8a167fffffffffffffffffffffffff000000000000000000000000000000000000000082168117835560018a81558993927fffffffffffffffffffffff000000000000000000000000000000000000000000169091179074010000000000000000000000000000000000000000908490811115610bdf57610bdf610d31565b02179055508315610c455784547fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b5050505050505050565b5f610c5b835a84610c62565b9392505050565b5f805f805f858888f1949350505050565b803560028110610c81575f80fd5b919050565b5f60208284031215610c96575f80fd5b610c5b82610c73565b73ffffffffffffffffffffffffffffffffffffffff81168114610cc0575f80fd5b50565b5f60208284031215610cd3575f80fd5b8135610c5b81610c9f565b602081525f82518060208401528060208501604085015e5f6040828501015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011684010191505092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b60028110610d93577f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b9052565b60208101610da58284610d5e565b92915050565b5f60208284031215610dbb575f80fd5b5035919050565b5f805f60608486031215610dd4575f80fd5b8335610ddf81610c9f565b925060208401359150610df460408501610c73565b90509250925092565b5f60208284031215610e0d575f80fd5b8151610c5b81610c9f565b60408101610e268285610d5e565b610c5b6020830184610d5e565b80820180821115610da5577f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b84815273ffffffffffffffffffffffffffffffffffffffff84811660208301528316604082015260808101610ea36060830184610d5e565b9594505050505056fea164736f6c6343000819000a", +} + +// OperatorFeeVaultABI is the input ABI used to generate the binding from. +// Deprecated: Use OperatorFeeVaultMetaData.ABI instead. +var OperatorFeeVaultABI = OperatorFeeVaultMetaData.ABI + +// OperatorFeeVaultBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use OperatorFeeVaultMetaData.Bin instead. +var OperatorFeeVaultBin = OperatorFeeVaultMetaData.Bin + +// DeployOperatorFeeVault deploys a new Ethereum contract, binding an instance of OperatorFeeVault to it. +func DeployOperatorFeeVault(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *OperatorFeeVault, error) { + parsed, err := OperatorFeeVaultMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(OperatorFeeVaultBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &OperatorFeeVault{OperatorFeeVaultCaller: OperatorFeeVaultCaller{contract: contract}, OperatorFeeVaultTransactor: OperatorFeeVaultTransactor{contract: contract}, OperatorFeeVaultFilterer: OperatorFeeVaultFilterer{contract: contract}}, nil +} + +// OperatorFeeVault is an auto generated Go binding around an Ethereum contract. +type OperatorFeeVault struct { + OperatorFeeVaultCaller // Read-only binding to the contract + OperatorFeeVaultTransactor // Write-only binding to the contract + OperatorFeeVaultFilterer // Log filterer for contract events +} + +// OperatorFeeVaultCaller is an auto generated read-only Go binding around an Ethereum contract. +type OperatorFeeVaultCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// OperatorFeeVaultTransactor is an auto generated write-only Go binding around an Ethereum contract. +type OperatorFeeVaultTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// OperatorFeeVaultFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type OperatorFeeVaultFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// OperatorFeeVaultSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type OperatorFeeVaultSession struct { + Contract *OperatorFeeVault // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// OperatorFeeVaultCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type OperatorFeeVaultCallerSession struct { + Contract *OperatorFeeVaultCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// OperatorFeeVaultTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type OperatorFeeVaultTransactorSession struct { + Contract *OperatorFeeVaultTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// OperatorFeeVaultRaw is an auto generated low-level Go binding around an Ethereum contract. +type OperatorFeeVaultRaw struct { + Contract *OperatorFeeVault // Generic contract binding to access the raw methods on +} + +// OperatorFeeVaultCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type OperatorFeeVaultCallerRaw struct { + Contract *OperatorFeeVaultCaller // Generic read-only contract binding to access the raw methods on +} + +// OperatorFeeVaultTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type OperatorFeeVaultTransactorRaw struct { + Contract *OperatorFeeVaultTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewOperatorFeeVault creates a new instance of OperatorFeeVault, bound to a specific deployed contract. +func NewOperatorFeeVault(address common.Address, backend bind.ContractBackend) (*OperatorFeeVault, error) { + contract, err := bindOperatorFeeVault(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &OperatorFeeVault{OperatorFeeVaultCaller: OperatorFeeVaultCaller{contract: contract}, OperatorFeeVaultTransactor: OperatorFeeVaultTransactor{contract: contract}, OperatorFeeVaultFilterer: OperatorFeeVaultFilterer{contract: contract}}, nil +} + +// NewOperatorFeeVaultCaller creates a new read-only instance of OperatorFeeVault, bound to a specific deployed contract. +func NewOperatorFeeVaultCaller(address common.Address, caller bind.ContractCaller) (*OperatorFeeVaultCaller, error) { + contract, err := bindOperatorFeeVault(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &OperatorFeeVaultCaller{contract: contract}, nil +} + +// NewOperatorFeeVaultTransactor creates a new write-only instance of OperatorFeeVault, bound to a specific deployed contract. +func NewOperatorFeeVaultTransactor(address common.Address, transactor bind.ContractTransactor) (*OperatorFeeVaultTransactor, error) { + contract, err := bindOperatorFeeVault(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &OperatorFeeVaultTransactor{contract: contract}, nil +} + +// NewOperatorFeeVaultFilterer creates a new log filterer instance of OperatorFeeVault, bound to a specific deployed contract. +func NewOperatorFeeVaultFilterer(address common.Address, filterer bind.ContractFilterer) (*OperatorFeeVaultFilterer, error) { + contract, err := bindOperatorFeeVault(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &OperatorFeeVaultFilterer{contract: contract}, nil +} + +// bindOperatorFeeVault binds a generic wrapper to an already deployed contract. +func bindOperatorFeeVault(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := OperatorFeeVaultMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_OperatorFeeVault *OperatorFeeVaultRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _OperatorFeeVault.Contract.OperatorFeeVaultCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_OperatorFeeVault *OperatorFeeVaultRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.OperatorFeeVaultTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_OperatorFeeVault *OperatorFeeVaultRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.OperatorFeeVaultTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_OperatorFeeVault *OperatorFeeVaultCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _OperatorFeeVault.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_OperatorFeeVault *OperatorFeeVaultTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_OperatorFeeVault *OperatorFeeVaultTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.contract.Transact(opts, method, params...) +} + +// MINWITHDRAWALAMOUNT is a free data retrieval call binding the contract method 0xd3e5792b. +// +// Solidity: function MIN_WITHDRAWAL_AMOUNT() view returns(uint256) +func (_OperatorFeeVault *OperatorFeeVaultCaller) MINWITHDRAWALAMOUNT(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _OperatorFeeVault.contract.Call(opts, &out, "MIN_WITHDRAWAL_AMOUNT") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// MINWITHDRAWALAMOUNT is a free data retrieval call binding the contract method 0xd3e5792b. +// +// Solidity: function MIN_WITHDRAWAL_AMOUNT() view returns(uint256) +func (_OperatorFeeVault *OperatorFeeVaultSession) MINWITHDRAWALAMOUNT() (*big.Int, error) { + return _OperatorFeeVault.Contract.MINWITHDRAWALAMOUNT(&_OperatorFeeVault.CallOpts) +} + +// MINWITHDRAWALAMOUNT is a free data retrieval call binding the contract method 0xd3e5792b. +// +// Solidity: function MIN_WITHDRAWAL_AMOUNT() view returns(uint256) +func (_OperatorFeeVault *OperatorFeeVaultCallerSession) MINWITHDRAWALAMOUNT() (*big.Int, error) { + return _OperatorFeeVault.Contract.MINWITHDRAWALAMOUNT(&_OperatorFeeVault.CallOpts) +} + +// RECIPIENT is a free data retrieval call binding the contract method 0x0d9019e1. +// +// Solidity: function RECIPIENT() view returns(address) +func (_OperatorFeeVault *OperatorFeeVaultCaller) RECIPIENT(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _OperatorFeeVault.contract.Call(opts, &out, "RECIPIENT") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// RECIPIENT is a free data retrieval call binding the contract method 0x0d9019e1. +// +// Solidity: function RECIPIENT() view returns(address) +func (_OperatorFeeVault *OperatorFeeVaultSession) RECIPIENT() (common.Address, error) { + return _OperatorFeeVault.Contract.RECIPIENT(&_OperatorFeeVault.CallOpts) +} + +// RECIPIENT is a free data retrieval call binding the contract method 0x0d9019e1. +// +// Solidity: function RECIPIENT() view returns(address) +func (_OperatorFeeVault *OperatorFeeVaultCallerSession) RECIPIENT() (common.Address, error) { + return _OperatorFeeVault.Contract.RECIPIENT(&_OperatorFeeVault.CallOpts) +} + +// WITHDRAWALNETWORK is a free data retrieval call binding the contract method 0xd0e12f90. +// +// Solidity: function WITHDRAWAL_NETWORK() view returns(uint8) +func (_OperatorFeeVault *OperatorFeeVaultCaller) WITHDRAWALNETWORK(opts *bind.CallOpts) (uint8, error) { + var out []interface{} + err := _OperatorFeeVault.contract.Call(opts, &out, "WITHDRAWAL_NETWORK") + + if err != nil { + return *new(uint8), err + } + + out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + + return out0, err + +} + +// WITHDRAWALNETWORK is a free data retrieval call binding the contract method 0xd0e12f90. +// +// Solidity: function WITHDRAWAL_NETWORK() view returns(uint8) +func (_OperatorFeeVault *OperatorFeeVaultSession) WITHDRAWALNETWORK() (uint8, error) { + return _OperatorFeeVault.Contract.WITHDRAWALNETWORK(&_OperatorFeeVault.CallOpts) +} + +// WITHDRAWALNETWORK is a free data retrieval call binding the contract method 0xd0e12f90. +// +// Solidity: function WITHDRAWAL_NETWORK() view returns(uint8) +func (_OperatorFeeVault *OperatorFeeVaultCallerSession) WITHDRAWALNETWORK() (uint8, error) { + return _OperatorFeeVault.Contract.WITHDRAWALNETWORK(&_OperatorFeeVault.CallOpts) +} + +// MinWithdrawalAmount is a free data retrieval call binding the contract method 0x8312f149. +// +// Solidity: function minWithdrawalAmount() view returns(uint256) +func (_OperatorFeeVault *OperatorFeeVaultCaller) MinWithdrawalAmount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _OperatorFeeVault.contract.Call(opts, &out, "minWithdrawalAmount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// MinWithdrawalAmount is a free data retrieval call binding the contract method 0x8312f149. +// +// Solidity: function minWithdrawalAmount() view returns(uint256) +func (_OperatorFeeVault *OperatorFeeVaultSession) MinWithdrawalAmount() (*big.Int, error) { + return _OperatorFeeVault.Contract.MinWithdrawalAmount(&_OperatorFeeVault.CallOpts) +} + +// MinWithdrawalAmount is a free data retrieval call binding the contract method 0x8312f149. +// +// Solidity: function minWithdrawalAmount() view returns(uint256) +func (_OperatorFeeVault *OperatorFeeVaultCallerSession) MinWithdrawalAmount() (*big.Int, error) { + return _OperatorFeeVault.Contract.MinWithdrawalAmount(&_OperatorFeeVault.CallOpts) +} + +// Recipient is a free data retrieval call binding the contract method 0x66d003ac. +// +// Solidity: function recipient() view returns(address) +func (_OperatorFeeVault *OperatorFeeVaultCaller) Recipient(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _OperatorFeeVault.contract.Call(opts, &out, "recipient") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Recipient is a free data retrieval call binding the contract method 0x66d003ac. +// +// Solidity: function recipient() view returns(address) +func (_OperatorFeeVault *OperatorFeeVaultSession) Recipient() (common.Address, error) { + return _OperatorFeeVault.Contract.Recipient(&_OperatorFeeVault.CallOpts) +} + +// Recipient is a free data retrieval call binding the contract method 0x66d003ac. +// +// Solidity: function recipient() view returns(address) +func (_OperatorFeeVault *OperatorFeeVaultCallerSession) Recipient() (common.Address, error) { + return _OperatorFeeVault.Contract.Recipient(&_OperatorFeeVault.CallOpts) +} + +// TotalProcessed is a free data retrieval call binding the contract method 0x84411d65. +// +// Solidity: function totalProcessed() view returns(uint256) +func (_OperatorFeeVault *OperatorFeeVaultCaller) TotalProcessed(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _OperatorFeeVault.contract.Call(opts, &out, "totalProcessed") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// TotalProcessed is a free data retrieval call binding the contract method 0x84411d65. +// +// Solidity: function totalProcessed() view returns(uint256) +func (_OperatorFeeVault *OperatorFeeVaultSession) TotalProcessed() (*big.Int, error) { + return _OperatorFeeVault.Contract.TotalProcessed(&_OperatorFeeVault.CallOpts) +} + +// TotalProcessed is a free data retrieval call binding the contract method 0x84411d65. +// +// Solidity: function totalProcessed() view returns(uint256) +func (_OperatorFeeVault *OperatorFeeVaultCallerSession) TotalProcessed() (*big.Int, error) { + return _OperatorFeeVault.Contract.TotalProcessed(&_OperatorFeeVault.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_OperatorFeeVault *OperatorFeeVaultCaller) Version(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _OperatorFeeVault.contract.Call(opts, &out, "version") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_OperatorFeeVault *OperatorFeeVaultSession) Version() (string, error) { + return _OperatorFeeVault.Contract.Version(&_OperatorFeeVault.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_OperatorFeeVault *OperatorFeeVaultCallerSession) Version() (string, error) { + return _OperatorFeeVault.Contract.Version(&_OperatorFeeVault.CallOpts) +} + +// WithdrawalNetwork is a free data retrieval call binding the contract method 0x82356d8a. +// +// Solidity: function withdrawalNetwork() view returns(uint8) +func (_OperatorFeeVault *OperatorFeeVaultCaller) WithdrawalNetwork(opts *bind.CallOpts) (uint8, error) { + var out []interface{} + err := _OperatorFeeVault.contract.Call(opts, &out, "withdrawalNetwork") + + if err != nil { + return *new(uint8), err + } + + out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + + return out0, err + +} + +// WithdrawalNetwork is a free data retrieval call binding the contract method 0x82356d8a. +// +// Solidity: function withdrawalNetwork() view returns(uint8) +func (_OperatorFeeVault *OperatorFeeVaultSession) WithdrawalNetwork() (uint8, error) { + return _OperatorFeeVault.Contract.WithdrawalNetwork(&_OperatorFeeVault.CallOpts) +} + +// WithdrawalNetwork is a free data retrieval call binding the contract method 0x82356d8a. +// +// Solidity: function withdrawalNetwork() view returns(uint8) +func (_OperatorFeeVault *OperatorFeeVaultCallerSession) WithdrawalNetwork() (uint8, error) { + return _OperatorFeeVault.Contract.WithdrawalNetwork(&_OperatorFeeVault.CallOpts) +} + +// Initialize is a paid mutator transaction binding the contract method 0xb49dc741. +// +// Solidity: function initialize(address _recipient, uint256 _minWithdrawalAmount, uint8 _withdrawalNetwork) returns() +func (_OperatorFeeVault *OperatorFeeVaultTransactor) Initialize(opts *bind.TransactOpts, _recipient common.Address, _minWithdrawalAmount *big.Int, _withdrawalNetwork uint8) (*types.Transaction, error) { + return _OperatorFeeVault.contract.Transact(opts, "initialize", _recipient, _minWithdrawalAmount, _withdrawalNetwork) +} + +// Initialize is a paid mutator transaction binding the contract method 0xb49dc741. +// +// Solidity: function initialize(address _recipient, uint256 _minWithdrawalAmount, uint8 _withdrawalNetwork) returns() +func (_OperatorFeeVault *OperatorFeeVaultSession) Initialize(_recipient common.Address, _minWithdrawalAmount *big.Int, _withdrawalNetwork uint8) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.Initialize(&_OperatorFeeVault.TransactOpts, _recipient, _minWithdrawalAmount, _withdrawalNetwork) +} + +// Initialize is a paid mutator transaction binding the contract method 0xb49dc741. +// +// Solidity: function initialize(address _recipient, uint256 _minWithdrawalAmount, uint8 _withdrawalNetwork) returns() +func (_OperatorFeeVault *OperatorFeeVaultTransactorSession) Initialize(_recipient common.Address, _minWithdrawalAmount *big.Int, _withdrawalNetwork uint8) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.Initialize(&_OperatorFeeVault.TransactOpts, _recipient, _minWithdrawalAmount, _withdrawalNetwork) +} + +// SetMinWithdrawalAmount is a paid mutator transaction binding the contract method 0x85b5b14d. +// +// Solidity: function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) returns() +func (_OperatorFeeVault *OperatorFeeVaultTransactor) SetMinWithdrawalAmount(opts *bind.TransactOpts, _newMinWithdrawalAmount *big.Int) (*types.Transaction, error) { + return _OperatorFeeVault.contract.Transact(opts, "setMinWithdrawalAmount", _newMinWithdrawalAmount) +} + +// SetMinWithdrawalAmount is a paid mutator transaction binding the contract method 0x85b5b14d. +// +// Solidity: function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) returns() +func (_OperatorFeeVault *OperatorFeeVaultSession) SetMinWithdrawalAmount(_newMinWithdrawalAmount *big.Int) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.SetMinWithdrawalAmount(&_OperatorFeeVault.TransactOpts, _newMinWithdrawalAmount) +} + +// SetMinWithdrawalAmount is a paid mutator transaction binding the contract method 0x85b5b14d. +// +// Solidity: function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) returns() +func (_OperatorFeeVault *OperatorFeeVaultTransactorSession) SetMinWithdrawalAmount(_newMinWithdrawalAmount *big.Int) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.SetMinWithdrawalAmount(&_OperatorFeeVault.TransactOpts, _newMinWithdrawalAmount) +} + +// SetRecipient is a paid mutator transaction binding the contract method 0x3bbed4a0. +// +// Solidity: function setRecipient(address _newRecipient) returns() +func (_OperatorFeeVault *OperatorFeeVaultTransactor) SetRecipient(opts *bind.TransactOpts, _newRecipient common.Address) (*types.Transaction, error) { + return _OperatorFeeVault.contract.Transact(opts, "setRecipient", _newRecipient) +} + +// SetRecipient is a paid mutator transaction binding the contract method 0x3bbed4a0. +// +// Solidity: function setRecipient(address _newRecipient) returns() +func (_OperatorFeeVault *OperatorFeeVaultSession) SetRecipient(_newRecipient common.Address) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.SetRecipient(&_OperatorFeeVault.TransactOpts, _newRecipient) +} + +// SetRecipient is a paid mutator transaction binding the contract method 0x3bbed4a0. +// +// Solidity: function setRecipient(address _newRecipient) returns() +func (_OperatorFeeVault *OperatorFeeVaultTransactorSession) SetRecipient(_newRecipient common.Address) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.SetRecipient(&_OperatorFeeVault.TransactOpts, _newRecipient) +} + +// SetWithdrawalNetwork is a paid mutator transaction binding the contract method 0x307f2962. +// +// Solidity: function setWithdrawalNetwork(uint8 _newWithdrawalNetwork) returns() +func (_OperatorFeeVault *OperatorFeeVaultTransactor) SetWithdrawalNetwork(opts *bind.TransactOpts, _newWithdrawalNetwork uint8) (*types.Transaction, error) { + return _OperatorFeeVault.contract.Transact(opts, "setWithdrawalNetwork", _newWithdrawalNetwork) +} + +// SetWithdrawalNetwork is a paid mutator transaction binding the contract method 0x307f2962. +// +// Solidity: function setWithdrawalNetwork(uint8 _newWithdrawalNetwork) returns() +func (_OperatorFeeVault *OperatorFeeVaultSession) SetWithdrawalNetwork(_newWithdrawalNetwork uint8) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.SetWithdrawalNetwork(&_OperatorFeeVault.TransactOpts, _newWithdrawalNetwork) +} + +// SetWithdrawalNetwork is a paid mutator transaction binding the contract method 0x307f2962. +// +// Solidity: function setWithdrawalNetwork(uint8 _newWithdrawalNetwork) returns() +func (_OperatorFeeVault *OperatorFeeVaultTransactorSession) SetWithdrawalNetwork(_newWithdrawalNetwork uint8) (*types.Transaction, error) { + return _OperatorFeeVault.Contract.SetWithdrawalNetwork(&_OperatorFeeVault.TransactOpts, _newWithdrawalNetwork) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x3ccfd60b. +// +// Solidity: function withdraw() returns(uint256 value_) +func (_OperatorFeeVault *OperatorFeeVaultTransactor) Withdraw(opts *bind.TransactOpts) (*types.Transaction, error) { + return _OperatorFeeVault.contract.Transact(opts, "withdraw") +} + +// Withdraw is a paid mutator transaction binding the contract method 0x3ccfd60b. +// +// Solidity: function withdraw() returns(uint256 value_) +func (_OperatorFeeVault *OperatorFeeVaultSession) Withdraw() (*types.Transaction, error) { + return _OperatorFeeVault.Contract.Withdraw(&_OperatorFeeVault.TransactOpts) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x3ccfd60b. +// +// Solidity: function withdraw() returns(uint256 value_) +func (_OperatorFeeVault *OperatorFeeVaultTransactorSession) Withdraw() (*types.Transaction, error) { + return _OperatorFeeVault.Contract.Withdraw(&_OperatorFeeVault.TransactOpts) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_OperatorFeeVault *OperatorFeeVaultTransactor) Receive(opts *bind.TransactOpts) (*types.Transaction, error) { + return _OperatorFeeVault.contract.RawTransact(opts, nil) // calldata is disallowed for receive function +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_OperatorFeeVault *OperatorFeeVaultSession) Receive() (*types.Transaction, error) { + return _OperatorFeeVault.Contract.Receive(&_OperatorFeeVault.TransactOpts) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_OperatorFeeVault *OperatorFeeVaultTransactorSession) Receive() (*types.Transaction, error) { + return _OperatorFeeVault.Contract.Receive(&_OperatorFeeVault.TransactOpts) +} + +// OperatorFeeVaultInitializedIterator is returned from FilterInitialized and is used to iterate over the raw logs and unpacked data for Initialized events raised by the OperatorFeeVault contract. +type OperatorFeeVaultInitializedIterator struct { + Event *OperatorFeeVaultInitialized // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *OperatorFeeVaultInitializedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultInitialized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultInitialized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *OperatorFeeVaultInitializedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *OperatorFeeVaultInitializedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// OperatorFeeVaultInitialized represents a Initialized event raised by the OperatorFeeVault contract. +type OperatorFeeVaultInitialized struct { + Version uint64 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInitialized is a free log retrieval operation binding the contract event 0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2. +// +// Solidity: event Initialized(uint64 version) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) FilterInitialized(opts *bind.FilterOpts) (*OperatorFeeVaultInitializedIterator, error) { + + logs, sub, err := _OperatorFeeVault.contract.FilterLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return &OperatorFeeVaultInitializedIterator{contract: _OperatorFeeVault.contract, event: "Initialized", logs: logs, sub: sub}, nil +} + +// WatchInitialized is a free log subscription operation binding the contract event 0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2. +// +// Solidity: event Initialized(uint64 version) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) WatchInitialized(opts *bind.WatchOpts, sink chan<- *OperatorFeeVaultInitialized) (event.Subscription, error) { + + logs, sub, err := _OperatorFeeVault.contract.WatchLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(OperatorFeeVaultInitialized) + if err := _OperatorFeeVault.contract.UnpackLog(event, "Initialized", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseInitialized is a log parse operation binding the contract event 0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2. +// +// Solidity: event Initialized(uint64 version) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) ParseInitialized(log types.Log) (*OperatorFeeVaultInitialized, error) { + event := new(OperatorFeeVaultInitialized) + if err := _OperatorFeeVault.contract.UnpackLog(event, "Initialized", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// OperatorFeeVaultMinWithdrawalAmountUpdatedIterator is returned from FilterMinWithdrawalAmountUpdated and is used to iterate over the raw logs and unpacked data for MinWithdrawalAmountUpdated events raised by the OperatorFeeVault contract. +type OperatorFeeVaultMinWithdrawalAmountUpdatedIterator struct { + Event *OperatorFeeVaultMinWithdrawalAmountUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *OperatorFeeVaultMinWithdrawalAmountUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultMinWithdrawalAmountUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultMinWithdrawalAmountUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *OperatorFeeVaultMinWithdrawalAmountUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *OperatorFeeVaultMinWithdrawalAmountUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// OperatorFeeVaultMinWithdrawalAmountUpdated represents a MinWithdrawalAmountUpdated event raised by the OperatorFeeVault contract. +type OperatorFeeVaultMinWithdrawalAmountUpdated struct { + OldWithdrawalAmount *big.Int + NewWithdrawalAmount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterMinWithdrawalAmountUpdated is a free log retrieval operation binding the contract event 0x895a067c78583e800418fabf3da26a9496aab2ff3429cebdf7fefa642b2e4203. +// +// Solidity: event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) FilterMinWithdrawalAmountUpdated(opts *bind.FilterOpts) (*OperatorFeeVaultMinWithdrawalAmountUpdatedIterator, error) { + + logs, sub, err := _OperatorFeeVault.contract.FilterLogs(opts, "MinWithdrawalAmountUpdated") + if err != nil { + return nil, err + } + return &OperatorFeeVaultMinWithdrawalAmountUpdatedIterator{contract: _OperatorFeeVault.contract, event: "MinWithdrawalAmountUpdated", logs: logs, sub: sub}, nil +} + +// WatchMinWithdrawalAmountUpdated is a free log subscription operation binding the contract event 0x895a067c78583e800418fabf3da26a9496aab2ff3429cebdf7fefa642b2e4203. +// +// Solidity: event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) WatchMinWithdrawalAmountUpdated(opts *bind.WatchOpts, sink chan<- *OperatorFeeVaultMinWithdrawalAmountUpdated) (event.Subscription, error) { + + logs, sub, err := _OperatorFeeVault.contract.WatchLogs(opts, "MinWithdrawalAmountUpdated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(OperatorFeeVaultMinWithdrawalAmountUpdated) + if err := _OperatorFeeVault.contract.UnpackLog(event, "MinWithdrawalAmountUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseMinWithdrawalAmountUpdated is a log parse operation binding the contract event 0x895a067c78583e800418fabf3da26a9496aab2ff3429cebdf7fefa642b2e4203. +// +// Solidity: event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) ParseMinWithdrawalAmountUpdated(log types.Log) (*OperatorFeeVaultMinWithdrawalAmountUpdated, error) { + event := new(OperatorFeeVaultMinWithdrawalAmountUpdated) + if err := _OperatorFeeVault.contract.UnpackLog(event, "MinWithdrawalAmountUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// OperatorFeeVaultRecipientUpdatedIterator is returned from FilterRecipientUpdated and is used to iterate over the raw logs and unpacked data for RecipientUpdated events raised by the OperatorFeeVault contract. +type OperatorFeeVaultRecipientUpdatedIterator struct { + Event *OperatorFeeVaultRecipientUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *OperatorFeeVaultRecipientUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultRecipientUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultRecipientUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *OperatorFeeVaultRecipientUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *OperatorFeeVaultRecipientUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// OperatorFeeVaultRecipientUpdated represents a RecipientUpdated event raised by the OperatorFeeVault contract. +type OperatorFeeVaultRecipientUpdated struct { + OldRecipient common.Address + NewRecipient common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRecipientUpdated is a free log retrieval operation binding the contract event 0x62e69886a5df0ba8ffcacbfc1388754e7abd9bde24b036354c561f1acd4e4593. +// +// Solidity: event RecipientUpdated(address oldRecipient, address newRecipient) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) FilterRecipientUpdated(opts *bind.FilterOpts) (*OperatorFeeVaultRecipientUpdatedIterator, error) { + + logs, sub, err := _OperatorFeeVault.contract.FilterLogs(opts, "RecipientUpdated") + if err != nil { + return nil, err + } + return &OperatorFeeVaultRecipientUpdatedIterator{contract: _OperatorFeeVault.contract, event: "RecipientUpdated", logs: logs, sub: sub}, nil +} + +// WatchRecipientUpdated is a free log subscription operation binding the contract event 0x62e69886a5df0ba8ffcacbfc1388754e7abd9bde24b036354c561f1acd4e4593. +// +// Solidity: event RecipientUpdated(address oldRecipient, address newRecipient) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) WatchRecipientUpdated(opts *bind.WatchOpts, sink chan<- *OperatorFeeVaultRecipientUpdated) (event.Subscription, error) { + + logs, sub, err := _OperatorFeeVault.contract.WatchLogs(opts, "RecipientUpdated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(OperatorFeeVaultRecipientUpdated) + if err := _OperatorFeeVault.contract.UnpackLog(event, "RecipientUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseRecipientUpdated is a log parse operation binding the contract event 0x62e69886a5df0ba8ffcacbfc1388754e7abd9bde24b036354c561f1acd4e4593. +// +// Solidity: event RecipientUpdated(address oldRecipient, address newRecipient) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) ParseRecipientUpdated(log types.Log) (*OperatorFeeVaultRecipientUpdated, error) { + event := new(OperatorFeeVaultRecipientUpdated) + if err := _OperatorFeeVault.contract.UnpackLog(event, "RecipientUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// OperatorFeeVaultWithdrawalIterator is returned from FilterWithdrawal and is used to iterate over the raw logs and unpacked data for Withdrawal events raised by the OperatorFeeVault contract. +type OperatorFeeVaultWithdrawalIterator struct { + Event *OperatorFeeVaultWithdrawal // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *OperatorFeeVaultWithdrawalIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultWithdrawal) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultWithdrawal) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *OperatorFeeVaultWithdrawalIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *OperatorFeeVaultWithdrawalIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// OperatorFeeVaultWithdrawal represents a Withdrawal event raised by the OperatorFeeVault contract. +type OperatorFeeVaultWithdrawal struct { + Value *big.Int + To common.Address + From common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterWithdrawal is a free log retrieval operation binding the contract event 0xc8a211cc64b6ed1b50595a9fcb1932b6d1e5a6e8ef15b60e5b1f988ea9086bba. +// +// Solidity: event Withdrawal(uint256 value, address to, address from) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) FilterWithdrawal(opts *bind.FilterOpts) (*OperatorFeeVaultWithdrawalIterator, error) { + + logs, sub, err := _OperatorFeeVault.contract.FilterLogs(opts, "Withdrawal") + if err != nil { + return nil, err + } + return &OperatorFeeVaultWithdrawalIterator{contract: _OperatorFeeVault.contract, event: "Withdrawal", logs: logs, sub: sub}, nil +} + +// WatchWithdrawal is a free log subscription operation binding the contract event 0xc8a211cc64b6ed1b50595a9fcb1932b6d1e5a6e8ef15b60e5b1f988ea9086bba. +// +// Solidity: event Withdrawal(uint256 value, address to, address from) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) WatchWithdrawal(opts *bind.WatchOpts, sink chan<- *OperatorFeeVaultWithdrawal) (event.Subscription, error) { + + logs, sub, err := _OperatorFeeVault.contract.WatchLogs(opts, "Withdrawal") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(OperatorFeeVaultWithdrawal) + if err := _OperatorFeeVault.contract.UnpackLog(event, "Withdrawal", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseWithdrawal is a log parse operation binding the contract event 0xc8a211cc64b6ed1b50595a9fcb1932b6d1e5a6e8ef15b60e5b1f988ea9086bba. +// +// Solidity: event Withdrawal(uint256 value, address to, address from) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) ParseWithdrawal(log types.Log) (*OperatorFeeVaultWithdrawal, error) { + event := new(OperatorFeeVaultWithdrawal) + if err := _OperatorFeeVault.contract.UnpackLog(event, "Withdrawal", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// OperatorFeeVaultWithdrawal0Iterator is returned from FilterWithdrawal0 and is used to iterate over the raw logs and unpacked data for Withdrawal0 events raised by the OperatorFeeVault contract. +type OperatorFeeVaultWithdrawal0Iterator struct { + Event *OperatorFeeVaultWithdrawal0 // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *OperatorFeeVaultWithdrawal0Iterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultWithdrawal0) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultWithdrawal0) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *OperatorFeeVaultWithdrawal0Iterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *OperatorFeeVaultWithdrawal0Iterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// OperatorFeeVaultWithdrawal0 represents a Withdrawal0 event raised by the OperatorFeeVault contract. +type OperatorFeeVaultWithdrawal0 struct { + Value *big.Int + To common.Address + From common.Address + WithdrawalNetwork uint8 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterWithdrawal0 is a free log retrieval operation binding the contract event 0x38e04cbeb8c10f8f568618aa75be0f10b6729b8b4237743b4de20cbcde2839ee. +// +// Solidity: event Withdrawal(uint256 value, address to, address from, uint8 withdrawalNetwork) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) FilterWithdrawal0(opts *bind.FilterOpts) (*OperatorFeeVaultWithdrawal0Iterator, error) { + + logs, sub, err := _OperatorFeeVault.contract.FilterLogs(opts, "Withdrawal0") + if err != nil { + return nil, err + } + return &OperatorFeeVaultWithdrawal0Iterator{contract: _OperatorFeeVault.contract, event: "Withdrawal0", logs: logs, sub: sub}, nil +} + +// WatchWithdrawal0 is a free log subscription operation binding the contract event 0x38e04cbeb8c10f8f568618aa75be0f10b6729b8b4237743b4de20cbcde2839ee. +// +// Solidity: event Withdrawal(uint256 value, address to, address from, uint8 withdrawalNetwork) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) WatchWithdrawal0(opts *bind.WatchOpts, sink chan<- *OperatorFeeVaultWithdrawal0) (event.Subscription, error) { + + logs, sub, err := _OperatorFeeVault.contract.WatchLogs(opts, "Withdrawal0") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(OperatorFeeVaultWithdrawal0) + if err := _OperatorFeeVault.contract.UnpackLog(event, "Withdrawal0", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseWithdrawal0 is a log parse operation binding the contract event 0x38e04cbeb8c10f8f568618aa75be0f10b6729b8b4237743b4de20cbcde2839ee. +// +// Solidity: event Withdrawal(uint256 value, address to, address from, uint8 withdrawalNetwork) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) ParseWithdrawal0(log types.Log) (*OperatorFeeVaultWithdrawal0, error) { + event := new(OperatorFeeVaultWithdrawal0) + if err := _OperatorFeeVault.contract.UnpackLog(event, "Withdrawal0", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// OperatorFeeVaultWithdrawalNetworkUpdatedIterator is returned from FilterWithdrawalNetworkUpdated and is used to iterate over the raw logs and unpacked data for WithdrawalNetworkUpdated events raised by the OperatorFeeVault contract. +type OperatorFeeVaultWithdrawalNetworkUpdatedIterator struct { + Event *OperatorFeeVaultWithdrawalNetworkUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *OperatorFeeVaultWithdrawalNetworkUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultWithdrawalNetworkUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(OperatorFeeVaultWithdrawalNetworkUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *OperatorFeeVaultWithdrawalNetworkUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *OperatorFeeVaultWithdrawalNetworkUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// OperatorFeeVaultWithdrawalNetworkUpdated represents a WithdrawalNetworkUpdated event raised by the OperatorFeeVault contract. +type OperatorFeeVaultWithdrawalNetworkUpdated struct { + OldWithdrawalNetwork uint8 + NewWithdrawalNetwork uint8 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterWithdrawalNetworkUpdated is a free log retrieval operation binding the contract event 0xf2ec44eb1c3b3acd547b76333eb2c4b27eee311860c57a9fdb04c95f62398fc8. +// +// Solidity: event WithdrawalNetworkUpdated(uint8 oldWithdrawalNetwork, uint8 newWithdrawalNetwork) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) FilterWithdrawalNetworkUpdated(opts *bind.FilterOpts) (*OperatorFeeVaultWithdrawalNetworkUpdatedIterator, error) { + + logs, sub, err := _OperatorFeeVault.contract.FilterLogs(opts, "WithdrawalNetworkUpdated") + if err != nil { + return nil, err + } + return &OperatorFeeVaultWithdrawalNetworkUpdatedIterator{contract: _OperatorFeeVault.contract, event: "WithdrawalNetworkUpdated", logs: logs, sub: sub}, nil +} + +// WatchWithdrawalNetworkUpdated is a free log subscription operation binding the contract event 0xf2ec44eb1c3b3acd547b76333eb2c4b27eee311860c57a9fdb04c95f62398fc8. +// +// Solidity: event WithdrawalNetworkUpdated(uint8 oldWithdrawalNetwork, uint8 newWithdrawalNetwork) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) WatchWithdrawalNetworkUpdated(opts *bind.WatchOpts, sink chan<- *OperatorFeeVaultWithdrawalNetworkUpdated) (event.Subscription, error) { + + logs, sub, err := _OperatorFeeVault.contract.WatchLogs(opts, "WithdrawalNetworkUpdated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(OperatorFeeVaultWithdrawalNetworkUpdated) + if err := _OperatorFeeVault.contract.UnpackLog(event, "WithdrawalNetworkUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseWithdrawalNetworkUpdated is a log parse operation binding the contract event 0xf2ec44eb1c3b3acd547b76333eb2c4b27eee311860c57a9fdb04c95f62398fc8. +// +// Solidity: event WithdrawalNetworkUpdated(uint8 oldWithdrawalNetwork, uint8 newWithdrawalNetwork) +func (_OperatorFeeVault *OperatorFeeVaultFilterer) ParseWithdrawalNetworkUpdated(log types.Log) (*OperatorFeeVaultWithdrawalNetworkUpdated, error) { + event := new(OperatorFeeVaultWithdrawalNetworkUpdated) + if err := _OperatorFeeVault.contract.UnpackLog(event, "WithdrawalNetworkUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/op-e2e/config/init.go b/op-e2e/config/init.go index 48867df80a642..d06a243a4c0c5 100644 --- a/op-e2e/config/init.go +++ b/op-e2e/config/init.go @@ -355,9 +355,11 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, "baseFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", + "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "baseFeeVaultWithdrawalNetwork": 0, "l1FeeVaultWithdrawalNetwork": 0, "sequencerFeeVaultWithdrawalNetwork": 0, + "operatorFeeVaultWithdrawalNetwork": 0, "finalizationPeriodSeconds": 2, "l2GenesisBlockBaseFeePerGas": "0x1", "gasPriceOracleOverhead": 2100, @@ -387,6 +389,7 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, BaseFeeVaultRecipient: common.HexToAddress("0x14dC79964da2C08b23698B3D3cc7Ca32193d9955"), L1FeeVaultRecipient: common.HexToAddress("0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f"), SequencerFeeVaultRecipient: common.HexToAddress("0xa0Ee7A142d267C1f36714E4a8F75612F20a79720"), + OperatorFeeVaultRecipient: common.HexToAddress("0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec"), Eip1559Denominator: 250, Eip1559DenominatorCanyon: 250, Eip1559Elasticity: 6, @@ -401,6 +404,8 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, Proposer: addrs.Proposer, Challenger: common.HexToAddress("0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65"), }, + UseRevenueShare: true, + ChainFeesRecipient: common.HexToAddress("0xBcd4042DE499D14e55001CcbB24a551F3b954096"), AdditionalDisputeGames: []state.AdditionalDisputeGame{ { ChainProofParams: state.ChainProofParams{ diff --git a/op-e2e/e2eutils/intentbuilder/builder.go b/op-e2e/e2eutils/intentbuilder/builder.go index 8a8ac2d54e55c..c28dd86ffe798 100644 --- a/op-e2e/e2eutils/intentbuilder/builder.go +++ b/op-e2e/e2eutils/intentbuilder/builder.go @@ -52,6 +52,7 @@ type L2Configurator interface { WithL1StartBlockHash(hash common.Hash) WithAdditionalDisputeGames(games []state.AdditionalDisputeGame) WithFinalizationPeriodSeconds(value uint64) + WithRevenueShare(enabled bool, chainFeesRecipient common.Address) ContractsConfigurator L2VaultsConfigurator L2RolesConfigurator @@ -70,6 +71,7 @@ type L2VaultsConfigurator interface { WithBaseFeeVaultRecipient(address common.Address) WithSequencerFeeVaultRecipient(address common.Address) WithL1FeeVaultRecipient(address common.Address) + WithOperatorFeeVaultRecipient(address common.Address) } type L2RolesConfigurator interface { @@ -114,6 +116,7 @@ func WithDevkeyVaults(t require.TestingT, dk devkeys.Keys, configurator L2Config configurator.WithBaseFeeVaultRecipient(addrFor(devkeys.BaseFeeVaultRecipientRole)) configurator.WithSequencerFeeVaultRecipient(addrFor(devkeys.SequencerFeeVaultRecipientRole)) configurator.WithL1FeeVaultRecipient(addrFor(devkeys.L1FeeVaultRecipientRole)) + configurator.WithOperatorFeeVaultRecipient(addrFor(devkeys.OperatorFeeVaultRecipientRole)) } func WithDevkeyL2Roles(t require.TestingT, dk devkeys.Keys, configurator L2Configurator) { @@ -379,6 +382,10 @@ func (c *l2Configurator) WithL1FeeVaultRecipient(address common.Address) { c.builder.intent.Chains[c.chainIndex].L1FeeVaultRecipient = address } +func (c *l2Configurator) WithOperatorFeeVaultRecipient(address common.Address) { + c.builder.intent.Chains[c.chainIndex].OperatorFeeVaultRecipient = address +} + func (c *l2Configurator) WithL1ProxyAdminOwner(address common.Address) { c.builder.intent.Chains[c.chainIndex].Roles.L1ProxyAdminOwner = address } @@ -462,6 +469,11 @@ func (c *l2Configurator) WithForkAtOffset(fork forks.Name, offset *uint64) { } } +func (c *l2Configurator) WithRevenueShare(enabled bool, chainFeesRecipient common.Address) { + c.builder.intent.Chains[c.chainIndex].UseRevenueShare = enabled + c.builder.intent.Chains[c.chainIndex].ChainFeesRecipient = chainFeesRecipient +} + func (c *l2Configurator) initL2DevGenesisParams() *state.L2DevGenesisParams { chainIntent := c.builder.intent.Chains[c.chainIndex] if chainIntent.L2DevGenesisParams == nil { diff --git a/op-e2e/e2eutils/intentbuilder/builder_test.go b/op-e2e/e2eutils/intentbuilder/builder_test.go index 661091adc6351..f959b5de150c9 100644 --- a/op-e2e/e2eutils/intentbuilder/builder_test.go +++ b/op-e2e/e2eutils/intentbuilder/builder_test.go @@ -80,13 +80,18 @@ func TestBuilder(t *testing.T) { l2Config.WithL1ContractsLocator("http://l1.example.com") l2Config.WithL2ContractsLocator("http://l2.example.com") + // Test RevenueShareConfigurator methods + l2Config.WithRevenueShare(true, common.HexToAddress("0x4444")) + // Test L2VaultsConfigurator methods baseFeeRecipient := common.HexToAddress("0x1111") sequencerFeeRecipient := common.HexToAddress("0x2222") l1FeeRecipient := common.HexToAddress("0x3333") + operatorFeeRecipient := common.HexToAddress("0x4444") l2Config.WithBaseFeeVaultRecipient(baseFeeRecipient) l2Config.WithSequencerFeeVaultRecipient(sequencerFeeRecipient) l2Config.WithL1FeeVaultRecipient(l1FeeRecipient) + l2Config.WithOperatorFeeVaultRecipient(operatorFeeRecipient) // Test L2RolesConfigurator methods l1ProxyAdminOwner := common.HexToAddress("0x4444") @@ -153,6 +158,7 @@ func TestBuilder(t *testing.T) { BaseFeeVaultRecipient: baseFeeRecipient, SequencerFeeVaultRecipient: sequencerFeeRecipient, L1FeeVaultRecipient: l1FeeRecipient, + OperatorFeeVaultRecipient: operatorFeeRecipient, DAFootprintGasScalar: 400, Roles: state.ChainRoles{ L1ProxyAdminOwner: l1ProxyAdminOwner, @@ -185,6 +191,8 @@ func TestBuilder(t *testing.T) { bob: (*hexutil.U256)(bobFunds), }, }, + UseRevenueShare: true, + ChainFeesRecipient: common.HexToAddress("0x4444"), }, }, } diff --git a/packages/contracts-bedrock/book/src/contributing/style-guide.md b/packages/contracts-bedrock/book/src/contributing/style-guide.md index a8686aa023ffd..4a5981b24990f 100644 --- a/packages/contracts-bedrock/book/src/contributing/style-guide.md +++ b/packages/contracts-bedrock/book/src/contributing/style-guide.md @@ -389,9 +389,4 @@ Certain types of tests are excluded from standard naming conventions: - **Script tests** (`test/scripts/`): Test deployment and utility scripts - **Library tests** (`test/libraries/`): May have different artifact structures - **Formal verification** (`test/kontrol/`): Use specialized tooling conventions -- **Vendor tests** (`test/vendor/`): Test external code with different patterns - -## Withdrawing From Fee Vaults - -See the file `scripts/FeeVaultWithdrawal.s.sol` to withdraw from the L2 fee vaults. It includes -instructions on how to run it. `foundry` is required. +- **Vendor tests** (`test/vendor/`): Test external code with different patterns \ No newline at end of file diff --git a/packages/contracts-bedrock/deploy-config/hardhat.json b/packages/contracts-bedrock/deploy-config/hardhat.json index 736991c93ba26..3dc3be5e48ef3 100644 --- a/packages/contracts-bedrock/deploy-config/hardhat.json +++ b/packages/contracts-bedrock/deploy-config/hardhat.json @@ -25,12 +25,15 @@ "baseFeeVaultRecipient": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", "l1FeeVaultRecipient": "0x71bE63f3384f5fb98995898A86B02Fb2426c5788", "sequencerFeeVaultRecipient": "0xfabb0ac9d68b0b445fb7357272ff202c5651694a", + "operatorFeeVaultRecipient": "0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec", + "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "baseFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "baseFeeVaultWithdrawalNetwork": 0, "l1FeeVaultWithdrawalNetwork": 0, "sequencerFeeVaultWithdrawalNetwork": 0, + "operatorFeeVaultWithdrawalNetwork": 0, "enableGovernance": true, "governanceTokenName": "Optimism", "governanceTokenSymbol": "OP", @@ -62,5 +65,7 @@ "daChallengeWindow": 100, "daResolveWindow": 100, "daBondSize": 1000, - "daResolverRefundPercentage": 50 + "daResolverRefundPercentage": 50, + "useRevenueShare": false, + "chainFeesRecipient": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc" } diff --git a/packages/contracts-bedrock/deploy-config/internal-devnet.json b/packages/contracts-bedrock/deploy-config/internal-devnet.json index 643c507dc86d5..b5fe59b972381 100644 --- a/packages/contracts-bedrock/deploy-config/internal-devnet.json +++ b/packages/contracts-bedrock/deploy-config/internal-devnet.json @@ -22,12 +22,15 @@ "baseFeeVaultRecipient": "0x858F0751ef8B4067f0d2668C076BDB50a8549fbF", "l1FeeVaultRecipient": "0x858F0751ef8B4067f0d2668C076BDB50a8549fbF", "sequencerFeeVaultRecipient": "0x858F0751ef8B4067f0d2668C076BDB50a8549fbF", + "operatorFeeVaultRecipient": "0x858F0751ef8B4067f0d2668C076BDB50a8549fbF", "baseFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", + "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "baseFeeVaultWithdrawalNetwork": 0, "l1FeeVaultWithdrawalNetwork": 0, "sequencerFeeVaultWithdrawalNetwork": 0, + "operatorFeeVaultWithdrawalNetwork": 0, "enableGovernance": true, "governanceTokenName": "Optimism", "governanceTokenSymbol": "OP", @@ -38,5 +41,7 @@ "eip1559Elasticity": 10, "systemConfigStartBlock": 8364212, "requiredProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", - "recommendedProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000" + "recommendedProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", + "useRevenueShare": true, + "chainFeesRecipient": "0x858F0751ef8B4067f0d2668C076BDB50a8549fbF" } diff --git a/packages/contracts-bedrock/deploy-config/mainnet.json b/packages/contracts-bedrock/deploy-config/mainnet.json index cd217ba111538..eb87403014da6 100644 --- a/packages/contracts-bedrock/deploy-config/mainnet.json +++ b/packages/contracts-bedrock/deploy-config/mainnet.json @@ -22,12 +22,15 @@ "baseFeeVaultRecipient": "0xa3d596EAfaB6B13Ab18D40FaE1A962700C84ADEa", "l1FeeVaultRecipient": "0xa3d596EAfaB6B13Ab18D40FaE1A962700C84ADEa", "sequencerFeeVaultRecipient": "0xa3d596EAfaB6B13Ab18D40FaE1A962700C84ADEa", + "operatorFeeVaultRecipient": "0xa3d596EAfaB6B13Ab18D40FaE1A962700C84ADEa", "baseFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", + "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "baseFeeVaultWithdrawalNetwork": 0, "l1FeeVaultWithdrawalNetwork": 0, "sequencerFeeVaultWithdrawalNetwork": 0, + "operatorFeeVaultWithdrawalNetwork": 0, "enableGovernance": true, "governanceTokenName": "Optimism", "governanceTokenSymbol": "OP", @@ -55,5 +58,7 @@ "proofMaturityDelaySeconds": 604800, "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, - "useFaultProofs": true + "useFaultProofs": true, + "useRevenueShare": true, + "chainFeesRecipient": "0xa3d596EAfaB6B13Ab18D40FaE1A962700C84ADEa" } diff --git a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json index 2392dcb9281a5..baf6b1f323054 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json +++ b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json @@ -43,12 +43,15 @@ "baseFeeVaultRecipient": "0x8c20c40180751d93e939dddee3517ae0d1ebead2", "l1FeeVaultRecipient": "0x8c20c40180751d93e939dddee3517ae0d1ebead2", "sequencerFeeVaultRecipient": "0x8c20c40180751d93e939dddee3517ae0d1ebead2", + "operatorFeeVaultRecipient": "0x8c20c40180751d93e939dddee3517ae0d1ebead2", "baseFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", + "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "baseFeeVaultWithdrawalNetwork": 1, "l1FeeVaultWithdrawalNetwork": 1, "sequencerFeeVaultWithdrawalNetwork": 1, + "operatorFeeVaultWithdrawalNetwork": 1, "l1StandardBridgeProxy": "0x0000000000000000000000000000000000000000", "l1CrossDomainMessengerProxy": "0x0000000000000000000000000000000000000000", "l1ERC721BridgeProxy": "0x0000000000000000000000000000000000000000", @@ -79,5 +82,7 @@ "useFaultProofs": true, "fundDevAccounts": false, "requiredProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", - "recommendedProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000" + "recommendedProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", + "useRevenueShare": true, + "chainFeesRecipient": "0x8c20c40180751d93e939dddee3517ae0d1ebead2" } diff --git a/packages/contracts-bedrock/deploy-config/sepolia.json b/packages/contracts-bedrock/deploy-config/sepolia.json index 12a1cc392c803..9306c1764d14c 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia.json +++ b/packages/contracts-bedrock/deploy-config/sepolia.json @@ -22,12 +22,15 @@ "baseFeeVaultRecipient": "0xfd1D2e729aE8eEe2E146c033bf4400fE75284301", "l1FeeVaultRecipient": "0xfd1D2e729aE8eEe2E146c033bf4400fE75284301", "sequencerFeeVaultRecipient": "0xfd1D2e729aE8eEe2E146c033bf4400fE75284301", + "operatorFeeVaultRecipient": "0xfd1D2e729aE8eEe2E146c033bf4400fE75284301", "baseFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", + "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "baseFeeVaultWithdrawalNetwork": 0, "l1FeeVaultWithdrawalNetwork": 0, "sequencerFeeVaultWithdrawalNetwork": 0, + "operatorFeeVaultWithdrawalNetwork": 0, "enableGovernance": true, "governanceTokenSymbol": "OP", "governanceTokenName": "Optimism", @@ -54,5 +57,7 @@ "proofMaturityDelaySeconds": 604800, "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, - "useFaultProofs": true + "useFaultProofs": true, + "useRevenueShare": true, + "chainFeesRecipient": "0xfd1D2e729aE8eEe2E146c033bf4400fE75284301" } diff --git a/packages/contracts-bedrock/interfaces/L1/IFeesDepositor.sol b/packages/contracts-bedrock/interfaces/L1/IFeesDepositor.sol new file mode 100644 index 0000000000000..09c4ed009e5e3 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L1/IFeesDepositor.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; +import { IReinitializableBase } from "interfaces/universal/IReinitializableBase.sol"; +import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; + +interface IFeesDepositor is ISemver, IProxyAdminOwnedBase, IReinitializableBase { + event Initialized(uint8 version); + event FundsReceived(address indexed sender, uint256 amount, uint256 newBalance); + event FeesDeposited(address indexed l2Recipient, uint256 amount); + event MinDepositAmountUpdated(uint96 oldMinDepositAmount, uint96 newMinDepositAmount); + event L2RecipientUpdated(address oldL2Recipient, address newL2Recipient); + event GasLimitUpdated(uint32 oldGasLimit, uint32 newGasLimit); + + function minDepositAmount() external view returns (uint96); + function messenger() external view returns (IL1CrossDomainMessenger); + function l2Recipient() external view returns (address); + function gasLimit() external view returns (uint32); + function initialize( + uint96 _minDepositAmount, + address _l2Recipient, + IL1CrossDomainMessenger _messenger, + uint32 _gasLimit + ) + external; + + function setMinDepositAmount(uint96 _newMinDepositAmount) external; + function setL2Recipient(address _newL2Recipient) external; + function setGasLimit(uint32 _newGasLimit) external; + + receive() external payable; + + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/interfaces/L2/IBaseFeeVault.sol b/packages/contracts-bedrock/interfaces/L2/IBaseFeeVault.sol index 7ce731f2e6312..2c6c51ea63118 100644 --- a/packages/contracts-bedrock/interfaces/L2/IBaseFeeVault.sol +++ b/packages/contracts-bedrock/interfaces/L2/IBaseFeeVault.sol @@ -4,26 +4,39 @@ pragma solidity ^0.8.0; import { Types } from "src/libraries/Types.sol"; interface IBaseFeeVault { + error FeeVault_OnlyProxyAdminOwner(); + + error InvalidInitialization(); + error NotInitializing(); + + event Initialized(uint64 version); event Withdrawal(uint256 value, address to, address from); event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); + event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount); + event RecipientUpdated(address oldRecipient, address newRecipient); + event WithdrawalNetworkUpdated( + Types.WithdrawalNetwork oldWithdrawalNetwork, Types.WithdrawalNetwork newWithdrawalNetwork + ); receive() external payable; + function initialize( + address _recipient, + uint256 _minWithdrawalAmount, + Types.WithdrawalNetwork _withdrawalNetwork + ) + external; function MIN_WITHDRAWAL_AMOUNT() external view returns (uint256); function RECIPIENT() external view returns (address); function WITHDRAWAL_NETWORK() external view returns (Types.WithdrawalNetwork); - function minWithdrawalAmount() external view returns (uint256 amount_); - function recipient() external view returns (address recipient_); + function minWithdrawalAmount() external view returns (uint256); + function recipient() external view returns (address); function totalProcessed() external view returns (uint256); - function withdraw() external; - function withdrawalNetwork() external view returns (Types.WithdrawalNetwork network_); + function withdraw() external returns (uint256 value_); + function withdrawalNetwork() external view returns (Types.WithdrawalNetwork); + function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external; + function setRecipient(address _newRecipient) external; + function setWithdrawalNetwork(Types.WithdrawalNetwork _newWithdrawalNetwork) external; function version() external view returns (string memory); - - function __constructor__( - address _recipient, - uint256 _minWithdrawalAmount, - Types.WithdrawalNetwork _withdrawalNetwork - ) - external; } diff --git a/packages/contracts-bedrock/interfaces/L2/IFeeSplitter.sol b/packages/contracts-bedrock/interfaces/L2/IFeeSplitter.sol new file mode 100644 index 0000000000000..4dc2ac4b476d1 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/IFeeSplitter.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { ISharesCalculator } from "interfaces/L2/ISharesCalculator.sol"; + +interface IFeeSplitter is ISemver { + event Initialized(uint8 version); + error FeeSplitter_ExceedsMaxFeeDisbursementTime(); + error FeeSplitter_FeeDisbursementIntervalCannotBeZero(); + error FeeSplitter_SharesCalculatorCannotBeZero(); + error FeeSplitter_DisbursementIntervalNotReached(); + error FeeSplitter_FeeShareInfoEmpty(); + error FeeSplitter_NoFeesCollected(); + error FeeSplitter_FeeVaultMustWithdrawToL2(); + error FeeSplitter_FeeVaultMustWithdrawToFeeSplitter(); + error FeeSplitter_FeeVaultWithdrawalAmountMismatch(); + error FeeSplitter_OnlyProxyAdminOwner(); + error FeeSplitter_FailedToSendToRevenueShareRecipient(); + error FeeSplitter_SharesCalculatorMalformedOutput(); + error FeeSplitter_SenderNotCurrentVault(); + + event FeesReceived(address indexed sender, uint256 amount, uint256 newBalance); + event FeeDisbursementIntervalUpdated(uint128 oldFeeDisbursementInterval, uint128 newFeeDisbursementInterval); + event FeesDisbursed(ISharesCalculator.ShareInfo[] shareInfo, uint256 grossRevenue); + event SharesCalculatorUpdated(address oldSharesCalculator, address newSharesCalculator); + + function MAX_DISBURSEMENT_INTERVAL() external view returns (uint128); + function sharesCalculator() external view returns (ISharesCalculator); + function lastDisbursementTime() external view returns (uint128); + function feeDisbursementInterval() external view returns (uint128); + + function initialize(ISharesCalculator _sharesCalculator) external; + + function disburseFees() external; + + function setFeeDisbursementInterval(uint128 _newFeeDisbursementInterval) external; + + function setSharesCalculator(ISharesCalculator _newSharesCalculator) external; + + receive() external payable; +} diff --git a/packages/contracts-bedrock/interfaces/L2/IFeeVault.sol b/packages/contracts-bedrock/interfaces/L2/IFeeVault.sol index 3b4cd0209f133..96f8a6687938b 100644 --- a/packages/contracts-bedrock/interfaces/L2/IFeeVault.sol +++ b/packages/contracts-bedrock/interfaces/L2/IFeeVault.sol @@ -1,27 +1,42 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -library Types { - enum WithdrawalNetwork { - L1, - L2 - } -} +import { Types } from "src/libraries/Types.sol"; interface IFeeVault { + error FeeVault_OnlyProxyAdminOwner(); + error InvalidInitialization(); + error NotInitializing(); + + event Initialized(uint64 version); + event Withdrawal(uint256 value, address to, address from); event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); + event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount); + event RecipientUpdated(address oldRecipient, address newRecipient); + event WithdrawalNetworkUpdated( + Types.WithdrawalNetwork oldWithdrawalNetwork, Types.WithdrawalNetwork newWithdrawalNetwork + ); receive() external payable; + function initialize( + address _recipient, + uint256 _minWithdrawalAmount, + Types.WithdrawalNetwork _withdrawalNetwork + ) + external; function MIN_WITHDRAWAL_AMOUNT() external view returns (uint256); function RECIPIENT() external view returns (address); function WITHDRAWAL_NETWORK() external view returns (Types.WithdrawalNetwork); - function minWithdrawalAmount() external view returns (uint256 amount_); - function recipient() external view returns (address recipient_); + function minWithdrawalAmount() external view returns (uint256); + function recipient() external view returns (address); function totalProcessed() external view returns (uint256); - function withdraw() external; - function withdrawalNetwork() external view returns (Types.WithdrawalNetwork network_); + function withdraw() external returns (uint256 value_); + function withdrawalNetwork() external view returns (Types.WithdrawalNetwork); + function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external; + function setRecipient(address _newRecipient) external; + function setWithdrawalNetwork(Types.WithdrawalNetwork _newWithdrawalNetwork) external; function __constructor__() external; } diff --git a/packages/contracts-bedrock/interfaces/L2/IL1FeeVault.sol b/packages/contracts-bedrock/interfaces/L2/IL1FeeVault.sol index eb695a7e9a58e..6da52552c3701 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL1FeeVault.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL1FeeVault.sol @@ -4,26 +4,39 @@ pragma solidity ^0.8.0; import { Types } from "src/libraries/Types.sol"; interface IL1FeeVault { + error FeeVault_OnlyProxyAdminOwner(); + + error InvalidInitialization(); + error NotInitializing(); + + event Initialized(uint64 version); event Withdrawal(uint256 value, address to, address from); event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); + event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount); + event RecipientUpdated(address oldRecipient, address newRecipient); + event WithdrawalNetworkUpdated( + Types.WithdrawalNetwork oldWithdrawalNetwork, Types.WithdrawalNetwork newWithdrawalNetwork + ); receive() external payable; + function initialize( + address _recipient, + uint256 _minWithdrawalAmount, + Types.WithdrawalNetwork _withdrawalNetwork + ) + external; function MIN_WITHDRAWAL_AMOUNT() external view returns (uint256); function RECIPIENT() external view returns (address); function WITHDRAWAL_NETWORK() external view returns (Types.WithdrawalNetwork); - function minWithdrawalAmount() external view returns (uint256 amount_); - function recipient() external view returns (address recipient_); + function minWithdrawalAmount() external view returns (uint256); + function recipient() external view returns (address); function totalProcessed() external view returns (uint256); - function withdraw() external; - function withdrawalNetwork() external view returns (Types.WithdrawalNetwork network_); + function withdraw() external returns (uint256 value_); + function withdrawalNetwork() external view returns (Types.WithdrawalNetwork); + function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external; + function setRecipient(address _newRecipient) external; + function setWithdrawalNetwork(Types.WithdrawalNetwork _newWithdrawalNetwork) external; function version() external view returns (string memory); - - function __constructor__( - address _recipient, - uint256 _minWithdrawalAmount, - Types.WithdrawalNetwork _withdrawalNetwork - ) - external; } diff --git a/packages/contracts-bedrock/interfaces/L2/IL1Withdrawer.sol b/packages/contracts-bedrock/interfaces/L2/IL1Withdrawer.sol new file mode 100644 index 0000000000000..b1d2b58dea30c --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/IL1Withdrawer.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; + +interface IL1Withdrawer is ISemver { + error L1Withdrawer_OnlyProxyAdminOwner(); + + event WithdrawalInitiated(address indexed recipient, uint256 amount); + event FundsReceived(address indexed sender, uint256 amount, uint256 newBalance); + event MinWithdrawalAmountUpdated(uint256 oldMinWithdrawalAmount, uint256 newMinWithdrawalAmount); + event RecipientUpdated(address oldRecipient, address newRecipient); + event WithdrawalGasLimitUpdated(uint32 oldWithdrawalGasLimit, uint32 newWithdrawalGasLimit); + + function minWithdrawalAmount() external view returns (uint256); + function recipient() external view returns (address); + function withdrawalGasLimit() external view returns (uint32); + + function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external; + function setRecipient(address _newRecipient) external; + function setWithdrawalGasLimit(uint32 _newWithdrawalGasLimit) external; + + receive() external payable; + + function __constructor__(uint256 _minWithdrawalAmount, address _recipient, uint32 _withdrawalGasLimit) external; +} diff --git a/packages/contracts-bedrock/interfaces/L2/IOperatorFeeVault.sol b/packages/contracts-bedrock/interfaces/L2/IOperatorFeeVault.sol index 63978e203bca2..fe2789ca9680a 100644 --- a/packages/contracts-bedrock/interfaces/L2/IOperatorFeeVault.sol +++ b/packages/contracts-bedrock/interfaces/L2/IOperatorFeeVault.sol @@ -4,22 +4,39 @@ pragma solidity ^0.8.0; import { Types } from "src/libraries/Types.sol"; interface IOperatorFeeVault { + error FeeVault_OnlyProxyAdminOwner(); + + error InvalidInitialization(); + error NotInitializing(); + + event Initialized(uint64 version); event Withdrawal(uint256 value, address to, address from); event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); + event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount); + event RecipientUpdated(address oldRecipient, address newRecipient); + event WithdrawalNetworkUpdated( + Types.WithdrawalNetwork oldWithdrawalNetwork, Types.WithdrawalNetwork newWithdrawalNetwork + ); receive() external payable; + function initialize( + address _recipient, + uint256 _minWithdrawalAmount, + Types.WithdrawalNetwork _withdrawalNetwork + ) + external; function MIN_WITHDRAWAL_AMOUNT() external view returns (uint256); function RECIPIENT() external view returns (address); function WITHDRAWAL_NETWORK() external view returns (Types.WithdrawalNetwork); - function minWithdrawalAmount() external view returns (uint256 amount_); - function recipient() external view returns (address recipient_); + function minWithdrawalAmount() external view returns (uint256); + function recipient() external view returns (address); function totalProcessed() external view returns (uint256); - function withdraw() external; - function withdrawalNetwork() external view returns (Types.WithdrawalNetwork network_); + function withdraw() external returns (uint256 value_); + function withdrawalNetwork() external view returns (Types.WithdrawalNetwork); + function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external; + function setRecipient(address _newRecipient) external; + function setWithdrawalNetwork(Types.WithdrawalNetwork _newWithdrawalNetwork) external; function version() external view returns (string memory); - - function __constructor__() - external; } diff --git a/packages/contracts-bedrock/interfaces/L2/ISequencerFeeVault.sol b/packages/contracts-bedrock/interfaces/L2/ISequencerFeeVault.sol index e4ae46413effe..1770b5126bf76 100644 --- a/packages/contracts-bedrock/interfaces/L2/ISequencerFeeVault.sol +++ b/packages/contracts-bedrock/interfaces/L2/ISequencerFeeVault.sol @@ -4,27 +4,40 @@ pragma solidity ^0.8.0; import { Types } from "src/libraries/Types.sol"; interface ISequencerFeeVault { + error FeeVault_OnlyProxyAdminOwner(); + + error InvalidInitialization(); + error NotInitializing(); + + event Initialized(uint64 version); event Withdrawal(uint256 value, address to, address from); event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); + event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount); + event RecipientUpdated(address oldRecipient, address newRecipient); + event WithdrawalNetworkUpdated( + Types.WithdrawalNetwork oldWithdrawalNetwork, Types.WithdrawalNetwork newWithdrawalNetwork + ); receive() external payable; + function initialize( + address _recipient, + uint256 _minWithdrawalAmount, + Types.WithdrawalNetwork _withdrawalNetwork + ) + external; function MIN_WITHDRAWAL_AMOUNT() external view returns (uint256); function RECIPIENT() external view returns (address); function WITHDRAWAL_NETWORK() external view returns (Types.WithdrawalNetwork); - function minWithdrawalAmount() external view returns (uint256 amount_); - function recipient() external view returns (address recipient_); + function minWithdrawalAmount() external view returns (uint256); + function recipient() external view returns (address); function totalProcessed() external view returns (uint256); - function withdraw() external; - function withdrawalNetwork() external view returns (Types.WithdrawalNetwork network_); + function withdraw() external returns (uint256 value_); + function withdrawalNetwork() external view returns (Types.WithdrawalNetwork); + function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external; + function setRecipient(address _newRecipient) external; + function setWithdrawalNetwork(Types.WithdrawalNetwork _newWithdrawalNetwork) external; function version() external view returns (string memory); function l1FeeWallet() external view returns (address); - - function __constructor__( - address _recipient, - uint256 _minWithdrawalAmount, - Types.WithdrawalNetwork _withdrawalNetwork - ) - external; } diff --git a/packages/contracts-bedrock/interfaces/L2/ISharesCalculator.sol b/packages/contracts-bedrock/interfaces/L2/ISharesCalculator.sol new file mode 100644 index 0000000000000..f7935f1bb115d --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/ISharesCalculator.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title ISharesCalculator +/// @notice Interface for a contract that calculates the recipients and amounts for fee distribution. +/// @dev Meant to be called by the FeeSplitter contract. +interface ISharesCalculator { + /// @notice Struct to hold the recipient and amount for each fee share. + /// @param recipient The address that will receive the fee share + /// @param amount The amount of ETH to be sent to the recipient + struct ShareInfo { + address payable recipient; + uint256 amount; + } + + /// @notice Returns the recipients and amounts for fee distribution. + /// @dev Any implementation MUST return ShareInfo where the sum of all amounts equals + /// the total revenue (sum of all vault balances) as it will revert otherwise + /// @param _sequencerFeeRevenue Balance of the sequencer fee vault. + /// @param _baseFeeRevenue Balance of the base fee vault. + /// @param _operatorFeeRevenue Balance of the operator fee vault. + /// @param _l1FeeRevenue Balance of the L1 fee vault. + /// @return shareInfo Array of ShareInfo structs containing recipients and amounts. + function getRecipientsAndAmounts( + uint256 _sequencerFeeRevenue, + uint256 _baseFeeRevenue, + uint256 _operatorFeeRevenue, + uint256 _l1FeeRevenue + ) + external + view + returns (ShareInfo[] memory shareInfo); +} \ No newline at end of file diff --git a/packages/contracts-bedrock/interfaces/L2/ISuperchainRevSharesCalculator.sol b/packages/contracts-bedrock/interfaces/L2/ISuperchainRevSharesCalculator.sol new file mode 100644 index 0000000000000..0773f0e8be017 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/ISuperchainRevSharesCalculator.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { ISharesCalculator } from "interfaces/L2/ISharesCalculator.sol"; + +interface ISuperchainRevSharesCalculator is ISemver { + event ShareRecipientUpdated(address indexed oldShareRecipient, address indexed newShareRecipient); + event RemainderRecipientUpdated(address indexed oldRemainderRecipient, address indexed newRemainderRecipient); + + error SharesCalculator_OnlyProxyAdminOwner(); + error SharesCalculator_ZeroGrossShare(); + + function BASIS_POINT_SCALE() external view returns (uint32); + function GROSS_SHARE_BPS() external view returns (uint32); + function NET_SHARE_BPS() external view returns (uint32); + function shareRecipient() external view returns (address payable); + function remainderRecipient() external view returns (address payable); + + function getRecipientsAndAmounts( + uint256 _sequencerFeeRevenue, + uint256 _baseFeeRevenue, + uint256 _operatorFeeRevenue, + uint256 _l1FeeRevenue + ) + external + view + returns (ISharesCalculator.ShareInfo[] memory shareInfo_); + + function setShareRecipient(address payable _newShareRecipient) external; + function setRemainderRecipient(address payable _newRemainderRecipient) external; + + function __constructor__(address payable _shareRecipient, address payable _remainderRecipient) external; +} diff --git a/packages/contracts-bedrock/scripts/Artifacts.s.sol b/packages/contracts-bedrock/scripts/Artifacts.s.sol index 5858c8aa4c597..fac400dd68b36 100644 --- a/packages/contracts-bedrock/scripts/Artifacts.s.sol +++ b/packages/contracts-bedrock/scripts/Artifacts.s.sol @@ -130,6 +130,8 @@ contract Artifacts { return payable(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); } else if (digest == keccak256(bytes("SuperchainTokenBridge"))) { return payable(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + } else if (digest == keccak256(bytes("FeeSplitter"))) { + return payable(Predeploys.FEE_SPLITTER); } return payable(address(0)); } diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 5708dfe33c7a1..f914ab50a821e 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -6,9 +6,9 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Scripts import { Script } from "forge-std/Script.sol"; -import { OutputMode, OutputModeUtils, Fork, ForkUtils } from "scripts/libraries/Config.sol"; import { SetPreinstalls } from "scripts/SetPreinstalls.s.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { OutputMode, OutputModeUtils, Fork, ForkUtils } from "scripts/libraries/Config.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -16,10 +16,6 @@ import { Preinstalls } from "src/libraries/Preinstalls.sol"; import { Types } from "src/libraries/Types.sol"; // Interfaces -import { ISequencerFeeVault } from "interfaces/L2/ISequencerFeeVault.sol"; -import { IBaseFeeVault } from "interfaces/L2/IBaseFeeVault.sol"; -import { IL1FeeVault } from "interfaces/L2/IL1FeeVault.sol"; -import { IOperatorFeeVault } from "interfaces/L2/IOperatorFeeVault.sol"; import { IOptimismMintableERC721Factory } from "interfaces/L2/IOptimismMintableERC721Factory.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; @@ -30,6 +26,11 @@ import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenge import { IL2CrossDomainMessenger } from "interfaces/L2/IL2CrossDomainMessenger.sol"; import { IGasPriceOracle } from "interfaces/L2/IGasPriceOracle.sol"; import { IL1Block } from "interfaces/L2/IL1Block.sol"; +import { IFeeSplitter } from "interfaces/L2/IFeeSplitter.sol"; +import { ISharesCalculator } from "interfaces/L2/ISharesCalculator.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; +import { IL1Withdrawer } from "interfaces/L2/IL1Withdrawer.sol"; +import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; /// @title L2Genesis /// @notice Generates the genesis state for the L2 network. @@ -39,6 +40,13 @@ import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// 2. A contract must be deployed using the `new` syntax if there are immutables in the code. /// Any other side effects from the init code besides setting the immutables must be cleaned up afterwards. contract L2Genesis is Script { + error L2Genesis_ChainFeesRecipientCannotBeZero(); + error L2Genesis_L1FeesDepositorCannotBeZero(); + error L2Genesis_MisconfiguredSequencerFeeVault(); + error L2Genesis_MisconfiguredBaseFeeVault(); + error L2Genesis_MisconfiguredL1FeeVault(); + error L2Genesis_MisconfiguredOperatorFeeVault(); + struct Input { uint256 l1ChainID; uint256 l2ChainID; @@ -55,11 +63,17 @@ contract L2Genesis is Script { address l1FeeVaultRecipient; uint256 l1FeeVaultMinimumWithdrawalAmount; uint256 l1FeeVaultWithdrawalNetwork; + address operatorFeeVaultRecipient; + uint256 operatorFeeVaultMinimumWithdrawalAmount; + uint256 operatorFeeVaultWithdrawalNetwork; address governanceTokenOwner; uint256 fork; bool deployCrossL2Inbox; bool enableGovernance; bool fundDevAccounts; + bool useRevenueShare; + address chainFeesRecipient; + address l1FeesDepositor; } using ForkUtils for Fork; @@ -68,6 +82,8 @@ contract L2Genesis is Script { uint256 internal constant PRECOMPILE_COUNT = 256; uint80 internal constant DEV_ACCOUNT_FUND_AMT = 10_000 ether; + uint32 internal constant WITHDRAWAL_MIN_GAS_LIMIT = 1_000_000; + uint256 internal constant MIN_WITHDRAWAL_AMOUNT_THRESHOLD = 10 ether; /// @notice Default Anvil dev accounts. Only funded if `cfg.fundDevAccounts == true`. /// Also known as "test test test test test test test test test test test junk" mnemonic accounts, @@ -227,11 +243,12 @@ contract L2Genesis is Script { setProxyAdmin(_input); // 18 setBaseFeeVault(_input); // 19 setL1FeeVault(_input); // 1A - setOperatorFeeVault(); // 1B + setOperatorFeeVault(_input); // 1B // 1C,1D,1E,1F: not used. setSchemaRegistry(); // 20 setEAS(); // 21 setGovernanceToken(_input); // 42: OP (not behind a proxy) + setFeeSplitter(_input); // 2B: FeeSplitter if (_input.fork >= uint256(Fork.INTEROP)) { if (_input.deployCrossL2Inbox) { setCrossL2Inbox(); // 22 @@ -291,28 +308,13 @@ contract L2Genesis is Script { /// @notice This predeploy is following the safety invariant #2, function setSequencerFeeVault(Input memory _input) internal { - ISequencerFeeVault vault = ISequencerFeeVault( - DeployUtils.create1({ - _name: "SequencerFeeVault", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - ISequencerFeeVault.__constructor__, - ( - _input.sequencerFeeVaultRecipient, - _input.sequencerFeeVaultMinimumWithdrawalAmount, - Types.WithdrawalNetwork(_input.sequencerFeeVaultWithdrawalNetwork) - ) - ) - ) - }) - ); - - address impl = Predeploys.predeployToCodeNamespace(Predeploys.SEQUENCER_FEE_WALLET); - vm.etch(impl, address(vault).code); - - /// Reset so its not included state dump - vm.etch(address(vault), ""); - vm.resetNonce(address(vault)); + _setFeeVault({ + _vaultAddr: Predeploys.SEQUENCER_FEE_WALLET, + _useRevenueShare: _input.useRevenueShare, + _recipient: _input.sequencerFeeVaultRecipient, + _minWithdrawalAmount: _input.sequencerFeeVaultMinimumWithdrawalAmount, + _withdrawalNetwork: Types.WithdrawalNetwork(_input.sequencerFeeVaultWithdrawalNetwork) + }); } /// @notice This predeploy is following the safety invariant #1. @@ -383,71 +385,35 @@ contract L2Genesis is Script { /// @notice This predeploy is following the safety invariant #2. function setBaseFeeVault(Input memory _input) internal { - IBaseFeeVault vault = IBaseFeeVault( - DeployUtils.create1({ - _name: "BaseFeeVault", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IBaseFeeVault.__constructor__, - ( - _input.baseFeeVaultRecipient, - _input.baseFeeVaultMinimumWithdrawalAmount, - Types.WithdrawalNetwork(_input.baseFeeVaultWithdrawalNetwork) - ) - ) - ) - }) - ); - - address impl = Predeploys.predeployToCodeNamespace(Predeploys.BASE_FEE_VAULT); - vm.etch(impl, address(vault).code); - - /// Reset so its not included state dump - vm.etch(address(vault), ""); - vm.resetNonce(address(vault)); + _setFeeVault({ + _vaultAddr: Predeploys.BASE_FEE_VAULT, + _useRevenueShare: _input.useRevenueShare, + _recipient: _input.baseFeeVaultRecipient, + _minWithdrawalAmount: _input.baseFeeVaultMinimumWithdrawalAmount, + _withdrawalNetwork: Types.WithdrawalNetwork(_input.baseFeeVaultWithdrawalNetwork) + }); } /// @notice This predeploy is following the safety invariant #2. function setL1FeeVault(Input memory _input) internal { - IL1FeeVault vault = IL1FeeVault( - DeployUtils.create1({ - _name: "L1FeeVault", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IL1FeeVault.__constructor__, - ( - _input.l1FeeVaultRecipient, - _input.l1FeeVaultMinimumWithdrawalAmount, - Types.WithdrawalNetwork(_input.l1FeeVaultWithdrawalNetwork) - ) - ) - ) - }) - ); - - address impl = Predeploys.predeployToCodeNamespace(Predeploys.L1_FEE_VAULT); - vm.etch(impl, address(vault).code); - - /// Reset so its not included state dump - vm.etch(address(vault), ""); - vm.resetNonce(address(vault)); + _setFeeVault({ + _vaultAddr: Predeploys.L1_FEE_VAULT, + _useRevenueShare: _input.useRevenueShare, + _recipient: _input.l1FeeVaultRecipient, + _minWithdrawalAmount: _input.l1FeeVaultMinimumWithdrawalAmount, + _withdrawalNetwork: Types.WithdrawalNetwork(_input.l1FeeVaultWithdrawalNetwork) + }); } /// @notice This predeploy is following the safety invariant #2. - function setOperatorFeeVault() internal { - IOperatorFeeVault vault = IOperatorFeeVault( - DeployUtils.create1({ - _name: "OperatorFeeVault", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IOperatorFeeVault.__constructor__, ())) - }) - ); - - address impl = Predeploys.predeployToCodeNamespace(Predeploys.OPERATOR_FEE_VAULT); - vm.etch(impl, address(vault).code); - - /// Reset so its not included state dump - vm.etch(address(vault), ""); - vm.resetNonce(address(vault)); + function setOperatorFeeVault(Input memory _input) internal { + _setFeeVault({ + _vaultAddr: Predeploys.OPERATOR_FEE_VAULT, + _useRevenueShare: _input.useRevenueShare, + _recipient: _input.operatorFeeVaultRecipient, + _minWithdrawalAmount: _input.operatorFeeVaultMinimumWithdrawalAmount, + _withdrawalNetwork: Types.WithdrawalNetwork(_input.operatorFeeVaultWithdrawalNetwork) + }); } /// @notice This predeploy is following the safety invariant #2. @@ -573,6 +539,79 @@ contract L2Genesis is Script { IGasPriceOracle(Predeploys.GAS_PRICE_ORACLE).setIsthmus(); } + /// @notice This predeploy is following the safety invariant #1. + function setFeeSplitter(Input memory _input) internal { + address revSharesCalculator; + + // Only set the shares calculator if revenue sharing is enabled + if (_input.useRevenueShare) { + if (_input.chainFeesRecipient == address(0)) revert L2Genesis_ChainFeesRecipientCannotBeZero(); + if (_input.l1FeesDepositor == address(0)) revert L2Genesis_L1FeesDepositorCannotBeZero(); + + // Check that the vaults are properly configured + IFeeVault baseFeeVault = IFeeVault(payable(Predeploys.BASE_FEE_VAULT)); + if ( + baseFeeVault.recipient() != Predeploys.FEE_SPLITTER + || baseFeeVault.withdrawalNetwork() != Types.WithdrawalNetwork.L2 + ) revert L2Genesis_MisconfiguredBaseFeeVault(); + + IFeeVault l1FeeVault = IFeeVault(payable(Predeploys.L1_FEE_VAULT)); + if ( + l1FeeVault.recipient() != Predeploys.FEE_SPLITTER + || l1FeeVault.withdrawalNetwork() != Types.WithdrawalNetwork.L2 + ) revert L2Genesis_MisconfiguredL1FeeVault(); + + IFeeVault sequencerFeeVault = IFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)); + if ( + sequencerFeeVault.recipient() != Predeploys.FEE_SPLITTER + || sequencerFeeVault.withdrawalNetwork() != Types.WithdrawalNetwork.L2 + ) revert L2Genesis_MisconfiguredSequencerFeeVault(); + + IFeeVault operatorFeeVault = IFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)); + if ( + operatorFeeVault.recipient() != Predeploys.FEE_SPLITTER + || operatorFeeVault.withdrawalNetwork() != Types.WithdrawalNetwork.L2 + ) revert L2Genesis_MisconfiguredOperatorFeeVault(); + + // NOTE: L1Withdrawer and SuperchainRevSharesCalculator use CREATE2 (not vm.etch) because they're not + // predeploys (no fixed addresses), and they have constructor arguments. + + // Deploy L1Withdrawer with constructor args + bytes32 l1WithdrawerSalt = keccak256("L1Withdrawer"); + address l1Withdrawer = DeployUtils.create2({ + _name: "L1Withdrawer.sol:L1Withdrawer", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IL1Withdrawer.__constructor__, + (MIN_WITHDRAWAL_AMOUNT_THRESHOLD, _input.l1FeesDepositor, WITHDRAWAL_MIN_GAS_LIMIT) + ) + ), + _salt: l1WithdrawerSalt + }); + + // Deploy SuperchainRevSharesCalculator with constructor args + bytes32 calcSalt = keccak256("SuperchainRevSharesCalculator"); + revSharesCalculator = DeployUtils.create2({ + _name: "SuperchainRevSharesCalculator.sol:SuperchainRevSharesCalculator", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + ISuperchainRevSharesCalculator.__constructor__, + (payable(l1Withdrawer), payable(_input.chainFeesRecipient)) + ) + ), + _salt: calcSalt + }); + } + + // Initialize the implementation with dummy values + address impl = _setImplementationCode(Predeploys.FEE_SPLITTER); + IFeeSplitter(payable(impl)).initialize(ISharesCalculator(address(0))); + + // Initialize the proxy with the actual values + address sharesCalculator = revSharesCalculator; + IFeeSplitter(payable(Predeploys.FEE_SPLITTER)).initialize(ISharesCalculator(sharesCalculator)); + } + function activateJovian() internal { vm.prank(IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).DEPOSITOR_ACCOUNT()); IGasPriceOracle(Predeploys.GAS_PRICE_ORACLE).setJovian(); @@ -586,6 +625,48 @@ contract L2Genesis is Script { return impl; } + /// @notice Helper function to set up a fee vault predeploy with revenue sharing support. + /// This follows safety invariant #2 (initializable contracts). + /// @param _vaultAddr The predeploy address of the fee vault. + /// @param _useRevenueShare Whether revenue sharing is enabled. + /// @param _recipient The recipient address (ignored if revenue sharing is enabled). + /// @param _minWithdrawalAmount The minimum withdrawal amount (ignored if revenue sharing is enabled). + /// @param _withdrawalNetwork The withdrawal network (ignored if revenue sharing is enabled). + function _setFeeVault( + address _vaultAddr, + bool _useRevenueShare, + address _recipient, + uint256 _minWithdrawalAmount, + Types.WithdrawalNetwork _withdrawalNetwork + ) + internal + { + address recipient; + Types.WithdrawalNetwork network; + uint256 minWithdrawalAmount; + + if (_useRevenueShare) { + recipient = Predeploys.FEE_SPLITTER; + network = Types.WithdrawalNetwork.L2; + minWithdrawalAmount = 0; + } else { + recipient = _recipient; + network = _withdrawalNetwork; + minWithdrawalAmount = _minWithdrawalAmount; + } + + address impl = _setImplementationCode(_vaultAddr); + + /// Initialize the implementation using max value for min withdrawal amount to make it unusable + IFeeVault(payable(impl)).initialize(address(0), type(uint256).max, Types.WithdrawalNetwork.L1); + // Initialize the predeploy + IFeeVault(payable(_vaultAddr)).initialize({ + _recipient: recipient, + _minWithdrawalAmount: minWithdrawalAmount, + _withdrawalNetwork: network + }); + } + /// @notice Funds the default dev accounts with ether function fundDevAccounts() internal { for (uint256 i; i < devAccounts.length; i++) { diff --git a/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml b/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml index cf00ca4e9ca9c..14f060bba6fe3 100644 --- a/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml +++ b/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml @@ -29,6 +29,8 @@ src_validation = [ "test/universal/ExtendedPause.t.sol", # Tests extended functionality "test/vendor/Initializable.t.sol", # Tests external vendor code "test/vendor/InitializableOZv5.t.sol", # Tests external vendor code + "test/L2/LegacyFeeSplitter.t.sol", # Tests legacy fee splitter with updated vaults interface + "test/L2/FeeSplitterVaults.t.sol", # Tests FeeSplitter with vault-specific scenarios using test helper ] # PATHS EXCLUDED FROM CONTRACT NAME FILE PATH VALIDATION: @@ -57,6 +59,7 @@ contract_name_validation = [ "test/L2/GasPriceOracle.t.sol", # Contains contracts not matching GasPriceOracle base name "test/universal/StandardBridge.t.sol", # Contains contracts not matching StandardBridge base name "test/L1/OPContractsManagerContractsContainer.t.sol", # Contains contracts not matching OPContractsManagerContractsContainer base name + "test/L2/RevenueSharingIntegration.t.sol", # Contains contracts not matching RevenueSharingIntegration base name "test/libraries/Blueprint.t.sol", # Contains helper contracts (BlueprintHarness, ConstructorArgMock) "test/libraries/SafeCall.t.sol", # Contains helper contracts (SimpleSafeCaller) ] diff --git a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol index 928faad8129bc..b792d4f48bee4 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol @@ -48,6 +48,9 @@ contract DeployConfig is Script { address public sequencerFeeVaultRecipient; uint256 public sequencerFeeVaultMinimumWithdrawalAmount; uint256 public sequencerFeeVaultWithdrawalNetwork; + address public operatorFeeVaultRecipient; + uint256 public operatorFeeVaultMinimumWithdrawalAmount; + uint256 public operatorFeeVaultWithdrawalNetwork; address public governanceTokenOwner; uint256 public l2GenesisBlockGasLimit; uint32 public basefeeScalar; @@ -86,6 +89,12 @@ contract DeployConfig is Script { bool public useUpgradedFork; bytes32 public devFeatureBitmap; + bool public useRevenueShare; + address public chainFeesRecipient; + /// @notice This is not read from JSON because it is hardcoded in the deployer. It is overwritten with its setter + /// for testing. + address public l1FeesDepositor; + function read(string memory _path) public { console.log("DeployConfig: reading file %s", _path); try vm.readFile(_path) returns (string memory data_) { @@ -124,6 +133,9 @@ contract DeployConfig is Script { sequencerFeeVaultRecipient = stdJson.readAddress(_json, "$.sequencerFeeVaultRecipient"); sequencerFeeVaultMinimumWithdrawalAmount = stdJson.readUint(_json, "$.sequencerFeeVaultMinimumWithdrawalAmount"); sequencerFeeVaultWithdrawalNetwork = stdJson.readUint(_json, "$.sequencerFeeVaultWithdrawalNetwork"); + operatorFeeVaultRecipient = stdJson.readAddress(_json, "$.operatorFeeVaultRecipient"); + operatorFeeVaultMinimumWithdrawalAmount = stdJson.readUint(_json, "$.operatorFeeVaultMinimumWithdrawalAmount"); + operatorFeeVaultWithdrawalNetwork = stdJson.readUint(_json, "$.operatorFeeVaultWithdrawalNetwork"); governanceTokenOwner = stdJson.readAddress(_json, "$.governanceTokenOwner"); l2GenesisBlockGasLimit = stdJson.readUint(_json, "$.l2GenesisBlockGasLimit"); basefeeScalar = uint32(_readOr(_json, "$.gasPriceOracleBaseFeeScalar", 1368)); @@ -160,6 +172,8 @@ contract DeployConfig is Script { useInterop = _readOr(_json, "$.useInterop", false); devFeatureBitmap = bytes32(_readOr(_json, "$.devFeatureBitmap", 0)); useUpgradedFork; + useRevenueShare = _readOr(_json, "$.useRevenueShare", false); + chainFeesRecipient = _readOr(_json, "$.chainFeesRecipient", address(0)); faultGameV2MaxGameDepth = _readOr(_json, "$.faultGameV2MaxGameDepth", 73); faultGameV2SplitDepth = _readOr(_json, "$.faultGameV2SplitDepth", 30); faultGameV2ClockExtension = _readOr(_json, "$.faultGameV2ClockExtension", 10800); @@ -215,6 +229,21 @@ contract DeployConfig is Script { useInterop = _useInterop; } + /// @notice Allow the `useRevenueShare` config to be overridden in testing environments + function setUseRevenueShare(bool _useRevenueShare) public { + useRevenueShare = _useRevenueShare; + } + + /// @notice Allow the `l1FeesDepositor` config to be overridden in testing environments + function setL1FeesDepositor(address _l1FeesDepositor) public { + l1FeesDepositor = _l1FeesDepositor; + } + + /// @notice Allow the `chainFeesRecipient` config to be overridden in testing environments + function setChainFeesRecipient(address _chainFeesRecipient) public { + chainFeesRecipient = _chainFeesRecipient; + } + /// @notice Allow the `fundDevAccounts` config to be overridden. function setFundDevAccounts(bool _fundDevAccounts) public { fundDevAccounts = _fundDevAccounts; diff --git a/packages/contracts-bedrock/scripts/getting-started/config.sh b/packages/contracts-bedrock/scripts/getting-started/config.sh index ca2df6e099d52..bdecc6ef4a502 100755 --- a/packages/contracts-bedrock/scripts/getting-started/config.sh +++ b/packages/contracts-bedrock/scripts/getting-started/config.sh @@ -77,15 +77,18 @@ cat << EOL > tmp_config.json "baseFeeVaultRecipient": "$GS_ADMIN_ADDRESS", "l1FeeVaultRecipient": "$GS_ADMIN_ADDRESS", "sequencerFeeVaultRecipient": "$GS_ADMIN_ADDRESS", + "operatorFeeVaultRecipient": "$GS_ADMIN_ADDRESS", "finalSystemOwner": "$GS_ADMIN_ADDRESS", "superchainConfigGuardian": "$GS_ADMIN_ADDRESS", "baseFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", + "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "baseFeeVaultWithdrawalNetwork": 0, "l1FeeVaultWithdrawalNetwork": 0, "sequencerFeeVaultWithdrawalNetwork": 0, + "operatorFeeVaultWithdrawalNetwork": 0, "gasPriceOracleOverhead": 0, "gasPriceOracleScalar": 1000000, diff --git a/packages/contracts-bedrock/scripts/libraries/ForgeArtifacts.sol b/packages/contracts-bedrock/scripts/libraries/ForgeArtifacts.sol index de00f7e4ec621..847145434b564 100644 --- a/packages/contracts-bedrock/scripts/libraries/ForgeArtifacts.sol +++ b/packages/contracts-bedrock/scripts/libraries/ForgeArtifacts.sol @@ -218,6 +218,16 @@ library ForgeArtifacts { initialized_ = uint8((uint256(slotVal) >> (slot.offset * 8)) & 0xFF) != 0; } + /// @notice Checks if a contract is initialized using OpenZeppelin v5 namespaced storage pattern. + /// OZ v5 storage slot: keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) + /// & ~bytes32(uint256(0xff)) + function isInitializedV5(address _addr) internal view returns (bool) { + bytes32 INITIALIZABLE_STORAGE_SLOT = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + bytes32 slotVal = vm.load(_addr, INITIALIZABLE_STORAGE_SLOT); + // In OZ v5, byte 0 is _initialized, byte 1 is _initializing + return uint8(uint256(slotVal) & 0xFF) != 0; + } + /// @notice Returns the names of all contracts in a given directory. /// @param _path The path to search for contracts. /// @param _pathExcludes An array of paths to exclude from the search. diff --git a/packages/contracts-bedrock/snapshots/abi/BaseFeeVault.json b/packages/contracts-bedrock/snapshots/abi/BaseFeeVault.json index b745bcb8184c6..63f770b275ab1 100644 --- a/packages/contracts-bedrock/snapshots/abi/BaseFeeVault.json +++ b/packages/contracts-bedrock/snapshots/abi/BaseFeeVault.json @@ -1,25 +1,4 @@ [ - { - "inputs": [ - { - "internalType": "address", - "name": "_recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_minWithdrawalAmount", - "type": "uint256" - }, - { - "internalType": "enum Types.WithdrawalNetwork", - "name": "_withdrawalNetwork", - "type": "uint8" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, { "stateMutability": "payable", "type": "receive" @@ -63,13 +42,36 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minWithdrawalAmount", + "type": "uint256" + }, + { + "internalType": "enum Types.WithdrawalNetwork", + "name": "_withdrawalNetwork", + "type": "uint8" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "minWithdrawalAmount", "outputs": [ { "internalType": "uint256", - "name": "amount_", + "name": "", "type": "uint256" } ], @@ -82,13 +84,52 @@ "outputs": [ { "internalType": "address", - "name": "recipient_", + "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_newMinWithdrawalAmount", + "type": "uint256" + } + ], + "name": "setMinWithdrawalAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newRecipient", + "type": "address" + } + ], + "name": "setRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum Types.WithdrawalNetwork", + "name": "_newWithdrawalNetwork", + "type": "uint8" + } + ], + "name": "setWithdrawalNetwork", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "totalProcessed", @@ -118,7 +159,13 @@ { "inputs": [], "name": "withdraw", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "value_", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -128,13 +175,64 @@ "outputs": [ { "internalType": "enum Types.WithdrawalNetwork", - "name": "network_", + "name": "", "type": "uint8" } ], "stateMutability": "view", "type": "function" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldWithdrawalAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newWithdrawalAmount", + "type": "uint256" + } + ], + "name": "MinWithdrawalAmountUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldRecipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newRecipient", + "type": "address" + } + ], + "name": "RecipientUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -190,5 +288,39 @@ ], "name": "Withdrawal", "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "enum Types.WithdrawalNetwork", + "name": "oldWithdrawalNetwork", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "enum Types.WithdrawalNetwork", + "name": "newWithdrawalNetwork", + "type": "uint8" + } + ], + "name": "WithdrawalNetworkUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "FeeVault_OnlyProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/FeeSplitter.json b/packages/contracts-bedrock/snapshots/abi/FeeSplitter.json new file mode 100644 index 0000000000000..4e9e6bad93143 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/FeeSplitter.json @@ -0,0 +1,294 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "stateMutability": "payable", + "type": "receive" + }, + { + "inputs": [], + "name": "MAX_DISBURSEMENT_INTERVAL", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "disburseFees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "feeDisbursementInterval", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ISharesCalculator", + "name": "_sharesCalculator", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "lastDisbursementTime", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "_newFeeDisbursementInterval", + "type": "uint128" + } + ], + "name": "setFeeDisbursementInterval", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ISharesCalculator", + "name": "_newSharesCalculator", + "type": "address" + } + ], + "name": "setSharesCalculator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "sharesCalculator", + "outputs": [ + { + "internalType": "contract ISharesCalculator", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint128", + "name": "oldFeeDisbursementInterval", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "newFeeDisbursementInterval", + "type": "uint128" + } + ], + "name": "FeeDisbursementIntervalUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISharesCalculator.ShareInfo[]", + "name": "shareInfo", + "type": "tuple[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "grossRevenue", + "type": "uint256" + } + ], + "name": "FeesDisbursed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBalance", + "type": "uint256" + } + ], + "name": "FeesReceived", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldSharesCalculator", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newSharesCalculator", + "type": "address" + } + ], + "name": "SharesCalculatorUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "FeeSplitter_DisbursementIntervalNotReached", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_ExceedsMaxFeeDisbursementTime", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_FailedToSendToRevenueShareRecipient", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_FeeDisbursementIntervalCannotBeZero", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_FeeShareInfoEmpty", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_FeeVaultMustWithdrawToFeeSplitter", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_FeeVaultMustWithdrawToL2", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_FeeVaultWithdrawalAmountMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_NoFeesCollected", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_OnlyProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_SenderNotCurrentVault", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_SharesCalculatorCannotBeZero", + "type": "error" + }, + { + "inputs": [], + "name": "FeeSplitter_SharesCalculatorMalformedOutput", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/FeesDepositor.json b/packages/contracts-bedrock/snapshots/abi/FeesDepositor.json new file mode 100644 index 0000000000000..1a9c2cc2522e6 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/FeesDepositor.json @@ -0,0 +1,331 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "stateMutability": "payable", + "type": "receive" + }, + { + "inputs": [], + "name": "gasLimit", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "_minDepositAmount", + "type": "uint96" + }, + { + "internalType": "address", + "name": "_l2Recipient", + "type": "address" + }, + { + "internalType": "contract IL1CrossDomainMessenger", + "name": "_messenger", + "type": "address" + }, + { + "internalType": "uint32", + "name": "_gasLimit", + "type": "uint32" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "l2Recipient", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "messenger", + "outputs": [ + { + "internalType": "contract IL1CrossDomainMessenger", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minDepositAmount", + "outputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdmin", + "outputs": [ + { + "internalType": "contract IProxyAdmin", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_newGasLimit", + "type": "uint32" + } + ], + "name": "setGasLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newL2Recipient", + "type": "address" + } + ], + "name": "setL2Recipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "_newMinDepositAmount", + "type": "uint96" + } + ], + "name": "setMinDepositAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "l2Recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "FeesDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBalance", + "type": "uint256" + } + ], + "name": "FundsReceived", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "oldGasLimit", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "newGasLimit", + "type": "uint32" + } + ], + "name": "GasLimitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldL2Recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newL2Recipient", + "type": "address" + } + ], + "name": "L2RecipientUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint96", + "name": "oldMinDepositAmount", + "type": "uint96" + }, + { + "indexed": false, + "internalType": "uint96", + "name": "newMinDepositAmount", + "type": "uint96" + } + ], + "name": "MinDepositAmountUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotResolvedDelegateProxy", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotSharedProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_ProxyAdminNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/L1FeeVault.json b/packages/contracts-bedrock/snapshots/abi/L1FeeVault.json index b745bcb8184c6..63f770b275ab1 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1FeeVault.json +++ b/packages/contracts-bedrock/snapshots/abi/L1FeeVault.json @@ -1,25 +1,4 @@ [ - { - "inputs": [ - { - "internalType": "address", - "name": "_recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_minWithdrawalAmount", - "type": "uint256" - }, - { - "internalType": "enum Types.WithdrawalNetwork", - "name": "_withdrawalNetwork", - "type": "uint8" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, { "stateMutability": "payable", "type": "receive" @@ -63,13 +42,36 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minWithdrawalAmount", + "type": "uint256" + }, + { + "internalType": "enum Types.WithdrawalNetwork", + "name": "_withdrawalNetwork", + "type": "uint8" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "minWithdrawalAmount", "outputs": [ { "internalType": "uint256", - "name": "amount_", + "name": "", "type": "uint256" } ], @@ -82,13 +84,52 @@ "outputs": [ { "internalType": "address", - "name": "recipient_", + "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_newMinWithdrawalAmount", + "type": "uint256" + } + ], + "name": "setMinWithdrawalAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newRecipient", + "type": "address" + } + ], + "name": "setRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum Types.WithdrawalNetwork", + "name": "_newWithdrawalNetwork", + "type": "uint8" + } + ], + "name": "setWithdrawalNetwork", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "totalProcessed", @@ -118,7 +159,13 @@ { "inputs": [], "name": "withdraw", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "value_", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -128,13 +175,64 @@ "outputs": [ { "internalType": "enum Types.WithdrawalNetwork", - "name": "network_", + "name": "", "type": "uint8" } ], "stateMutability": "view", "type": "function" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldWithdrawalAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newWithdrawalAmount", + "type": "uint256" + } + ], + "name": "MinWithdrawalAmountUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldRecipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newRecipient", + "type": "address" + } + ], + "name": "RecipientUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -190,5 +288,39 @@ ], "name": "Withdrawal", "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "enum Types.WithdrawalNetwork", + "name": "oldWithdrawalNetwork", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "enum Types.WithdrawalNetwork", + "name": "newWithdrawalNetwork", + "type": "uint8" + } + ], + "name": "WithdrawalNetworkUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "FeeVault_OnlyProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/L1Withdrawer.json b/packages/contracts-bedrock/snapshots/abi/L1Withdrawer.json new file mode 100644 index 0000000000000..3ead4d7a5c326 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/L1Withdrawer.json @@ -0,0 +1,224 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_minWithdrawalAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint32", + "name": "_withdrawalGasLimit", + "type": "uint32" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "stateMutability": "payable", + "type": "receive" + }, + { + "inputs": [], + "name": "minWithdrawalAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "recipient", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_newMinWithdrawalAmount", + "type": "uint256" + } + ], + "name": "setMinWithdrawalAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newRecipient", + "type": "address" + } + ], + "name": "setRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_newWithdrawalGasLimit", + "type": "uint32" + } + ], + "name": "setWithdrawalGasLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawalGasLimit", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBalance", + "type": "uint256" + } + ], + "name": "FundsReceived", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldMinWithdrawalAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newMinWithdrawalAmount", + "type": "uint256" + } + ], + "name": "MinWithdrawalAmountUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldRecipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newRecipient", + "type": "address" + } + ], + "name": "RecipientUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "oldWithdrawalGasLimit", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "newWithdrawalGasLimit", + "type": "uint32" + } + ], + "name": "WithdrawalGasLimitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "WithdrawalInitiated", + "type": "event" + }, + { + "inputs": [], + "name": "L1Withdrawer_OnlyProxyAdminOwner", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OperatorFeeVault.json b/packages/contracts-bedrock/snapshots/abi/OperatorFeeVault.json index 657c3d278085b..63f770b275ab1 100644 --- a/packages/contracts-bedrock/snapshots/abi/OperatorFeeVault.json +++ b/packages/contracts-bedrock/snapshots/abi/OperatorFeeVault.json @@ -1,9 +1,4 @@ [ - { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" - }, { "stateMutability": "payable", "type": "receive" @@ -47,13 +42,36 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minWithdrawalAmount", + "type": "uint256" + }, + { + "internalType": "enum Types.WithdrawalNetwork", + "name": "_withdrawalNetwork", + "type": "uint8" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "minWithdrawalAmount", "outputs": [ { "internalType": "uint256", - "name": "amount_", + "name": "", "type": "uint256" } ], @@ -66,13 +84,52 @@ "outputs": [ { "internalType": "address", - "name": "recipient_", + "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_newMinWithdrawalAmount", + "type": "uint256" + } + ], + "name": "setMinWithdrawalAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newRecipient", + "type": "address" + } + ], + "name": "setRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum Types.WithdrawalNetwork", + "name": "_newWithdrawalNetwork", + "type": "uint8" + } + ], + "name": "setWithdrawalNetwork", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "totalProcessed", @@ -102,7 +159,13 @@ { "inputs": [], "name": "withdraw", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "value_", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -112,13 +175,64 @@ "outputs": [ { "internalType": "enum Types.WithdrawalNetwork", - "name": "network_", + "name": "", "type": "uint8" } ], "stateMutability": "view", "type": "function" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldWithdrawalAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newWithdrawalAmount", + "type": "uint256" + } + ], + "name": "MinWithdrawalAmountUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldRecipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newRecipient", + "type": "address" + } + ], + "name": "RecipientUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -174,5 +288,39 @@ ], "name": "Withdrawal", "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "enum Types.WithdrawalNetwork", + "name": "oldWithdrawalNetwork", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "enum Types.WithdrawalNetwork", + "name": "newWithdrawalNetwork", + "type": "uint8" + } + ], + "name": "WithdrawalNetworkUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "FeeVault_OnlyProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/SequencerFeeVault.json b/packages/contracts-bedrock/snapshots/abi/SequencerFeeVault.json index 700e7d7b98103..0b3563dcc71a7 100644 --- a/packages/contracts-bedrock/snapshots/abi/SequencerFeeVault.json +++ b/packages/contracts-bedrock/snapshots/abi/SequencerFeeVault.json @@ -1,25 +1,4 @@ [ - { - "inputs": [ - { - "internalType": "address", - "name": "_recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_minWithdrawalAmount", - "type": "uint256" - }, - { - "internalType": "enum Types.WithdrawalNetwork", - "name": "_withdrawalNetwork", - "type": "uint8" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, { "stateMutability": "payable", "type": "receive" @@ -63,6 +42,29 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minWithdrawalAmount", + "type": "uint256" + }, + { + "internalType": "enum Types.WithdrawalNetwork", + "name": "_withdrawalNetwork", + "type": "uint8" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "l1FeeWallet", @@ -82,7 +84,7 @@ "outputs": [ { "internalType": "uint256", - "name": "amount_", + "name": "", "type": "uint256" } ], @@ -95,13 +97,52 @@ "outputs": [ { "internalType": "address", - "name": "recipient_", + "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_newMinWithdrawalAmount", + "type": "uint256" + } + ], + "name": "setMinWithdrawalAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newRecipient", + "type": "address" + } + ], + "name": "setRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum Types.WithdrawalNetwork", + "name": "_newWithdrawalNetwork", + "type": "uint8" + } + ], + "name": "setWithdrawalNetwork", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "totalProcessed", @@ -131,7 +172,13 @@ { "inputs": [], "name": "withdraw", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "value_", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -141,13 +188,64 @@ "outputs": [ { "internalType": "enum Types.WithdrawalNetwork", - "name": "network_", + "name": "", "type": "uint8" } ], "stateMutability": "view", "type": "function" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldWithdrawalAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newWithdrawalAmount", + "type": "uint256" + } + ], + "name": "MinWithdrawalAmountUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldRecipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newRecipient", + "type": "address" + } + ], + "name": "RecipientUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -203,5 +301,39 @@ ], "name": "Withdrawal", "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "enum Types.WithdrawalNetwork", + "name": "oldWithdrawalNetwork", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "enum Types.WithdrawalNetwork", + "name": "newWithdrawalNetwork", + "type": "uint8" + } + ], + "name": "WithdrawalNetworkUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "FeeVault_OnlyProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/SuperchainRevSharesCalculator.json b/packages/contracts-bedrock/snapshots/abi/SuperchainRevSharesCalculator.json new file mode 100644 index 0000000000000..5b5962cdf3203 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainRevSharesCalculator.json @@ -0,0 +1,216 @@ +[ + { + "inputs": [ + { + "internalType": "address payable", + "name": "_shareRecipient", + "type": "address" + }, + { + "internalType": "address payable", + "name": "_remainderRecipient", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "BASIS_POINT_SCALE", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GROSS_SHARE_BPS", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "NET_SHARE_BPS", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_sequencerFeeRevenue", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_baseFeeRevenue", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_operatorFeeRevenue", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_l1FeeRevenue", + "type": "uint256" + } + ], + "name": "getRecipientsAndAmounts", + "outputs": [ + { + "components": [ + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ISharesCalculator.ShareInfo[]", + "name": "shareInfo_", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "remainderRecipient", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "_newRemainderRecipient", + "type": "address" + } + ], + "name": "setRemainderRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "_newShareRecipient", + "type": "address" + } + ], + "name": "setShareRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "shareRecipient", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldRemainderRecipient", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newRemainderRecipient", + "type": "address" + } + ], + "name": "RemainderRecipientUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldShareRecipient", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newShareRecipient", + "type": "address" + } + ], + "name": "ShareRecipientUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "SharesCalculator_OnlyProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "SharesCalculator_ZeroGrossShare", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index f7dbfcffbe497..8fe873558cdea 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -7,6 +7,10 @@ "initCodeHash": "0x65db3aa3c2e3221065752f66016fa02b66688a01cc5c3066533b27fe620619c8", "sourceCodeHash": "0x6c9d3e2dee44c234d59ab93b6564536dfd807f1c4a02a82d5393bc53cb15b8b7" }, + "src/L1/FeesDepositor.sol:FeesDepositor": { + "initCodeHash": "0xe52c51805cfd55967d037173159f18aaf4344e32e5c8ad41f8d5d0025b1d36a8", + "sourceCodeHash": "0xe5f2b1915a201c0b8a107f168f5b9bc8aec8e8e95f938082e42ba5b5c8ebbd11" + }, "src/L1/L1CrossDomainMessenger.sol:L1CrossDomainMessenger": { "initCodeHash": "0x3dc659aafb03bd357f92abfc6794af89ee0ddd5212364551637422bf8d0b00f9", "sourceCodeHash": "0xef3d366cd22eac2dfd22a658e003700c679bd9c38758d9c21befa7335bbd82ad" @@ -48,8 +52,8 @@ "sourceCodeHash": "0x18ebe840a448ad52ca6f0864c90c475c8622958900f2296e8a7cdfb8cc5cc512" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { - "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", - "sourceCodeHash": "0x508610081cade08f935e2a66f31cc193874bc0e2971a65db4a7842f1a428b1d0" + "initCodeHash": "0x838bbd7f381e84e21887f72bd1da605bfc4588b3c39aed96cbce67c09335b3ee", + "sourceCodeHash": "0xcb329746df0baddd3dc03c6c88da5d6bdc0f0a96d30e6dc78d0891bb1e935032" }, "src/L2/CrossL2Inbox.sol:CrossL2Inbox": { "initCodeHash": "0x56f868e561c4abe539043f98b16aad9305479e68fd03ece2233249b0c73a24ea", @@ -59,6 +63,10 @@ "initCodeHash": "0xd4a8b5b95e29bdad905637c4007af161b28224f350f54508e66299f56cffcef0", "sourceCodeHash": "0x6d137fef431d75a8bf818444915fc39c8b1d93434a9af9971d96fb3170bc72b7" }, + "src/L2/FeeSplitter.sol:FeeSplitter": { + "initCodeHash": "0xdaae3903628f760e36da47c8f8d75d20962d1811fb5129cb09eb01803e67c095", + "sourceCodeHash": "0x95dd8da08e907fa398c98710bb12fda9fb50d9688c5d2144fd9a424c99e672c5" + }, "src/L2/GasPriceOracle.sol:GasPriceOracle": { "initCodeHash": "0xf72c23d9c3775afd7b645fde429d09800622d329116feb5ff9829634655123ca", "sourceCodeHash": "0xb4d1bf3669ba87bbeaf4373145c7e1490478c4a05ba4838a524aa6f0ce7348a6" @@ -68,8 +76,12 @@ "sourceCodeHash": "0xa45ec2f83b73ac7792d193098240e19e4b26e4102bd1e3c64bb7d0f91074be82" }, "src/L2/L1FeeVault.sol:L1FeeVault": { - "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", - "sourceCodeHash": "0xfdf158752a5802c3697d6e8467046f6378680ceaa9e59ab02da0f5dcd575e5e2" + "initCodeHash": "0x838bbd7f381e84e21887f72bd1da605bfc4588b3c39aed96cbce67c09335b3ee", + "sourceCodeHash": "0x34186bcab29963237b4e0d7575b0a1cff7caf42ccdb55d4b2b2c767db3279189" + }, + "src/L2/L1Withdrawer.sol:L1Withdrawer": { + "initCodeHash": "0x91e0be0d49636212678191c06b9b6840c399f08ad946bc7b52f24231691be28b", + "sourceCodeHash": "0x25422bdaf51d611c1688a835737368c0ff2ab639dac852af8a20ebb4e16fc103" }, "src/L2/L2CrossDomainMessenger.sol:L2CrossDomainMessenger": { "initCodeHash": "0xe160be403df12709c371c33195d1b9c3b5e9499e902e86bdabc8eed749c3fd61", @@ -96,8 +108,8 @@ "sourceCodeHash": "0xbea4229c5c6988243dbc7cf5a086ddd412fe1f2903b8e20d56699fec8de0c2c9" }, "src/L2/OperatorFeeVault.sol:OperatorFeeVault": { - "initCodeHash": "0x3d8c0d7736e8767f2f797da1c20c5fe30bd7f48a4cf75f376290481ad7c0f91f", - "sourceCodeHash": "0x2022fdb4e32769eb9446dab4aed4b8abb5261fd866f381cccfa7869df1a2adff" + "initCodeHash": "0x2ebab6af089a714df25888a4dea81dadcb1fb57146be84d2e079041a9396a810", + "sourceCodeHash": "0xd6e94bc9df025855916aa4184d0bc739b0fbe786dfd037b99dbb51d0d3e46918" }, "src/L2/OptimismMintableERC721.sol:OptimismMintableERC721": { "initCodeHash": "0x316c6d716358f5b5284cd5457ea9fca4b5ad4a37835d4b4b300413dafbfa2159", @@ -120,8 +132,8 @@ "sourceCodeHash": "0x11b6236911e909ed10d4f194fe7315c1f5533d21cbe69a8ff16248c827df2647" }, "src/L2/SequencerFeeVault.sol:SequencerFeeVault": { - "initCodeHash": "0x96474e1d225513c931b8c5c656541212d659686ddc6d49a72ac4df9d640eedd5", - "sourceCodeHash": "0x7df290a6fbc8e712cf4411c3965fd4c59511477f70e666882c57f7c25cf4523e" + "initCodeHash": "0x2cf94abac28d35065c7d361055199d5bf2bd49ec3907f8b81eefee4fcf7df484", + "sourceCodeHash": "0x5fa147acd34a5f1c451404234d22e114c79f1255decc51afd8930d5ce99d7e02" }, "src/L2/SuperchainERC20.sol:SuperchainERC20": { "initCodeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", @@ -131,6 +143,10 @@ "initCodeHash": "0xa43665ad0f2b4f092ff04b12e38f24aa8d2cb34ae7a06fc037970743547bdf98", "sourceCodeHash": "0x862b8a2e5dd5cafcda55e35df7713b0d0b7a93d4d6ce29ea9ca53e045bf63cb4" }, + "src/L2/SuperchainRevSharesCalculator.sol:SuperchainRevSharesCalculator": { + "initCodeHash": "0xdfff95660d2d470e198054bb1717a30a45a806d2eaa3720fb43acaa3356c9a3e", + "sourceCodeHash": "0x4f494790d6044882ca0150bb28bb4abbf45cd2617bbdae0ee13b0085961ca788" + }, "src/L2/SuperchainTokenBridge.sol:SuperchainTokenBridge": { "initCodeHash": "0xb0d25dc03b9c84b07b263921c2b717e6caad3f4297fa939207e35978d7d25abe", "sourceCodeHash": "0x0ff7c1f0264d784fac5d69b792c6bc9d064d4a09701c1bafa808388685c8c4f1" diff --git a/packages/contracts-bedrock/snapshots/storageLayout/BaseFeeVault.json b/packages/contracts-bedrock/snapshots/storageLayout/BaseFeeVault.json index 93c5a7ec8a6b3..c72044ffabe35 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/BaseFeeVault.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/BaseFeeVault.json @@ -7,10 +7,31 @@ "type": "uint256" }, { - "bytes": "1536", - "label": "__gap", + "bytes": "32", + "label": "minWithdrawalAmount", "offset": 0, "slot": "1", - "type": "uint256[48]" + "type": "uint256" + }, + { + "bytes": "20", + "label": "recipient", + "offset": 0, + "slot": "2", + "type": "address" + }, + { + "bytes": "1", + "label": "withdrawalNetwork", + "offset": 20, + "slot": "2", + "type": "enum Types.WithdrawalNetwork" + }, + { + "bytes": "1472", + "label": "__gap", + "offset": 0, + "slot": "3", + "type": "uint256[46]" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/FeeSplitter.json b/packages/contracts-bedrock/snapshots/storageLayout/FeeSplitter.json new file mode 100644 index 0000000000000..c46ad296e709d --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/FeeSplitter.json @@ -0,0 +1,37 @@ +[ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "20", + "label": "sharesCalculator", + "offset": 2, + "slot": "0", + "type": "contract ISharesCalculator" + }, + { + "bytes": "16", + "label": "lastDisbursementTime", + "offset": 0, + "slot": "1", + "type": "uint128" + }, + { + "bytes": "16", + "label": "feeDisbursementInterval", + "offset": 16, + "slot": "1", + "type": "uint128" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/FeesDepositor.json b/packages/contracts-bedrock/snapshots/storageLayout/FeesDepositor.json new file mode 100644 index 0000000000000..f19c8e649a8b4 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/FeesDepositor.json @@ -0,0 +1,44 @@ +[ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "20", + "label": "messenger", + "offset": 2, + "slot": "0", + "type": "contract IL1CrossDomainMessenger" + }, + { + "bytes": "12", + "label": "minDepositAmount", + "offset": 0, + "slot": "1", + "type": "uint96" + }, + { + "bytes": "20", + "label": "l2Recipient", + "offset": 12, + "slot": "1", + "type": "address" + }, + { + "bytes": "4", + "label": "gasLimit", + "offset": 0, + "slot": "2", + "type": "uint32" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/L1FeeVault.json b/packages/contracts-bedrock/snapshots/storageLayout/L1FeeVault.json index 93c5a7ec8a6b3..c72044ffabe35 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/L1FeeVault.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/L1FeeVault.json @@ -7,10 +7,31 @@ "type": "uint256" }, { - "bytes": "1536", - "label": "__gap", + "bytes": "32", + "label": "minWithdrawalAmount", "offset": 0, "slot": "1", - "type": "uint256[48]" + "type": "uint256" + }, + { + "bytes": "20", + "label": "recipient", + "offset": 0, + "slot": "2", + "type": "address" + }, + { + "bytes": "1", + "label": "withdrawalNetwork", + "offset": 20, + "slot": "2", + "type": "enum Types.WithdrawalNetwork" + }, + { + "bytes": "1472", + "label": "__gap", + "offset": 0, + "slot": "3", + "type": "uint256[46]" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/L1Withdrawer.json b/packages/contracts-bedrock/snapshots/storageLayout/L1Withdrawer.json new file mode 100644 index 0000000000000..cbc72ddb62e63 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/L1Withdrawer.json @@ -0,0 +1,23 @@ +[ + { + "bytes": "32", + "label": "minWithdrawalAmount", + "offset": 0, + "slot": "0", + "type": "uint256" + }, + { + "bytes": "20", + "label": "recipient", + "offset": 0, + "slot": "1", + "type": "address" + }, + { + "bytes": "4", + "label": "withdrawalGasLimit", + "offset": 20, + "slot": "1", + "type": "uint32" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OperatorFeeVault.json b/packages/contracts-bedrock/snapshots/storageLayout/OperatorFeeVault.json index 93c5a7ec8a6b3..c72044ffabe35 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OperatorFeeVault.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OperatorFeeVault.json @@ -7,10 +7,31 @@ "type": "uint256" }, { - "bytes": "1536", - "label": "__gap", + "bytes": "32", + "label": "minWithdrawalAmount", "offset": 0, "slot": "1", - "type": "uint256[48]" + "type": "uint256" + }, + { + "bytes": "20", + "label": "recipient", + "offset": 0, + "slot": "2", + "type": "address" + }, + { + "bytes": "1", + "label": "withdrawalNetwork", + "offset": 20, + "slot": "2", + "type": "enum Types.WithdrawalNetwork" + }, + { + "bytes": "1472", + "label": "__gap", + "offset": 0, + "slot": "3", + "type": "uint256[46]" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SequencerFeeVault.json b/packages/contracts-bedrock/snapshots/storageLayout/SequencerFeeVault.json index 93c5a7ec8a6b3..c72044ffabe35 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SequencerFeeVault.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SequencerFeeVault.json @@ -7,10 +7,31 @@ "type": "uint256" }, { - "bytes": "1536", - "label": "__gap", + "bytes": "32", + "label": "minWithdrawalAmount", "offset": 0, "slot": "1", - "type": "uint256[48]" + "type": "uint256" + }, + { + "bytes": "20", + "label": "recipient", + "offset": 0, + "slot": "2", + "type": "address" + }, + { + "bytes": "1", + "label": "withdrawalNetwork", + "offset": 20, + "slot": "2", + "type": "enum Types.WithdrawalNetwork" + }, + { + "bytes": "1472", + "label": "__gap", + "offset": 0, + "slot": "3", + "type": "uint256[46]" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SuperchainRevSharesCalculator.json b/packages/contracts-bedrock/snapshots/storageLayout/SuperchainRevSharesCalculator.json new file mode 100644 index 0000000000000..01786e811e51b --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/SuperchainRevSharesCalculator.json @@ -0,0 +1,16 @@ +[ + { + "bytes": "20", + "label": "shareRecipient", + "offset": 0, + "slot": "0", + "type": "address payable" + }, + { + "bytes": "20", + "label": "remainderRecipient", + "offset": 0, + "slot": "1", + "type": "address payable" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/FeesDepositor.sol b/packages/contracts-bedrock/src/L1/FeesDepositor.sol new file mode 100644 index 0000000000000..3c7185eb289dd --- /dev/null +++ b/packages/contracts-bedrock/src/L1/FeesDepositor.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; +import { ProxyAdminOwnedBase } from "src/L1/ProxyAdminOwnedBase.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +/// @custom:proxied true +/// @title FeesDepositor +/// @notice A contract that deposits fees to the L2 recipient when the deposit threshold is reached. +contract FeesDepositor is ProxyAdminOwnedBase, Initializable, ReinitializableBase, ISemver { + /// @notice The L1CrossDomainMessenger contract. + IL1CrossDomainMessenger public messenger; + + /// @notice The threshold at which fees are deposited. + uint96 public minDepositAmount; + + /// @notice The L2 recipient of the fees. + address public l2Recipient; + + /// @notice The gas limit for the deposit transaction. + uint32 public gasLimit; + + /// @notice Emitted when fees are received. + /// @param sender The sender of the fees. + /// @param amount The amount of fees received. + /// @param newBalance The new balance after receiving fees. + event FundsReceived(address indexed sender, uint256 amount, uint256 newBalance); + + /// @notice Emitted when fees are deposited. + /// @param amount The amount of fees deposited. + /// @param l2Recipient The L2 recipient of the fees. + event FeesDeposited(address indexed l2Recipient, uint256 amount); + + /// @notice Emitted when the deposit threshold is updated. + /// @param oldMinDepositAmount The old deposit threshold. + /// @param newMinDepositAmount The new deposit threshold. + event MinDepositAmountUpdated(uint96 oldMinDepositAmount, uint96 newMinDepositAmount); + + /// @notice Emitted when the L2 recipient is updated. + /// @param oldL2Recipient The old L2 recipient. + /// @param newL2Recipient The new L2 recipient. + event L2RecipientUpdated(address oldL2Recipient, address newL2Recipient); + + /// @notice Emitted when the gas limit is updated. + /// @param oldGasLimit The old gas limit. + /// @param newGasLimit The new gas limit. + event GasLimitUpdated(uint32 oldGasLimit, uint32 newGasLimit); + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice Constructs the FeesDepositor contract. + constructor() ReinitializableBase(1) { + _disableInitializers(); + } + + /// @notice Initializes the FeesDepositor contract. + /// @param _minDepositAmount The threshold at which fees are deposited. + /// @param _l2Recipient The L2 recipient of the fees. + /// @param _messenger The L1CrossDomainMessenger contract. + /// @param _gasLimit The gas limit for the deposit transaction. + function initialize( + uint96 _minDepositAmount, + address _l2Recipient, + IL1CrossDomainMessenger _messenger, + uint32 _gasLimit + ) + external + reinitializer(initVersion()) + { + // Initialization transactions must come from the ProxyAdmin or its owner. + _assertOnlyProxyAdminOrProxyAdminOwner(); + + messenger = _messenger; + minDepositAmount = _minDepositAmount; + l2Recipient = _l2Recipient; + gasLimit = _gasLimit; + } + + /// @notice Receives ETH and sends it to the L2 recipient via CrossDomainMessenger when the threshold is reached. + receive() external payable { + uint256 balance = address(this).balance; + emit FundsReceived(msg.sender, msg.value, balance); + + if (balance >= minDepositAmount) { + address recipient = l2Recipient; + messenger.sendMessage{ value: balance }(recipient, hex"", gasLimit); + emit FeesDeposited(recipient, balance); + } + } + + /// @notice Updates the deposit threshold. + /// @param _newMinDepositAmount The new deposit threshold. + function setMinDepositAmount(uint96 _newMinDepositAmount) external { + _assertOnlyProxyAdminOwner(); + uint96 oldMinDepositAmount = minDepositAmount; + minDepositAmount = _newMinDepositAmount; + emit MinDepositAmountUpdated(oldMinDepositAmount, _newMinDepositAmount); + } + + /// @notice Updates the L2 recipient for the deposit transaction. + /// @dev The L2 recipient MUST be able to receive ether or the deposit on L2 will fail. + /// @param _newL2Recipient The new L2 recipient. + function setL2Recipient(address _newL2Recipient) external { + _assertOnlyProxyAdminOwner(); + address oldL2Recipient = l2Recipient; + l2Recipient = _newL2Recipient; + emit L2RecipientUpdated(oldL2Recipient, _newL2Recipient); + } + + /// @notice Updates the gas limit for the deposit transaction. + /// @param _newGasLimit The new gas limit. + function setGasLimit(uint32 _newGasLimit) external { + _assertOnlyProxyAdminOwner(); + uint32 oldGasLimit = gasLimit; + gasLimit = _newGasLimit; + emit GasLimitUpdated(oldGasLimit, _newGasLimit); + } +} diff --git a/packages/contracts-bedrock/src/L2/BaseFeeVault.sol b/packages/contracts-bedrock/src/L2/BaseFeeVault.sol index 91f8e0d93b4b1..1f4e20249796a 100644 --- a/packages/contracts-bedrock/src/L2/BaseFeeVault.sol +++ b/packages/contracts-bedrock/src/L2/BaseFeeVault.sol @@ -1,12 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity 0.8.25; // Contracts import { FeeVault } from "src/L2/FeeVault.sol"; -// Libraries -import { Types } from "src/libraries/Types.sol"; - // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -16,18 +13,6 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// @notice The BaseFeeVault accumulates the base fee that is paid by transactions. contract BaseFeeVault is FeeVault, ISemver { /// @notice Semantic version. - /// @custom:semver 1.5.1 - string public constant version = "1.5.1"; - - /// @notice Constructs the BaseFeeVault contract. - /// @param _recipient Wallet that will receive the fees. - /// @param _minWithdrawalAmount Minimum balance for withdrawals. - /// @param _withdrawalNetwork Network which the recipient will receive fees on. - constructor( - address _recipient, - uint256 _minWithdrawalAmount, - Types.WithdrawalNetwork _withdrawalNetwork - ) - FeeVault(_recipient, _minWithdrawalAmount, _withdrawalNetwork) - { } + /// @custom:semver 1.6.0 + string public constant version = "1.6.0"; } diff --git a/packages/contracts-bedrock/src/L2/FeeSplitter.sol b/packages/contracts-bedrock/src/L2/FeeSplitter.sol new file mode 100644 index 0000000000000..b69b8a2691c6e --- /dev/null +++ b/packages/contracts-bedrock/src/L2/FeeSplitter.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Types } from "src/libraries/Types.sol"; +import { SafeCall } from "src/libraries/SafeCall.sol"; + +// Interfaces +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { ISharesCalculator } from "interfaces/L2/ISharesCalculator.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; + +// OpenZeppelin +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/// @custom:proxied +/// @custom:predeploy 0x420000000000000000000000000000000000002B +/// @title FeeSplitter +/// @notice Withdraws funds from system FeeVault contracts and distributes them according to the +/// configured SharesCalculator. +contract FeeSplitter is ISemver, Initializable { + /// @notice Thrown when the fee disbursement interval exceeds the maximum allowed. + error FeeSplitter_ExceedsMaxFeeDisbursementTime(); + + /// @notice Thrown when the fee disbursement interval is set to zero. + error FeeSplitter_FeeDisbursementIntervalCannotBeZero(); + + /// @notice Thrown when the share calculator address is zero. + error FeeSplitter_SharesCalculatorCannotBeZero(); + + /// @notice Thrown when the disbursement interval has not been reached. + error FeeSplitter_DisbursementIntervalNotReached(); + + /// @notice Thrown when the fee share recipients are empty. + error FeeSplitter_FeeShareInfoEmpty(); + + /// @notice Thrown when no fees are collected from vaults during disbursement. + error FeeSplitter_NoFeesCollected(); + + /// @notice Thrown when the FeeVault does not withdraw to L2. + error FeeSplitter_FeeVaultMustWithdrawToL2(); + + /// @notice Thrown when the FeeVault does not withdraw to FeeSplitter contract. + error FeeSplitter_FeeVaultMustWithdrawToFeeSplitter(); + + /// @notice Thrown when the FeeVault withdrawal amount does not match the expected amount. + error FeeSplitter_FeeVaultWithdrawalAmountMismatch(); + + /// @notice Thrown when the caller is not the ProxyAdmin owner. + error FeeSplitter_OnlyProxyAdminOwner(); + + /// @notice Thrown when sending funds to the fee recipient fails. + error FeeSplitter_FailedToSendToRevenueShareRecipient(); + + /// @notice Thrown when the sharesCalculator returns malformed output. + error FeeSplitter_SharesCalculatorMalformedOutput(); + + /// @notice Thrown when the sender is not the currently disbursing vault. + error FeeSplitter_SenderNotCurrentVault(); + + /// @notice Transient storage slot key for the address of the vault currently allowed to disburse. + /// Equal to bytes32(uint256(keccak256("feesplitter.disbursingAddress")) - 1) + bytes32 internal constant _FEE_SPLITTER_DISBURSING_ADDRESS_SLOT = + 0x21346dddac42cc163a6523eefc19df981df7352c870dc3b0b17a6a92fc6fe813; + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice max time between fee disbursements + uint128 public constant MAX_DISBURSEMENT_INTERVAL = 365 days; + + /// @notice The contract which determines the recipients and their weights for fee disbursement. + ISharesCalculator public sharesCalculator; + + /// @notice The timestamp of the last disbursal. + uint128 public lastDisbursementTime; + + /// @notice The minimum amount of time in seconds that must pass between fee disbursal. + uint128 public feeDisbursementInterval; + + /// @notice Emitted when fees are received from FeeVaults. + /// @param sender The FeeVault that sent the fees. + /// @param amount The amount of fees received. + /// @param newBalance The new balance after receiving fees. + event FeesReceived(address indexed sender, uint256 amount, uint256 newBalance); + + /// @notice Emitted when the fee disbursement interval is updated. + /// @param oldFeeDisbursementInterval The previous fee disbursement interval. + /// @param newFeeDisbursementInterval The new fee disbursement interval. + event FeeDisbursementIntervalUpdated(uint128 oldFeeDisbursementInterval, uint128 newFeeDisbursementInterval); + + /// @notice Emitted when fees are disbursed to the recipients. + /// @param shareInfo The recipients of the fee share. + /// @param grossRevenue The gross revenue before disbursement. + event FeesDisbursed(ISharesCalculator.ShareInfo[] shareInfo, uint256 grossRevenue); + + /// @notice Emitted when the share calculator is updated. + /// @param oldSharesCalculator The old share calculator contract. + /// @param newSharesCalculator The new share calculator contract. + event SharesCalculatorUpdated(address oldSharesCalculator, address newSharesCalculator); + + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract with all required addresses and parameters. + /// @dev This function can only be called once and must be called by the ProxyAdmin owner. + /// @param _sharesCalculator The share calculator contract. + function initialize(ISharesCalculator _sharesCalculator) external initializer { + sharesCalculator = _sharesCalculator; + // As default, the fee disbursement interval is 1 day + feeDisbursementInterval = 1 days; + + // Set the last disbursement time to the current block timestamp + lastDisbursementTime = uint128(block.timestamp); + } + + /// @dev Receives ETH fees withdrawn from L2 FeeVaults. + receive() external payable virtual { + // Sender must be the currently disbursing vault + if (msg.sender != _getTransientDisbursingAddress()) { + revert FeeSplitter_SenderNotCurrentVault(); + } + + uint256 newBalance = address(this).balance; + emit FeesReceived(msg.sender, msg.value, newBalance); + } + + /// @notice Withdraws funds from FeeVaults and disburses them to the recipients. + function disburseFees() external { + if (block.timestamp < lastDisbursementTime + feeDisbursementInterval) { + revert FeeSplitter_DisbursementIntervalNotReached(); + } + + // Update the last disbursement time + lastDisbursementTime = uint128(block.timestamp); + + // Pull fees into the contract + uint256 sequencerFees = _feeVaultWithdrawal(payable(Predeploys.SEQUENCER_FEE_WALLET)); + uint256 baseFees = _feeVaultWithdrawal(payable(Predeploys.BASE_FEE_VAULT)); + uint256 l1Fees = _feeVaultWithdrawal(payable(Predeploys.L1_FEE_VAULT)); + uint256 operatorFees = _feeVaultWithdrawal(payable(Predeploys.OPERATOR_FEE_VAULT)); + // Clear the transient disbursing address + _setTransientDisbursingAddress(address(0)); + + uint256 grossRevenue = sequencerFees + baseFees + operatorFees + l1Fees; + + // Revert if no fees were collected + if (grossRevenue == 0) { + revert FeeSplitter_NoFeesCollected(); + } + + // Call to the sharesCalculator to determine the fee share recipients, amounts, withdrawal networks, and data + // DoS risk if array size is too large. + (ISharesCalculator.ShareInfo[] memory shareInfo) = + sharesCalculator.getRecipientsAndAmounts(sequencerFees, baseFees, operatorFees, l1Fees); + + uint256 shareInfoLength = shareInfo.length; + + // Ensure the share calculator returned valid data + if (shareInfoLength == 0) revert FeeSplitter_FeeShareInfoEmpty(); + + // Loop through the recipients and their corresponding fee shares + uint256 totalFeesDisbursed; + for (uint256 i; i < shareInfoLength; i++) { + uint256 feesAmount = shareInfo[i].amount; + + // Ensure the fee share is greater than zero + if (feesAmount == 0) continue; + + bool success = SafeCall.send(shareInfo[i].recipient, feesAmount); + if (!success) { + revert FeeSplitter_FailedToSendToRevenueShareRecipient(); + } + totalFeesDisbursed += feesAmount; + } + + // Ensure the total fees disbursed is equal to the gross revenue + /// NOTE: Contract can hold some balance after disbursement if tokens are force sent (using SELFDESTRUCT). + if (totalFeesDisbursed != grossRevenue) revert FeeSplitter_SharesCalculatorMalformedOutput(); + + emit FeesDisbursed({ shareInfo: shareInfo, grossRevenue: grossRevenue }); + } + + /// @notice Updates the fee disbursement interval. Only callable by the ProxyAdmin owner. + /// @param _newFeeDisbursementInterval The new fee disbursement interval in seconds. + function setFeeDisbursementInterval(uint128 _newFeeDisbursementInterval) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { + revert FeeSplitter_OnlyProxyAdminOwner(); + } + if (_newFeeDisbursementInterval == 0) { + revert FeeSplitter_FeeDisbursementIntervalCannotBeZero(); + } + if (_newFeeDisbursementInterval > MAX_DISBURSEMENT_INTERVAL) { + revert FeeSplitter_ExceedsMaxFeeDisbursementTime(); + } + uint128 oldFeeDisbursementInterval = feeDisbursementInterval; + feeDisbursementInterval = _newFeeDisbursementInterval; + emit FeeDisbursementIntervalUpdated(oldFeeDisbursementInterval, _newFeeDisbursementInterval); + } + + /// @notice Updates the share calculator contract. Only callable by the ProxyAdmin owner. + /// @param _newSharesCalculator The new share calculator contract. + function setSharesCalculator(ISharesCalculator _newSharesCalculator) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { + revert FeeSplitter_OnlyProxyAdminOwner(); + } + if (address(_newSharesCalculator) == address(0)) revert FeeSplitter_SharesCalculatorCannotBeZero(); + address oldSharesCalculator = address(sharesCalculator); + sharesCalculator = _newSharesCalculator; + emit SharesCalculatorUpdated(oldSharesCalculator, address(_newSharesCalculator)); + } + + /// @notice Checks & Withdraws fees from a FeeVault. + /// @dev Withdrawal will only occur if the vault is properly configured. + /// The FeeVault itself will enforce minimum withdrawal requirements. + /// @param _feeVault The address of the FeeVault to withdraw from. + /// @return value_ The amount of ETH that was withdrawn from the vault. + function _feeVaultWithdrawal(address payable _feeVault) internal returns (uint256 value_) { + if (IFeeVault(_feeVault).withdrawalNetwork() != Types.WithdrawalNetwork.L2) { + revert FeeSplitter_FeeVaultMustWithdrawToL2(); + } + if (IFeeVault(_feeVault).recipient() != address(this)) { + revert FeeSplitter_FeeVaultMustWithdrawToFeeSplitter(); + } + + uint256 balanceBefore = address(this).balance; + _setTransientDisbursingAddress(address(_feeVault)); + value_ = IFeeVault(_feeVault).withdraw(); + uint256 balanceAfter = address(this).balance; + + if (balanceAfter - balanceBefore != value_) { + revert FeeSplitter_FeeVaultWithdrawalAmountMismatch(); + } + } + + /// @notice Sets the transient disbursing address. + /// @param _allowedCaller The address of the vault allowed to call receive(). + function _setTransientDisbursingAddress(address _allowedCaller) internal { + assembly { + tstore(_FEE_SPLITTER_DISBURSING_ADDRESS_SLOT, _allowedCaller) + } + } + + /// @notice Reads the transient disbursing address. + /// @return allowedCaller_ The address of the vault currently allowed to call receive(). + function _getTransientDisbursingAddress() internal view returns (address allowedCaller_) { + assembly { + allowedCaller_ := tload(_FEE_SPLITTER_DISBURSING_ADDRESS_SLOT) + } + } +} diff --git a/packages/contracts-bedrock/src/L2/FeeVault.sol b/packages/contracts-bedrock/src/L2/FeeVault.sol index 86c94fc3f6dca..7443d2b57f97f 100644 --- a/packages/contracts-bedrock/src/L2/FeeVault.sol +++ b/packages/contracts-bedrock/src/L2/FeeVault.sol @@ -1,47 +1,45 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity 0.8.25; // Libraries import { SafeCall } from "src/libraries/SafeCall.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Types } from "src/libraries/Types.sol"; // Interfaces import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -// Libraries -import { Types } from "src/libraries/Types.sol"; +// External +// import from openzeppelin-contracts-v5 +import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; /// @title FeeVault /// @notice The FeeVault contract contains the basic logic for the various different vault contracts /// used to hold fee revenue generated by the L2 system. -abstract contract FeeVault { - /// @notice Minimum balance before a withdrawal can be triggered. - /// Use the `minWithdrawalAmount()` getter as this is deprecated - /// and is subject to be removed in the future. - /// @custom:legacy - uint256 public immutable MIN_WITHDRAWAL_AMOUNT; - - /// @notice Account that will receive the fees. Can be located on L1 or L2. - /// Use the `recipient()` getter as this is deprecated - /// and is subject to be removed in the future. - /// @custom:legacy - address public immutable RECIPIENT; - - /// @notice Network which the recipient will receive fees on. - /// Use the `withdrawalNetwork()` getter as this is deprecated - /// and is subject to be removed in the future. - /// @custom:legacy - Types.WithdrawalNetwork public immutable WITHDRAWAL_NETWORK; +abstract contract FeeVault is Initializable { + /// @notice Error thrown when a function meant to be called by the ProxyAdmin owner is called by another account. + error FeeVault_OnlyProxyAdminOwner(); /// @notice The minimum gas limit for the FeeVault withdrawal transaction. - uint32 internal constant WITHDRAWAL_MIN_GAS = 400_000; + uint32 internal constant _WITHDRAWAL_MIN_GAS = 400_000; /// @notice Total amount of wei processed by the contract. uint256 public totalProcessed; - /// @notice Reserve extra slots in the storage layout for future upgrades. - uint256[48] private __gap; + /// @notice Minimum balance before a withdrawal can be triggered. + uint256 public minWithdrawalAmount; + + /// @notice Account that will receive the fees. Can be located on L1 or L2. + address public recipient; + + /// @notice Network which the recipient will receive fees on. + Types.WithdrawalNetwork public withdrawalNetwork; + + /// @notice Reserve extra slots in the storage layout for future upgrades, 50 in total. + uint256[46] private __gap; + /// @custom:legacy /// @notice Emitted each time a withdrawal occurs. This event will be deprecated /// in favor of the Withdrawal event containing the WithdrawalNetwork parameter. /// @param value Amount that was withdrawn (in wei). @@ -56,55 +54,144 @@ abstract contract FeeVault { /// @param withdrawalNetwork Network which the to address will receive funds on. event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); + /// @notice Emitted when the minimum withdrawal amount for the vault is updated. + /// @param oldWithdrawalAmount The previous minimum withdrawal amount. + /// @param newWithdrawalAmount The new minimum withdrawal amount. + event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount); + + /// @notice Emitted when the fee recipient for this vault is updated. + /// @param oldRecipient The previous recipient address. + /// @param newRecipient The new recipient address. + event RecipientUpdated(address oldRecipient, address newRecipient); + + /// @notice Emitted when the withdrawal network for this vault is updated. + /// @param oldWithdrawalNetwork The previous withdrawal network. + /// @param newWithdrawalNetwork The new withdrawal network. + event WithdrawalNetworkUpdated( + Types.WithdrawalNetwork oldWithdrawalNetwork, Types.WithdrawalNetwork newWithdrawalNetwork + ); + + /// @notice Constructor for the FeeVault contract. + /// This constructor is intentionally empty to prevent initialization. + /// Initialization is handled by the `initialize` function. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the FeeVault contract. /// @param _recipient Wallet that will receive the fees. /// @param _minWithdrawalAmount Minimum balance for withdrawals. /// @param _withdrawalNetwork Network which the recipient will receive fees on. - constructor(address _recipient, uint256 _minWithdrawalAmount, Types.WithdrawalNetwork _withdrawalNetwork) { - RECIPIENT = _recipient; - MIN_WITHDRAWAL_AMOUNT = _minWithdrawalAmount; - WITHDRAWAL_NETWORK = _withdrawalNetwork; + function initialize( + address _recipient, + uint256 _minWithdrawalAmount, + Types.WithdrawalNetwork _withdrawalNetwork + ) + external + initializer + { + recipient = _recipient; + minWithdrawalAmount = _minWithdrawalAmount; + withdrawalNetwork = _withdrawalNetwork; } /// @notice Allow the contract to receive ETH. receive() external payable { } - /// @notice Minimum balance before a withdrawal can be triggered. - function minWithdrawalAmount() public view returns (uint256 amount_) { - amount_ = MIN_WITHDRAWAL_AMOUNT; + /// @notice Updates the minimum amount of funds the FeeVault contract must hold before they can be + /// withdrawn. + /// @param _newMinWithdrawalAmount The new minimum withdrawal amount. + /// @dev If integrating the FeeSplitter contract, the minimum withdrawal amount must be set to 0 to + /// avoid blocking withdrawals and disbursements for all vaults if one vault doesn't reach the threshold. + function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { + revert FeeVault_OnlyProxyAdminOwner(); + } + + uint256 oldWithdrawalAmount = minWithdrawalAmount; + minWithdrawalAmount = _newMinWithdrawalAmount; + + emit MinWithdrawalAmountUpdated(oldWithdrawalAmount, _newMinWithdrawalAmount); } - /// @notice Account that will receive the fees. Can be located on L1 or L2. - function recipient() public view returns (address recipient_) { - recipient_ = RECIPIENT; + /// @notice Updates the recipient of vault fees when they are withdrawn from the vault. + /// @param _newRecipient The new recipient address. + function setRecipient(address _newRecipient) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { + revert FeeVault_OnlyProxyAdminOwner(); + } + + address oldRecipient = recipient; + recipient = _newRecipient; + + emit RecipientUpdated(oldRecipient, _newRecipient); } - /// @notice Network which the recipient will receive fees on. - function withdrawalNetwork() public view returns (Types.WithdrawalNetwork network_) { - network_ = WITHDRAWAL_NETWORK; + /// @notice Updates the network to which vault fees will be withdrawn. This can be either WithdrawalNetwork.L1 + /// to withdraw them to an address on L1 by using the L2ToL1MessagePasser predeploy, or WithdrawalNetwork.L2 to + /// withdraw them to an address on the same chain. + /// @param _newWithdrawalNetwork The new withdrawal network. + function setWithdrawalNetwork(Types.WithdrawalNetwork _newWithdrawalNetwork) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { + revert FeeVault_OnlyProxyAdminOwner(); + } + + Types.WithdrawalNetwork oldWithdrawalNetwork = withdrawalNetwork; + withdrawalNetwork = _newWithdrawalNetwork; + + emit WithdrawalNetworkUpdated(oldWithdrawalNetwork, _newWithdrawalNetwork); } /// @notice Triggers a withdrawal of funds to the fee wallet on L1 or L2. - function withdraw() external { + /// @return value_ The amount of ETH that was withdrawn. + function withdraw() external returns (uint256 value_) { require( - address(this).balance >= MIN_WITHDRAWAL_AMOUNT, + address(this).balance >= minWithdrawalAmount, "FeeVault: withdrawal amount must be greater than minimum withdrawal amount" ); - uint256 value = address(this).balance; - totalProcessed += value; + value_ = address(this).balance; + totalProcessed += value_; - emit Withdrawal(value, RECIPIENT, msg.sender); - emit Withdrawal(value, RECIPIENT, msg.sender, WITHDRAWAL_NETWORK); + address recipientAddr = recipient; - if (WITHDRAWAL_NETWORK == Types.WithdrawalNetwork.L2) { - bool success = SafeCall.send(RECIPIENT, value); + emit Withdrawal(value_, recipientAddr, msg.sender); + emit Withdrawal(value_, recipientAddr, msg.sender, withdrawalNetwork); + + if (withdrawalNetwork == Types.WithdrawalNetwork.L2) { + bool success = SafeCall.send(recipientAddr, value_); require(success, "FeeVault: failed to send ETH to L2 fee recipient"); } else { - IL2ToL1MessagePasser(payable(Predeploys.L2_TO_L1_MESSAGE_PASSER)).initiateWithdrawal{ value: value }({ - _target: RECIPIENT, - _gasLimit: WITHDRAWAL_MIN_GAS, + IL2ToL1MessagePasser(payable(Predeploys.L2_TO_L1_MESSAGE_PASSER)).initiateWithdrawal{ value: value_ }({ + _target: recipientAddr, + _gasLimit: _WITHDRAWAL_MIN_GAS, _data: hex"" }); } } + + /// @custom:legacy + /// @notice Minimum balance before a withdrawal can be triggered. + /// Use the `minWithdrawalAmount()` getter as this is deprecated + /// and is subject to be removed in the future. + function MIN_WITHDRAWAL_AMOUNT() public view returns (uint256) { + return minWithdrawalAmount; + } + + /// @custom:legacy + /// @notice Account that will receive the fees. Can be located on L1 or L2. + /// Use the `recipient()` getter as this is deprecated + /// and is subject to be removed in the future. + /// @return The recipient address. + function RECIPIENT() public view returns (address) { + return recipient; + } + + /// @custom:legacy + /// @notice Network which the recipient will receive fees on. + /// Use the `withdrawalNetwork()` getter as this is deprecated + /// and is subject to be removed in the future. + function WITHDRAWAL_NETWORK() public view returns (Types.WithdrawalNetwork) { + return withdrawalNetwork; + } } diff --git a/packages/contracts-bedrock/src/L2/L1FeeVault.sol b/packages/contracts-bedrock/src/L2/L1FeeVault.sol index de1a212b5e9d3..5e422d95dbb65 100644 --- a/packages/contracts-bedrock/src/L2/L1FeeVault.sol +++ b/packages/contracts-bedrock/src/L2/L1FeeVault.sol @@ -1,12 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity 0.8.25; // Contracts import { FeeVault } from "src/L2/FeeVault.sol"; -// Libraries -import { Types } from "src/libraries/Types.sol"; - // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -16,18 +13,6 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// @notice The L1FeeVault accumulates the L1 portion of the transaction fees. contract L1FeeVault is FeeVault, ISemver { /// @notice Semantic version. - /// @custom:semver 1.5.1 - string public constant version = "1.5.1"; - - /// @notice Constructs the L1FeeVault contract. - /// @param _recipient Wallet that will receive the fees. - /// @param _minWithdrawalAmount Minimum balance for withdrawals. - /// @param _withdrawalNetwork Network which the recipient will receive fees on. - constructor( - address _recipient, - uint256 _minWithdrawalAmount, - Types.WithdrawalNetwork _withdrawalNetwork - ) - FeeVault(_recipient, _minWithdrawalAmount, _withdrawalNetwork) - { } + /// @custom:semver 1.6.0 + string public constant version = "1.6.0"; } diff --git a/packages/contracts-bedrock/src/L2/L1Withdrawer.sol b/packages/contracts-bedrock/src/L2/L1Withdrawer.sol new file mode 100644 index 0000000000000..bbaf18e46389e --- /dev/null +++ b/packages/contracts-bedrock/src/L2/L1Withdrawer.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IL2CrossDomainMessenger } from "interfaces/L2/IL2CrossDomainMessenger.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +/// @title L1Withdrawer +/// @notice A contract that receives ETH and automatically initiates withdrawals to L1 when a +/// minimum balance threshold is reached. This contract is designed to be used as a +/// recipient for the FeeSplitter contract and is part of the revenue sharing standard contracts. +contract L1Withdrawer is ISemver { + /// @notice Thrown when the caller is not the ProxyAdmin owner. + error L1Withdrawer_OnlyProxyAdminOwner(); + + /// @notice The minimum amount of ETH that must be accumulated before a withdrawal is initiated. + uint256 public minWithdrawalAmount; + + /// @notice The L1 address that will receive the withdrawn ETH. + address public recipient; + + /// @notice The L1 gas limit set when initiating withdrawals. + /// @dev withdrawalGasLimit should be overestimated to account for expensive receive() + uint32 public withdrawalGasLimit; + + /// @notice Emitted when a withdrawal to L1 is initiated. + /// @param recipient The L1 address receiving the withdrawal. + /// @param amount The amount of ETH being withdrawn. + event WithdrawalInitiated(address indexed recipient, uint256 amount); + + /// @notice Emitted when the contract receives funds. + /// @param sender The address that sent the funds. + /// @param amount The amount of ETH received. + /// @param newBalance The new balance after receiving funds. + event FundsReceived(address indexed sender, uint256 amount, uint256 newBalance); + + /// @notice Emitted when the minimum withdrawal amount is updated. + /// @param oldMinWithdrawalAmount The previous minimum withdrawal amount. + /// @param newMinWithdrawalAmount The new minimum withdrawal amount. + event MinWithdrawalAmountUpdated(uint256 oldMinWithdrawalAmount, uint256 newMinWithdrawalAmount); + + /// @notice Emitted when the recipient is updated. + /// @param oldRecipient The previous recipient address. + /// @param newRecipient The new recipient address. + event RecipientUpdated(address oldRecipient, address newRecipient); + + /// @notice Emitted when the withdrawal gas limit is updated. + /// @param oldWithdrawalGasLimit The previous withdrawal gas limit. + /// @param newWithdrawalGasLimit The new withdrawal gas limit. + event WithdrawalGasLimitUpdated(uint32 oldWithdrawalGasLimit, uint32 newWithdrawalGasLimit); + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice Constructs the L1Withdrawer contract. + /// @param _minWithdrawalAmount The minimum amount of ETH required to trigger a withdrawal. + /// @param _recipient The L1 address that will receive withdrawals. + /// @param _withdrawalGasLimit The gas limit for the L1 withdrawal transaction. + /// @dev If target on L1 is `FeesDepositor`, the gas limit should be above 800k gas. + constructor(uint256 _minWithdrawalAmount, address _recipient, uint32 _withdrawalGasLimit) { + minWithdrawalAmount = _minWithdrawalAmount; + recipient = _recipient; + withdrawalGasLimit = _withdrawalGasLimit; + } + + /// @notice Receives ETH and initiates a withdrawal to L1 if the balance meets the threshold. + receive() external payable { + uint256 balance = address(this).balance; + emit FundsReceived(msg.sender, msg.value, balance); + + if (balance >= minWithdrawalAmount) { + IL2CrossDomainMessenger(Predeploys.L2_CROSS_DOMAIN_MESSENGER).sendMessage{ value: balance }( + recipient, hex"", withdrawalGasLimit + ); + + emit WithdrawalInitiated(recipient, balance); + } + } + + /// @notice Updates the minimum withdrawal amount. Only callable by the ProxyAdmin owner. + /// @param _newMinWithdrawalAmount The new minimum withdrawal amount. + function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { + revert L1Withdrawer_OnlyProxyAdminOwner(); + } + uint256 oldMinWithdrawalAmount = minWithdrawalAmount; + minWithdrawalAmount = _newMinWithdrawalAmount; + emit MinWithdrawalAmountUpdated(oldMinWithdrawalAmount, _newMinWithdrawalAmount); + } + + /// @notice Updates the recipient address. Only callable by the ProxyAdmin owner. + /// @dev The recipient MUST be able to receive ether or L1Withdrawer#receive will fail + /// when the withdrawal is finalized. + /// @param _newRecipient The new recipient address. + function setRecipient(address _newRecipient) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { + revert L1Withdrawer_OnlyProxyAdminOwner(); + } + address oldRecipient = recipient; + recipient = _newRecipient; + emit RecipientUpdated(oldRecipient, _newRecipient); + } + + /// @notice Updates the withdrawal gas limit. Only callable by the ProxyAdmin owner. + /// @param _newWithdrawalGasLimit The new withdrawal gas limit. + /// @dev If target on L1 is `FeesDepositor`, the gas limit should be above 800k gas. + function setWithdrawalGasLimit(uint32 _newWithdrawalGasLimit) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { + revert L1Withdrawer_OnlyProxyAdminOwner(); + } + uint32 oldWithdrawalGasLimit = withdrawalGasLimit; + withdrawalGasLimit = _newWithdrawalGasLimit; + emit WithdrawalGasLimitUpdated(oldWithdrawalGasLimit, _newWithdrawalGasLimit); + } +} diff --git a/packages/contracts-bedrock/src/L2/OperatorFeeVault.sol b/packages/contracts-bedrock/src/L2/OperatorFeeVault.sol index b7341824d6f80..ca51c9ea40325 100644 --- a/packages/contracts-bedrock/src/L2/OperatorFeeVault.sol +++ b/packages/contracts-bedrock/src/L2/OperatorFeeVault.sol @@ -1,13 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity 0.8.25; // Contracts import { FeeVault } from "src/L2/FeeVault.sol"; -// Libraries -import { Types } from "src/libraries/Types.sol"; -import { Predeploys } from "src/libraries/Predeploys.sol"; - // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -17,10 +13,6 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// @notice The OperatorFeeVault accumulates the operator portion of the transaction fees. contract OperatorFeeVault is FeeVault, ISemver { /// @notice Semantic version. - /// @custom:semver 1.0.0 - string public constant version = "1.0.0"; - - /// @notice Constructs the OperatorFeeVault contract. - /// Funds are withdrawn to the base fee vault on the L2 network. - constructor() FeeVault(Predeploys.BASE_FEE_VAULT, 0, Types.WithdrawalNetwork.L2) { } + /// @custom:semver 1.1.0 + string public constant version = "1.1.0"; } diff --git a/packages/contracts-bedrock/src/L2/SequencerFeeVault.sol b/packages/contracts-bedrock/src/L2/SequencerFeeVault.sol index 0b4401deed45f..5c91b324270c0 100644 --- a/packages/contracts-bedrock/src/L2/SequencerFeeVault.sol +++ b/packages/contracts-bedrock/src/L2/SequencerFeeVault.sol @@ -1,12 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity 0.8.25; // Contracts import { FeeVault } from "src/L2/FeeVault.sol"; -// Libraries -import { Types } from "src/libraries/Types.sol"; - // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -16,25 +13,13 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// @notice The SequencerFeeVault is the contract that holds any fees paid to the Sequencer during /// transaction processing and block production. contract SequencerFeeVault is FeeVault, ISemver { - /// @custom:semver 1.5.1 - string public constant version = "1.5.1"; - - /// @notice Constructs the SequencerFeeVault contract. - /// @param _recipient Wallet that will receive the fees. - /// @param _minWithdrawalAmount Minimum balance for withdrawals. - /// @param _withdrawalNetwork Network which the recipient will receive fees on. - constructor( - address _recipient, - uint256 _minWithdrawalAmount, - Types.WithdrawalNetwork _withdrawalNetwork - ) - FeeVault(_recipient, _minWithdrawalAmount, _withdrawalNetwork) - { } + /// @custom:semver 1.6.0 + string public constant version = "1.6.0"; /// @custom:legacy /// @notice Legacy getter for the recipient address. /// @return The recipient address. function l1FeeWallet() public view returns (address) { - return RECIPIENT; + return recipient; } } diff --git a/packages/contracts-bedrock/src/L2/SuperchainRevSharesCalculator.sol b/packages/contracts-bedrock/src/L2/SuperchainRevSharesCalculator.sol new file mode 100644 index 0000000000000..7270140a34f80 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/SuperchainRevSharesCalculator.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { ISharesCalculator } from "interfaces/L2/ISharesCalculator.sol"; + +/// @title SuperchainRevSharesCalculator +/// @notice Calculator for Superchain revenue share. 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 remaining revenue. +contract SuperchainRevSharesCalculator is ISemver, ISharesCalculator { + /// @notice Emitted when the share recipient is updated. + /// @param oldShareRecipient The old share recipient address. + /// @param newShareRecipient The new share recipient address. + event ShareRecipientUpdated(address indexed oldShareRecipient, address indexed newShareRecipient); + + /// @notice Emitted when the remainder recipient is updated. + /// @param oldRemainderRecipient The old remainder recipient address. + /// @param newRemainderRecipient The new remainder recipient address. + event RemainderRecipientUpdated(address indexed oldRemainderRecipient, address indexed newRemainderRecipient); + + /// @notice Thrown when the caller is not the ProxyAdmin owner. + error SharesCalculator_OnlyProxyAdminOwner(); + + /// @notice Thrown when the gross share is zero. + error SharesCalculator_ZeroGrossShare(); + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice Basis points scale. + uint32 public constant BASIS_POINT_SCALE = 10_000; + + /// @notice Gross revenue share in basis points (2.5%). + uint32 public constant GROSS_SHARE_BPS = 250; + + /// @notice Net revenue share in basis points (15%). + uint32 public constant NET_SHARE_BPS = 1_500; + + /// @notice Address that receives the Superchain revenue share. + address payable public shareRecipient; + + /// @notice Address that receives the remainder of the revenue. + address payable public remainderRecipient; + + /// @notice Constructs the contract with an initial configuration. + /// @param _shareRecipient Recipient of the Superchain revenue share. + /// @param _remainderRecipient Recipient of the remainder. + constructor(address payable _shareRecipient, address payable _remainderRecipient) { + shareRecipient = _shareRecipient; + remainderRecipient = _remainderRecipient; + } + + /// @notice Returns the recipients and amounts for fee distribution. + /// @dev The recipients returned MUST be able to receive ether or FeeSplitter#disburseFees will fail. + /// @param _sequencerFeeRevenue Revenue from sequencer fees. + /// @param _baseFeeRevenue Revenue from base fees. + /// @param _operatorFeeRevenue Revenue from operator fees. + /// @param _l1FeeRevenue Revenue from L1 fees. + /// @return shareInfo_ Array of ShareInfo structs containing recipients and amounts. + function getRecipientsAndAmounts( + uint256 _sequencerFeeRevenue, + uint256 _baseFeeRevenue, + uint256 _operatorFeeRevenue, + uint256 _l1FeeRevenue + ) + external + view + returns (ShareInfo[] memory shareInfo_) + { + // Two recipients: share recipient first (explicit amount), remainder recipient second (0; FeeSplitter sends + // remainder) + shareInfo_ = new ShareInfo[](2); + shareInfo_[0] = ShareInfo({ recipient: shareRecipient, amount: 0 }); + shareInfo_[1] = ShareInfo({ recipient: remainderRecipient, amount: 0 }); + + // Gross component: 2.5% of total revenue. + uint256 grossRevenue = _sequencerFeeRevenue + _baseFeeRevenue + _operatorFeeRevenue + _l1FeeRevenue; + uint256 grossShare = (grossRevenue * uint256(GROSS_SHARE_BPS)) / BASIS_POINT_SCALE; + + // Ensure gross share is greater than zero + if (grossShare == 0) { + revert SharesCalculator_ZeroGrossShare(); + } + + // Net component: 15% of (total - L1 fees). + uint256 netRevenue = grossRevenue - _l1FeeRevenue; + uint256 netShare = (netRevenue * uint256(NET_SHARE_BPS)) / BASIS_POINT_SCALE; + + uint256 amountToShareRecipient = grossShare > netShare ? grossShare : netShare; + + // Set the share amount and the remainder. + shareInfo_[0].amount = amountToShareRecipient; + shareInfo_[1].amount = grossRevenue - amountToShareRecipient; + } + + /// @notice Sets the share recipient. Only callable by the ProxyAdmin owner. + /// @param _newShareRecipient The new share recipient address. + function setShareRecipient(address payable _newShareRecipient) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { + revert SharesCalculator_OnlyProxyAdminOwner(); + } + address oldShareRecipient = shareRecipient; + shareRecipient = _newShareRecipient; + emit ShareRecipientUpdated(oldShareRecipient, _newShareRecipient); + } + + /// @notice Sets the remainder recipient. Only callable by the ProxyAdmin owner. + /// @param _newRemainderRecipient The new remainder recipient address. + function setRemainderRecipient(address payable _newRemainderRecipient) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { + revert SharesCalculator_OnlyProxyAdminOwner(); + } + address oldRemainderRecipient = remainderRecipient; + remainderRecipient = _newRemainderRecipient; + emit RemainderRecipientUpdated(oldRemainderRecipient, _newRemainderRecipient); + } +} diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index baeb6a143575c..92668d6cc6a5a 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -113,6 +113,9 @@ library Predeploys { /// @notice Address of the SuperchainTokenBridge predeploy. address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028; + /// @notice Address of the FeeSplitter predeploy. + address internal constant FEE_SPLITTER = 0x420000000000000000000000000000000000002B; + /// @notice Returns the name of the predeploy at the given address. function getName(address _addr) internal pure returns (string memory out_) { require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy"); @@ -145,6 +148,7 @@ library Predeploys { if (_addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) return "OptimismSuperchainERC20Factory"; if (_addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON) return "OptimismSuperchainERC20Beacon"; if (_addr == SUPERCHAIN_TOKEN_BRIDGE) return "SuperchainTokenBridge"; + if (_addr == FEE_SPLITTER) return "FeeSplitter"; revert("Predeploys: unnamed predeploy"); } @@ -169,7 +173,7 @@ library Predeploys { || _addr == L2_ERC721_BRIDGE || _addr == L1_BLOCK_ATTRIBUTES || _addr == L2_TO_L1_MESSAGE_PASSER || _addr == OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == PROXY_ADMIN || _addr == BASE_FEE_VAULT || _addr == L1_FEE_VAULT || _addr == OPERATOR_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS - || _addr == GOVERNANCE_TOKEN + || _addr == GOVERNANCE_TOKEN || _addr == FEE_SPLITTER || (_fork >= uint256(Fork.INTEROP) && _enableCrossL2Inbox && _addr == CROSS_L2_INBOX) || (_fork >= uint256(Fork.INTEROP) && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER); } diff --git a/packages/contracts-bedrock/test/L1/FeesDepositor.t.sol b/packages/contracts-bedrock/test/L1/FeesDepositor.t.sol new file mode 100644 index 0000000000000..5c253b4a7cd4f --- /dev/null +++ b/packages/contracts-bedrock/test/L1/FeesDepositor.t.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { CommonTest } from "test/setup/CommonTest.sol"; +import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { IFeesDepositor } from "interfaces/L1/IFeesDepositor.sol"; +import { FeesDepositor } from "src/L1/FeesDepositor.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; +import { Proxy } from "src/universal/Proxy.sol"; +import { Features } from "src/libraries/Features.sol"; + +/// @title FeesDepositor_TestInit +/// @notice Base test contract with initialization for `FeesDepositor` tests. +contract FeesDepositor_TestInit is CommonTest { + // Events + event FeesDeposited(address indexed l2Recipient, uint256 amount); + event FundsReceived(address indexed sender, uint256 amount, uint256 newBalance); + event MinDepositAmountUpdated(uint96 oldMinDepositAmount, uint96 newMinDepositAmount); + event L2RecipientUpdated(address oldL2Recipient, address newL2Recipient); + event GasLimitUpdated(uint32 oldGasLimit, uint32 newGasLimit); + + // Test state + FeesDepositor feesDepositor; + address l2Recipient = makeAddr("l2Recipient"); + uint96 minDepositAmount = 1 ether; + uint32 gasLimit = 150_000; + address depositFeesRecipient; + + /// @notice Test setup. + function setUp() public virtual override { + super.setUp(); + + // Deploy FeesDepositor implementation + address implementation = DeployUtils.create1({ + _name: "FeesDepositor", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IFeesDepositor.__constructor__, ())) + }); + + // Deploy proxy pointing to proxyAdmin + address proxy = address(new Proxy(address(proxyAdmin))); + + // Set implementation + vm.prank(address(proxyAdmin)); + Proxy(payable(proxy)).upgradeTo(implementation); + + // Cast proxy to FeesDepositor + feesDepositor = FeesDepositor(payable(proxy)); + + // Initialize through proxy + vm.prank(proxyAdminOwner); + feesDepositor.initialize(minDepositAmount, l2Recipient, l1CrossDomainMessenger, gasLimit); + + // Set depositFeesRecipient + depositFeesRecipient = + systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX) ? address(ethLockbox) : address(optimismPortal2); + } +} + +/// @title FeesDepositor_Initialize_Test +/// @notice Tests the initialization of the `FeesDepositor` contract. +contract FeesDepositor_Initialize_Test is FeesDepositor_TestInit { + /// @notice This contract is excluded from the Initializable.t.sol test because it is not deployed as part of the + /// standard deployment script and instead is deployed manually, that's why we have this test. + function test_cannotReinitialize_succeeds() public { + vm.expectRevert("Initializable: contract is already initialized"); + feesDepositor.initialize(minDepositAmount, l2Recipient, l1CrossDomainMessenger, gasLimit); + } +} + +/// @title FeesDepositor_Receive_Test +/// @notice Tests the receive function of the `FeesDepositor` contract. +contract FeesDepositor_Receive_Test is FeesDepositor_TestInit { + function testFuzz_receive_belowThreshold_succeeds(uint256 _amount) external { + // Handling the fork tests scenario + uint256 depositFeesRecipientBalanceBefore = depositFeesRecipient.balance; + _amount = bound(_amount, 0, minDepositAmount - 1); + + vm.deal(address(this), _amount); + + vm.expectEmit(address(feesDepositor)); + emit FundsReceived(address(this), _amount, _amount); + + // Expect call to the messenger not to be done + vm.expectCall( + address(l1CrossDomainMessenger), + _amount, + abi.encodeCall(ICrossDomainMessenger.sendMessage, (l2Recipient, hex"", gasLimit)), + 0 + ); + + (bool success,) = address(feesDepositor).call{ value: _amount }(""); + + assertTrue(success); + assertEq(address(feesDepositor).balance, _amount); + assertEq(address(depositFeesRecipient).balance, depositFeesRecipientBalanceBefore); + } + + function testFuzz_receive_atOrAboveThreshold_succeeds(uint256 _sendAmount) external { + // Handling the fork tests scenario case for the fork tests + uint256 depositFeesRecipientBalanceBefore = depositFeesRecipient.balance; + _sendAmount = bound(_sendAmount, minDepositAmount, type(uint256).max - depositFeesRecipientBalanceBefore); + + vm.deal(address(this), _sendAmount); + + vm.expectEmit(address(feesDepositor)); + emit FundsReceived(address(this), _sendAmount, _sendAmount); + + vm.expectEmit(address(feesDepositor)); + emit FeesDeposited(l2Recipient, _sendAmount); + + vm.expectCall( + address(l1CrossDomainMessenger), + _sendAmount, + abi.encodeCall(ICrossDomainMessenger.sendMessage, (l2Recipient, hex"", gasLimit)) + ); + + (bool success,) = address(feesDepositor).call{ value: _sendAmount }(""); + + assertTrue(success); + assertEq(address(feesDepositor).balance, 0); + assertEq(address(depositFeesRecipient).balance, depositFeesRecipientBalanceBefore + _sendAmount); + } + + function testFuzz_receive_multipleDeposits_succeeds(uint256 _firstAmount, uint256 _secondAmount) external { + // Handling the fork tests scenario + uint256 depositFeesRecipientBalanceBefore = depositFeesRecipient.balance; + // First amount should not exceed minDepositAmount (so it doesn't trigger deposit) + _firstAmount = bound(_firstAmount, 0, minDepositAmount - 1); + + // First deposit (should not trigger deposit) + vm.deal(address(this), _firstAmount); + + vm.expectEmit(address(feesDepositor)); + emit FundsReceived(address(this), _firstAmount, _firstAmount); + + (bool success1,) = address(feesDepositor).call{ value: _firstAmount }(""); + assertTrue(success1); + assertEq(address(feesDepositor).balance, _firstAmount); + assertEq( + address(depositFeesRecipient).balance, depositFeesRecipientBalanceBefore, "depositFeesRecipient balance 1" + ); + + // Second amount should ensure total reaches threshold to trigger deposit + _secondAmount = bound( + _secondAmount, + minDepositAmount - _firstAmount, + type(uint256).max - depositFeesRecipient.balance - _firstAmount + ); + + uint256 totalAmount = _firstAmount + _secondAmount; + + // Second deposit (will trigger deposit since total >= minDepositAmount) + vm.deal(address(this), _secondAmount); + + vm.expectEmit(address(feesDepositor)); + emit FundsReceived(address(this), _secondAmount, totalAmount); + + vm.expectEmit(address(feesDepositor)); + emit FeesDeposited(l2Recipient, totalAmount); + + vm.expectCall( + address(l1CrossDomainMessenger), + totalAmount, + abi.encodeCall(ICrossDomainMessenger.sendMessage, (l2Recipient, hex"", gasLimit)) + ); + + (bool success2,) = address(feesDepositor).call{ value: _secondAmount }(""); + assertTrue(success2); + + // Verify deposit occurred + assertEq(address(feesDepositor).balance, 0); + assertEq( + address(depositFeesRecipient).balance, + depositFeesRecipientBalanceBefore + totalAmount, + "depositFeesRecipient balance 2" + ); + } +} + +/// @title FeesDepositor_SetMinDepositAmount_Test +/// @notice Tests the setMinDepositAmount function of the `FeesDepositor` contract. +contract FeesDepositor_SetMinDepositAmount_Test is FeesDepositor_TestInit { + function testFuzz_setMinDepositAmount_asOwner_succeeds(uint96 _newMinDepositAmount) external { + address owner = proxyAdmin.owner(); + + vm.expectEmit(address(feesDepositor)); + emit MinDepositAmountUpdated(minDepositAmount, _newMinDepositAmount); + + vm.prank(owner); + feesDepositor.setMinDepositAmount(_newMinDepositAmount); + + assertEq(feesDepositor.minDepositAmount(), _newMinDepositAmount); + } + + function testFuzz_setMinDepositAmount_asNonOwner_reverts(address _caller) external { + address owner = proxyAdmin.owner(); + vm.assume(_caller != owner); + + uint96 newMinDepositAmount = 2 ether; + + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); + vm.prank(_caller); + feesDepositor.setMinDepositAmount(newMinDepositAmount); + + assertEq(feesDepositor.minDepositAmount(), minDepositAmount); + } +} + +/// @title FeesDepositor_SetL2Recipient_Test +/// @notice Tests the setL2Recipient function of the `FeesDepositor` contract. +contract FeesDepositor_SetL2Recipient_Test is FeesDepositor_TestInit { + function testFuzz_setL2Recipient_asOwner_succeeds(address _newL2Recipient) external { + address owner = proxyAdmin.owner(); + + vm.expectEmit(address(feesDepositor)); + emit L2RecipientUpdated(l2Recipient, _newL2Recipient); + + vm.prank(owner); + feesDepositor.setL2Recipient(_newL2Recipient); + + assertEq(feesDepositor.l2Recipient(), _newL2Recipient); + } + + function testFuzz_setL2Recipient_asNonOwner_reverts(address _caller) external { + address owner = proxyAdmin.owner(); + vm.assume(_caller != owner); + + address newL2Recipient = makeAddr("newL2Recipient"); + + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); + vm.prank(_caller); + feesDepositor.setL2Recipient(newL2Recipient); + + assertEq(feesDepositor.l2Recipient(), l2Recipient); + } +} + +/// @title FeesDepositor_SetGasLimit_Test +/// @notice Tests the setGasLimit function of the `FeesDepositor` contract. +contract FeesDepositor_SetGasLimit_Test is FeesDepositor_TestInit { + function testFuzz_setGasLimit_asOwner_succeeds(uint32 _newGasLimit) external { + address owner = proxyAdmin.owner(); + + vm.expectEmit(address(feesDepositor)); + emit GasLimitUpdated(gasLimit, _newGasLimit); + + vm.prank(owner); + feesDepositor.setGasLimit(_newGasLimit); + + assertEq(feesDepositor.gasLimit(), _newGasLimit); + } + + function testFuzz_setGasLimit_asNonOwner_reverts(address _caller) external { + address owner = proxyAdmin.owner(); + vm.assume(_caller != owner); + + uint32 newGasLimit = 200_000; + + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); + vm.prank(_caller); + feesDepositor.setGasLimit(newGasLimit); + + assertEq(feesDepositor.gasLimit(), gasLimit); + } +} diff --git a/packages/contracts-bedrock/test/L2/BaseFeeVault.t.sol b/packages/contracts-bedrock/test/L2/BaseFeeVault.t.sol index 7f7653e47c5f4..48b48648d08eb 100644 --- a/packages/contracts-bedrock/test/L2/BaseFeeVault.t.sol +++ b/packages/contracts-bedrock/test/L2/BaseFeeVault.t.sol @@ -1,24 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { CommonTest } from "test/setup/CommonTest.sol"; +// Interfaces +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; // Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { FeeVault_Uncategorized_Test } from "test/L2/FeeVault.t.sol"; import { Types } from "src/libraries/Types.sol"; import { SemverComp } from "src/libraries/SemverComp.sol"; +import { CommonTest } from "test/setup/CommonTest.sol"; -/// @title BaseFeeVault_Constructor_Test -/// @notice Tests the `constructor` of the `BaseFeeVault` contract. -contract BaseFeeVault_Constructor_Test is CommonTest { - /// @notice Tests that the constructor sets the correct values. - function test_constructor_succeeds() external view { - assertEq(baseFeeVault.RECIPIENT(), deploy.cfg().baseFeeVaultRecipient()); - assertEq(baseFeeVault.recipient(), deploy.cfg().baseFeeVaultRecipient()); - assertEq(baseFeeVault.MIN_WITHDRAWAL_AMOUNT(), deploy.cfg().baseFeeVaultMinimumWithdrawalAmount()); - assertEq(baseFeeVault.minWithdrawalAmount(), deploy.cfg().baseFeeVaultMinimumWithdrawalAmount()); - assertEq(uint8(baseFeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L1)); - assertEq(uint8(baseFeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L1)); +/// @title BaseFeeVault_Uncategorized_Test +/// @notice Test contract for the BaseFeeVault contract's functionality +contract BaseFeeVault_Uncategorized_Test is FeeVault_Uncategorized_Test { + /// @dev Sets up the test suite. + function setUp() public virtual override { + super.setUp(); + recipient = deploy.cfg().baseFeeVaultRecipient(); + feeVaultName = "BaseFeeVault"; + minWithdrawalAmount = deploy.cfg().baseFeeVaultMinimumWithdrawalAmount(); + feeVault = IFeeVault(payable(Predeploys.BASE_FEE_VAULT)); + withdrawalNetwork = Types.WithdrawalNetwork(uint8(deploy.cfg().baseFeeVaultWithdrawalNetwork())); + // Current recipient is a contract that reverts when receiving fees, so etching empty bytes to it + vm.etch(recipient, hex""); } } diff --git a/packages/contracts-bedrock/test/L2/FeeSplitter.t.sol b/packages/contracts-bedrock/test/L2/FeeSplitter.t.sol new file mode 100644 index 0000000000000..39d92312b9754 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/FeeSplitter.t.sol @@ -0,0 +1,741 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Mocks +import { MockFeeVault } from "test/mocks/MockFeeVault.sol"; +import { MaliciousMockFeeVault } from "test/mocks/MaliciousMockFeeVault.sol"; +import { RevertingRecipient } from "test/mocks/RevertingRecipient.sol"; +import { ReentrantMockFeeVault } from "test/mocks/ReentrantMockFeeVault.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Types } from "src/libraries/Types.sol"; + +// Interfaces +import { IFeeSplitter } from "interfaces/L2/IFeeSplitter.sol"; +import { ISharesCalculator } from "interfaces/L2/ISharesCalculator.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; + +/// @title FeeSplitter_TestInit +/// @notice Reusable test initialization for `FeeSplitter` tests. +contract FeeSplitter_TestInit is CommonTest { + // Events + event FeesReceived(address indexed sender, uint256 amount, uint256 newBalance); + event FeeDisbursementIntervalUpdated(uint128 oldFeeDisbursementInterval, uint128 newFeeDisbursementInterval); + event FeesDisbursed(ISharesCalculator.ShareInfo[] shareInfo, uint256 grossRevenue); + event SharesCalculatorUpdated(address oldSharesCalculator, address newSharesCalculator); + + // Test constants + address internal _owner; + address internal _defaultRevenueShareRecipient = makeAddr("RevenueShareRecipient"); + address internal _defaultRevenueRemainderRecipient = makeAddr("RemainderRecipient"); + uint128 internal _defaultFeeDisbursementInterval = 1 days; + address internal _defaultSharesCalculator = makeAddr("SharesCalculator"); + address[4] internal _feeVaults; + bytes32 internal constant _FEE_SPLITTER_DISBURSING_ADDRESS_SLOT = + 0x21346dddac42cc163a6523eefc19df981df7352c870dc3b0b17a6a92fc6fe813; + + /// @notice Test setup. + function setUp() public virtual override { + // Enable revenue sharing before calling parent setUp + super.enableRevenueShare(); + super.setUp(); + + // Get the owner from ProxyAdmin + _owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); + + // Initialize fee vaults array + _feeVaults[0] = Predeploys.SEQUENCER_FEE_WALLET; + _feeVaults[1] = Predeploys.BASE_FEE_VAULT; + _feeVaults[2] = Predeploys.L1_FEE_VAULT; + _feeVaults[3] = Predeploys.OPERATOR_FEE_VAULT; + } + + /// @notice Helper function to setup a mock and expect a call to it. + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + /// @notice Helper to mock fee vault calls for successful withdrawal scenarios + function _mockFeeVaultForSuccessfulWithdrawal(address _vault, uint256 _balance) internal { + _mockFeeVaultForSuccessfulWithdrawalWithSplitter(address(feeSplitter), _vault, _balance); + } + + /// @notice Helper to mock fee vault calls for successful withdrawal scenarios with a splitter different from the + /// Predeploy FeeSplitter + function _mockFeeVaultForSuccessfulWithdrawalWithSplitter( + address _splitter, + address _vault, + uint256 _balance + ) + internal + { + // Deploy a simple mock vault that can transfer ETH when withdraw() is called + MockFeeVault mockVault = new MockFeeVault(payable(address(_splitter)), 0, Types.WithdrawalNetwork.L2); + vm.deal(address(mockVault), _balance); + vm.etch(_vault, address(mockVault).code); + vm.deal(_vault, _balance); + } + + /// @notice Helper to setup standard fee vault mocks for disbursement + function _setupStandardFeeVaultMocks( + uint256 _sequencerBalance, + uint256 _baseBalance, + uint256 _l1Balance, + uint256 _operatorBalance + ) + internal + { + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, _sequencerBalance); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.BASE_FEE_VAULT, _baseBalance); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.L1_FEE_VAULT, _l1Balance); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.OPERATOR_FEE_VAULT, _operatorBalance); + } +} + +/// @title FeeSplitter_Initialize_Test +/// @notice Tests the initialization functions of the `FeeSplitter` contract. +contract FeeSplitter_Initialize_Test is FeeSplitter_TestInit { + event Initialized(uint8 version); + + /// @notice Test that re-initialization fails on the already-initialized predeploy + function test_feeSplitter_reinitialization_reverts() public { + // The FeeSplitter at the predeploy address is already initialized through genesis + vm.prank(_owner); + vm.expectRevert("Initializable: contract is already initialized"); + feeSplitter.initialize(ISharesCalculator(address(_defaultSharesCalculator))); + } + + /// @notice Test successful initialization with proper event emission on a fresh instance + function test_feeSplitter_initialization_succeeds() public { + // Deploy a fresh instance for testing initialization + address impl = address(uint160(uint256(keccak256("FeeSplitterTestImpl3")))); + vm.etch(impl, vm.getDeployedCode("FeeSplitter.sol:FeeSplitter")); + + vm.prank(_owner); + IFeeSplitter(payable(impl)).initialize(ISharesCalculator(address(_defaultSharesCalculator))); + + assertEq(address(IFeeSplitter(payable(impl)).sharesCalculator()), address(_defaultSharesCalculator)); + assertEq(IFeeSplitter(payable(impl)).feeDisbursementInterval(), 1 days); + assertEq(IFeeSplitter(payable(impl)).lastDisbursementTime(), block.timestamp); + } + + /// @notice Test that the implementation contract disables initializers in the constructor + function test_feeSplitterImplementation_constructorDisablesInitializers_succeeds() public { + bytes memory creationCode = vm.getCode("FeeSplitter.sol:FeeSplitter"); + address implementation; + + // Expect the Initialized event to be emitted + vm.expectEmit(true, true, true, true); + emit Initialized(type(uint8).max); + + // Deploy the implementation contract + assembly { + implementation := create(0, add(creationCode, 0x20), mload(creationCode)) + } + + // Verify the implementation contract is not zero address + assertTrue(implementation != address(0)); + + // Verify re-initialization fails + vm.expectRevert("Initializable: contract is already initialized"); + IFeeSplitter(payable(implementation)).initialize(ISharesCalculator(address(_defaultSharesCalculator))); + } +} + +/// @title FeeSplitter_Receive_Test +/// @notice Tests the receive function of the `FeeSplitter` contract. +contract FeeSplitter_Receive_Test is FeeSplitter_TestInit { + /// @notice Test that receive function reverts when sender is not an approved vault + function testFuzz_feeSplitterReceive_whenNotApprovedVault_reverts(address _caller, uint256 _amount) public { + vm.assume(_caller != Predeploys.SEQUENCER_FEE_WALLET); + vm.assume(_caller != Predeploys.BASE_FEE_VAULT); + vm.assume(_caller != Predeploys.OPERATOR_FEE_VAULT); + vm.assume(_caller != Predeploys.L1_FEE_VAULT); + vm.deal(_caller, _amount); + + vm.prank(_caller); + vm.expectRevert(IFeeSplitter.FeeSplitter_SenderNotCurrentVault.selector); + payable(address(feeSplitter)).call{ value: _amount }(""); + } + + /// @notice Test receive function from non-approved vault reverts even during disbursement + function testFuzz_feeSplitterReceive_whenNonFeeVault_reverts(address _caller, uint256 _amount) public { + vm.assume(_caller != Predeploys.SEQUENCER_FEE_WALLET); + vm.assume(_caller != Predeploys.BASE_FEE_VAULT); + vm.assume(_caller != Predeploys.OPERATOR_FEE_VAULT); + vm.assume(_caller != Predeploys.L1_FEE_VAULT); + + // Setup disbursement conditions but expect revert from non-approved sender + vm.deal(_caller, _amount); + vm.startPrank(_caller); + + // Now we test the actual sender validation + vm.expectRevert(IFeeSplitter.FeeSplitter_SenderNotCurrentVault.selector); + payable(address(feeSplitter)).call{ value: _amount }(""); + } + + /// @notice Test receive function works during disbursement from SequencerFeeVault + function testFuzz_feeSplitterReceive_sequencerFeeVault_succeeds(uint256 _amount) public { + _amount = bound(_amount, 1, type(uint256).max); + + // Setup mocks - only sequencer vault has balance + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, _amount); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.BASE_FEE_VAULT, 0); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.L1_FEE_VAULT, 0); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.OPERATOR_FEE_VAULT, 0); + + // Mock shares calculator to return valid shares + ISharesCalculator.ShareInfo[] memory shareInfo = new ISharesCalculator.ShareInfo[](1); + shareInfo[0] = ISharesCalculator.ShareInfo(payable(_defaultRevenueShareRecipient), _amount); + + // Get the actual shares calculator from the FeeSplitter + address actualSharesCalculator = address(feeSplitter.sharesCalculator()); + vm.mockCall( + actualSharesCalculator, + abi.encodeCall(ISharesCalculator.getRecipientsAndAmounts, (_amount, 0, 0, 0)), + abi.encode(shareInfo) + ); + + // Fast forward time to allow disbursement + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + + vm.expectEmit(true, true, true, true); + emit FeesReceived(Predeploys.SEQUENCER_FEE_WALLET, _amount, _amount); + + // Call disburseFees - this will trigger the receive function during withdrawal + feeSplitter.disburseFees(); + + // Verify the recipient got the funds (proves receive function worked) + assertEq(address(_defaultRevenueShareRecipient).balance, _amount); + assertEq(address(feeSplitter).balance, 0); + assertEq(feeSplitter.lastDisbursementTime(), block.timestamp); + } + + /// @notice Test receive function works during disbursement from BaseFeeVault + function testFuzz_feeSplitterReceive_baseFeeVault_succeeds(uint256 _amount) public { + _amount = bound(_amount, 1, type(uint256).max); + + // Setup mocks - only sequencer vault has balance + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, 0); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.BASE_FEE_VAULT, _amount); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.L1_FEE_VAULT, 0); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.OPERATOR_FEE_VAULT, 0); + + // Mock shares calculator to return valid shares + ISharesCalculator.ShareInfo[] memory shareInfo = new ISharesCalculator.ShareInfo[](1); + shareInfo[0] = ISharesCalculator.ShareInfo(payable(_defaultRevenueShareRecipient), _amount); + + // Get the actual shares calculator from the FeeSplitter + address actualSharesCalculator = address(feeSplitter.sharesCalculator()); + vm.mockCall( + actualSharesCalculator, + abi.encodeCall(ISharesCalculator.getRecipientsAndAmounts, (0, _amount, 0, 0)), + abi.encode(shareInfo) + ); + + // Fast forward time to allow disbursement + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + + vm.expectEmit(true, true, true, true); + emit FeesReceived(Predeploys.BASE_FEE_VAULT, _amount, _amount); + + // Call disburseFees - this will trigger the receive function during withdrawal + feeSplitter.disburseFees(); + + // Verify the recipient got the funds (proves receive function worked) + assertEq(address(_defaultRevenueShareRecipient).balance, _amount); + assertEq(address(feeSplitter).balance, 0); + assertEq(feeSplitter.lastDisbursementTime(), block.timestamp); + } + + /// @notice Test receive function works during disbursement from L1FeeVault + function testFuzz_feeSplitterReceive_l1FeeVault_succeeds(uint256 _amount) public { + _amount = bound(_amount, 1, type(uint256).max); + + // Setup mocks - only sequencer vault has balance + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, 0); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.BASE_FEE_VAULT, 0); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.L1_FEE_VAULT, _amount); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.OPERATOR_FEE_VAULT, 0); + + // Mock shares calculator to return valid shares + ISharesCalculator.ShareInfo[] memory shareInfo = new ISharesCalculator.ShareInfo[](1); + shareInfo[0] = ISharesCalculator.ShareInfo(payable(_defaultRevenueShareRecipient), _amount); + + // Get the actual shares calculator from the FeeSplitter + address actualSharesCalculator = address(feeSplitter.sharesCalculator()); + vm.mockCall( + actualSharesCalculator, + abi.encodeCall(ISharesCalculator.getRecipientsAndAmounts, (0, 0, 0, _amount)), + abi.encode(shareInfo) + ); + + // Fast forward time to allow disbursement + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + + vm.expectEmit(true, true, true, true); + emit FeesReceived(Predeploys.L1_FEE_VAULT, _amount, _amount); + + // Call disburseFees - this will trigger the receive function during withdrawal + feeSplitter.disburseFees(); + + // Verify the recipient got the funds (proves receive function worked) + assertEq(address(_defaultRevenueShareRecipient).balance, _amount); + assertEq(address(feeSplitter).balance, 0); + assertEq(feeSplitter.lastDisbursementTime(), block.timestamp); + } + + /// @notice Test receive function works during disbursement from OperatorFeeVault + function testFuzz_feeSplitterReceive_operatorFeeVault_succeeds(uint256 _amount) public { + _amount = bound(_amount, 1, type(uint256).max); + + // Setup mocks - only sequencer vault has balance + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, 0); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.BASE_FEE_VAULT, 0); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.L1_FEE_VAULT, 0); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.OPERATOR_FEE_VAULT, _amount); + + // Mock shares calculator to return valid shares + ISharesCalculator.ShareInfo[] memory shareInfo = new ISharesCalculator.ShareInfo[](1); + shareInfo[0] = ISharesCalculator.ShareInfo(payable(_defaultRevenueShareRecipient), _amount); + + // Get the actual shares calculator from the FeeSplitter + address actualSharesCalculator = address(feeSplitter.sharesCalculator()); + vm.mockCall( + actualSharesCalculator, + abi.encodeCall(ISharesCalculator.getRecipientsAndAmounts, (0, 0, _amount, 0)), + abi.encode(shareInfo) + ); + + // Fast forward time to allow disbursement + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + + vm.expectEmit(true, true, true, true); + emit FeesReceived(Predeploys.OPERATOR_FEE_VAULT, _amount, _amount); + + // Call disburseFees - this will trigger the receive function during withdrawal + feeSplitter.disburseFees(); + + // Verify the recipient got the funds (proves receive function worked) + assertEq(address(_defaultRevenueShareRecipient).balance, _amount); + assertEq(address(feeSplitter).balance, 0); + assertEq(feeSplitter.lastDisbursementTime(), block.timestamp); + } + + /// @notice Test that a malicious vault cannot trigger withdrawals from other vaults during its own withdrawal. + /// This test demonstrates that the stricter receive() validation (checking the specific vault address) + /// prevents a re-entrancy attack where one vault tries to indirectly call withdraw() on a different vault. + function test_feeSplitterReceive_reentrantVaultAttack_reverts() public { + uint256 sequencerAmount = 1 ether; + + // Setup SEQUENCER_FEE_WALLET as a malicious vault that will try to trigger BASE_FEE_VAULT withdrawal + ReentrantMockFeeVault maliciousVault = new ReentrantMockFeeVault( + payable(address(feeSplitter)), sequencerAmount, payable(Predeploys.BASE_FEE_VAULT) + ); + + vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(maliciousVault).code); + vm.deal(Predeploys.SEQUENCER_FEE_WALLET, sequencerAmount); + + // Fast forward time + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + + // Expect the disbursement to revert with the MockFeeVault error message. + // The flow is: + // 1. FeeSplitter:disburseFees() is called + // 2. Splitter triggers withdrawal from SEQUENCER_FEE_WALLET (etched with malicious code) + // 3. SEQUENCER_FEE_WALLET sends ETH to FeeSplitter and triggers withdrawal from BASE_FEE_VAULT + // 4. BASE_FEE_VAULT's withdraw() reverts with the FeeVault's error message + vm.expectRevert("FeeVault: failed to send ETH to L2 fee recipient"); + feeSplitter.disburseFees(); + } +} + +/// @title FeeSplitter_DisburseFees_Test +/// @notice Tests the disburseFees function of the `FeeSplitter` contract. +contract FeeSplitter_DisburseFees_Test is FeeSplitter_TestInit { + /// @notice Test disburseFees reverts when interval not reached + function test_feeSplitterDisburseFees_whenIntervalNotReached_reverts() public { + vm.prank(_owner); + feeSplitter.setFeeDisbursementInterval(48 hours); + + vm.expectRevert(IFeeSplitter.FeeSplitter_DisbursementIntervalNotReached.selector); + feeSplitter.disburseFees(); + } + + /// @notice Test disburseFees reverts when no fees collected + function test_feeSplitterDisburseFees_whenNoFeesCollected_reverts() public { + _setupStandardFeeVaultMocks(0, 0, 0, 0); + + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + vm.expectRevert(IFeeSplitter.FeeSplitter_NoFeesCollected.selector); + feeSplitter.disburseFees(); + } + + /// @notice Test disburseFees fails when fee vault has wrong withdrawal network + function test_feeSplitterDisburseFees_whenFeeVaultWrongNetwork_reverts() public { + // Mock fee vault with L1 withdrawal network (invalid) + vm.mockCall( + Predeploys.SEQUENCER_FEE_WALLET, + abi.encodeCall(IFeeVault.withdrawalNetwork, ()), + abi.encode(Types.WithdrawalNetwork.L1) + ); + + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + vm.expectRevert(IFeeSplitter.FeeSplitter_FeeVaultMustWithdrawToL2.selector); + feeSplitter.disburseFees(); + } + + /// @notice Test disburseFees fails when fee vault has wrong recipient + function test_feeSplitterDisburseFees_whenFeeVaultWrongRecipient_reverts() public { + // Mock fee vault with wrong recipient + vm.mockCall( + Predeploys.SEQUENCER_FEE_WALLET, + abi.encodeCall(IFeeVault.withdrawalNetwork, ()), + abi.encode(Types.WithdrawalNetwork.L2) + ); + vm.mockCall( + Predeploys.SEQUENCER_FEE_WALLET, abi.encodeCall(IFeeVault.recipient, ()), abi.encode(address(0x123)) + ); + + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + vm.expectRevert(IFeeSplitter.FeeSplitter_FeeVaultMustWithdrawToFeeSplitter.selector); + feeSplitter.disburseFees(); + } + + /// @notice Test disburseFees reverts when fee vault withdrawal amount does not match the expected amount + function testFuzz_feeSplitterDisburseFees_whenFeeVaultWithdrawalAmountMismatch_reverts( + uint256 _actualTransferAmount, + uint256 _claimedWithdrawalAmount + ) + public + { + vm.assume(_actualTransferAmount != _claimedWithdrawalAmount); + + // Create a malicious mock vault that lies about withdrawal amount + MaliciousMockFeeVault maliciousVault = + new MaliciousMockFeeVault(payable(address(feeSplitter)), _actualTransferAmount, _claimedWithdrawalAmount); + vm.deal(address(maliciousVault), _actualTransferAmount); + + // Replace SEQUENCER_FEE_WALLET with the malicious vault + vm.etch(Predeploys.SEQUENCER_FEE_WALLET, address(maliciousVault).code); + vm.deal(Predeploys.SEQUENCER_FEE_WALLET, _actualTransferAmount); + + // Setup other vaults normally with zero balance + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.BASE_FEE_VAULT, 0); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.L1_FEE_VAULT, 0); + _mockFeeVaultForSuccessfulWithdrawal(Predeploys.OPERATOR_FEE_VAULT, 0); + + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + vm.expectRevert(IFeeSplitter.FeeSplitter_FeeVaultWithdrawalAmountMismatch.selector); + feeSplitter.disburseFees(); + } + + /// @notice Test successful fee disbursement with fixed amounts + function test_feeSplitterDisburseFees_succeeds() public { + uint256 _sequencerAmount = 2 ether; + uint256 _baseAmount = 3 ether; + uint256 _l1Amount = 1 ether; + uint256 _operatorAmount = 4 ether; + + _setupStandardFeeVaultMocks(_sequencerAmount, _baseAmount, _l1Amount, _operatorAmount); + + // Calculate expected gross revenue + uint256 expectedGrossRevenue = _sequencerAmount + _baseAmount + _l1Amount + _operatorAmount; + + // Setup mock shares calculator to return 50/50 split + uint256 halfGrossRevenue = expectedGrossRevenue / 2; + ISharesCalculator.ShareInfo[] memory expectedShareInfo = new ISharesCalculator.ShareInfo[](2); + expectedShareInfo[0] = ISharesCalculator.ShareInfo(payable(_defaultRevenueShareRecipient), halfGrossRevenue); + expectedShareInfo[1] = ISharesCalculator.ShareInfo( + payable(_defaultRevenueRemainderRecipient), expectedGrossRevenue - halfGrossRevenue + ); + + // Get the actual shares calculator from the FeeSplitter + address actualSharesCalculator = address(feeSplitter.sharesCalculator()); + vm.mockCall( + actualSharesCalculator, + abi.encodeCall( + ISharesCalculator.getRecipientsAndAmounts, (_sequencerAmount, _baseAmount, _operatorAmount, _l1Amount) + ), + abi.encode(expectedShareInfo) + ); + + // Fast forward time to allow disbursement + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + + // Store initial balances + uint256 revenueShareRecipientBalanceBefore = address(_defaultRevenueShareRecipient).balance; + uint256 revenueRemainderRecipientBalanceBefore = address(_defaultRevenueRemainderRecipient).balance; + + // Call disburseFees + feeSplitter.disburseFees(); + + // Verify the last disbursement time was updated + assertEq(feeSplitter.lastDisbursementTime(), block.timestamp); + + // Verify recipients received their shares + assertEq(address(_defaultRevenueShareRecipient).balance, revenueShareRecipientBalanceBefore + halfGrossRevenue); + assertEq( + address(_defaultRevenueRemainderRecipient).balance, + revenueRemainderRecipientBalanceBefore + (expectedGrossRevenue - halfGrossRevenue) + ); + + // Verify the fee vaults have no balance + assertEq(address(Predeploys.SEQUENCER_FEE_WALLET).balance, 0); + assertEq(address(Predeploys.BASE_FEE_VAULT).balance, 0); + assertEq(address(Predeploys.L1_FEE_VAULT).balance, 0); + assertEq(address(Predeploys.OPERATOR_FEE_VAULT).balance, 0); + + // Verify the fee splitter has no balance + assertEq(address(feeSplitter).balance, 0); + } + + /// @notice Test disburseFees reverts when shares calculator returns an empty array + function test_feeSplitterDisburseFees_whenSharesInfoEmpty_reverts() public { + uint256 _sequencerAmount = 2 ether; + _setupStandardFeeVaultMocks(_sequencerAmount, 0, 0, 0); + + address actualSharesCalculator = address(feeSplitter.sharesCalculator()); + ISharesCalculator.ShareInfo[] memory emptyShareInfo = new ISharesCalculator.ShareInfo[](0); + vm.mockCall( + actualSharesCalculator, + abi.encodeCall(ISharesCalculator.getRecipientsAndAmounts, (_sequencerAmount, 0, 0, 0)), + abi.encode(emptyShareInfo) + ); + + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + + vm.expectRevert(IFeeSplitter.FeeSplitter_FeeShareInfoEmpty.selector); + feeSplitter.disburseFees(); + } + + /// @notice Test disburseFees reverts when sending to a recipient fails + function test_feeSplitterDisburseFees_whenSendingFails_reverts() public { + uint256 _sequencerAmount = 1 ether; + _setupStandardFeeVaultMocks(_sequencerAmount, 0, 0, 0); + + address revertingRecipient = address(new RevertingRecipient()); + ISharesCalculator.ShareInfo[] memory shareInfo = new ISharesCalculator.ShareInfo[](1); + shareInfo[0] = ISharesCalculator.ShareInfo(payable(revertingRecipient), _sequencerAmount); + + address actualSharesCalculator = address(feeSplitter.sharesCalculator()); + vm.mockCall( + actualSharesCalculator, + abi.encodeCall(ISharesCalculator.getRecipientsAndAmounts, (_sequencerAmount, 0, 0, 0)), + abi.encode(shareInfo) + ); + + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + + vm.expectRevert(IFeeSplitter.FeeSplitter_FailedToSendToRevenueShareRecipient.selector); + feeSplitter.disburseFees(); + } + + /// @notice Test disburseFees reverts when total shares do not match gross revenue + function test_feeSplitterDisburseFees_whenSharesMalformed_reverts() public { + uint256 _sequencerAmount = 1 ether; + _setupStandardFeeVaultMocks(_sequencerAmount, 0, 0, 0); + + ISharesCalculator.ShareInfo[] memory shareInfo = new ISharesCalculator.ShareInfo[](1); + shareInfo[0] = ISharesCalculator.ShareInfo(payable(_defaultRevenueShareRecipient), _sequencerAmount - 1); + + address actualSharesCalculator = address(feeSplitter.sharesCalculator()); + vm.mockCall( + actualSharesCalculator, + abi.encodeCall(ISharesCalculator.getRecipientsAndAmounts, (_sequencerAmount, 0, 0, 0)), + abi.encode(shareInfo) + ); + + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + + vm.expectRevert(IFeeSplitter.FeeSplitter_SharesCalculatorMalformedOutput.selector); + feeSplitter.disburseFees(); + } + + /// @notice Fuzz test that a vault with balance below minimum causes entire disbursement to revert + function testFuzz_disburseFees_vaultBelowMinimum_reverts( + uint256 _minWithdrawalAmount, + uint256 _vaultIndex + ) + public + { + // If uint256, the test will revert due to ETH transfer overflow + _minWithdrawalAmount = bound(_minWithdrawalAmount, 1, type(uint128).max); + _vaultIndex = bound(_vaultIndex, 0, 3); // 0-3 for the 4 vaults + + // Calculate vault balances: one vault will have insufficient balance + uint256 insufficientBalance = _minWithdrawalAmount - 1; + uint256 sufficientBalance = _minWithdrawalAmount; + + address[4] memory vaults = [ + Predeploys.SEQUENCER_FEE_WALLET, + Predeploys.BASE_FEE_VAULT, + Predeploys.L1_FEE_VAULT, + Predeploys.OPERATOR_FEE_VAULT + ]; + + // Setup all vaults with sufficient balance first + for (uint256 i = 0; i < 4; i++) { + _setFeeVaultData(vaults[i], sufficientBalance, _minWithdrawalAmount); + } + + // Override the selected vault with insufficient balance + _setFeeVaultData(vaults[_vaultIndex], insufficientBalance, _minWithdrawalAmount); + + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + + // The entire disbursement should revert because one vault doesn't meet its minimum + vm.expectRevert("FeeVault: withdrawal amount must be greater than minimum withdrawal amount"); + feeSplitter.disburseFees(); + + // Verify no funds were moved (all vaults retain their original balance) + for (uint256 i = 0; i < 4; i++) { + uint256 expectedBalance = (i == _vaultIndex) ? insufficientBalance : sufficientBalance; + assertEq(address(vaults[i]).balance, expectedBalance); + } + } + + function _setFeeVaultData(address _vault, uint256 _balance, uint256 _minWithdrawal) internal { + MockFeeVault mockVault = + new MockFeeVault(payable(address(feeSplitter)), _minWithdrawal, Types.WithdrawalNetwork.L2); + vm.deal(address(mockVault), _balance); + vm.etch(_vault, address(mockVault).code); + vm.deal(_vault, _balance); + } + + /// @notice Test that vaults cannot send ETH after disburseFees completes (transient storage cleanup check) + /// @param _vaultIndex The index of the vault to test (0-3). + function testFuzz_feeSplitterDisburseFees_vaultsCannotSendAfterDisbursement_reverts(uint256 _vaultIndex) public { + _vaultIndex = bound(_vaultIndex, 0, 3); + + uint256 _sequencerAmount = 2 ether; + uint256 _baseAmount = 3 ether; + uint256 _l1Amount = 1 ether; + uint256 _operatorAmount = 4 ether; + + _setupStandardFeeVaultMocks(_sequencerAmount, _baseAmount, _l1Amount, _operatorAmount); + + // Calculate expected gross revenue + uint256 expectedGrossRevenue = _sequencerAmount + _baseAmount + _l1Amount + _operatorAmount; + + // Setup mock shares calculator to return 50/50 split + uint256 halfGrossRevenue = expectedGrossRevenue / 2; + ISharesCalculator.ShareInfo[] memory expectedShareInfo = new ISharesCalculator.ShareInfo[](2); + expectedShareInfo[0] = ISharesCalculator.ShareInfo(payable(_defaultRevenueShareRecipient), halfGrossRevenue); + expectedShareInfo[1] = ISharesCalculator.ShareInfo( + payable(_defaultRevenueRemainderRecipient), expectedGrossRevenue - halfGrossRevenue + ); + + // Get the actual shares calculator from the FeeSplitter + address actualSharesCalculator = address(feeSplitter.sharesCalculator()); + vm.mockCall( + actualSharesCalculator, + abi.encodeCall( + ISharesCalculator.getRecipientsAndAmounts, (_sequencerAmount, _baseAmount, _operatorAmount, _l1Amount) + ), + abi.encode(expectedShareInfo) + ); + + // Fast forward time to allow disbursement + vm.warp(block.timestamp + feeSplitter.feeDisbursementInterval() + 1); + + // Call disburseFees + feeSplitter.disburseFees(); + + // Verify disbursement was successful + assertEq(feeSplitter.lastDisbursementTime(), block.timestamp); + + // Now try to send ETH from one of the vaults after disbursement + address _vault = _feeVaults[_vaultIndex]; + uint256 _attemptAmount = 1 ether; + vm.deal(_vault, _attemptAmount); + + // Attempt to send ETH from the vault - should revert because transient storage was cleared + vm.prank(_vault); + vm.expectRevert(IFeeSplitter.FeeSplitter_SenderNotCurrentVault.selector); + payable(address(feeSplitter)).call{ value: _attemptAmount }(""); + } +} + +/// @title FeeSplitter_SetSharesCalculator_Test +/// @notice Tests the setSharesCalculator function of the `FeeSplitter` contract. +contract FeeSplitter_SetSharesCalculator_Test is FeeSplitter_TestInit { + /// @notice Test setSharesCalculator reverts when caller is not owner + function testFuzz_feeSplitterSetSharesCalculator_whenNotOwner_reverts(address _caller) public { + vm.assume(_caller != _owner); + + vm.prank(_caller); + vm.expectRevert(IFeeSplitter.FeeSplitter_OnlyProxyAdminOwner.selector); + feeSplitter.setSharesCalculator(ISharesCalculator(address(0x123))); + } + + /// @notice Test setSharesCalculator reverts with zero address + function test_feeSplitterSetSharesCalculator_whenZeroAddress_reverts() public { + vm.prank(_owner); + vm.expectRevert(IFeeSplitter.FeeSplitter_SharesCalculatorCannotBeZero.selector); + feeSplitter.setSharesCalculator(ISharesCalculator(address(0))); + } + + /// @notice Test successful setSharesCalculator + function test_feeSplitterSetSharesCalculator_succeeds(address _newSharesCalculator) public { + vm.assume(_newSharesCalculator != address(0)); + + vm.expectEmit(address(feeSplitter)); + emit SharesCalculatorUpdated(address(feeSplitter.sharesCalculator()), _newSharesCalculator); + + vm.prank(_owner); + feeSplitter.setSharesCalculator(ISharesCalculator(_newSharesCalculator)); + + assertEq(address(feeSplitter.sharesCalculator()), _newSharesCalculator); + } +} + +/// @title FeeSplitter_SetFeeDisbursementInterval_Test +/// @notice Tests the setFeeDisbursementInterval function of the `FeeSplitter` contract. +contract FeeSplitter_SetFeeDisbursementInterval_Test is FeeSplitter_TestInit { + /// @notice Test setFeeDisbursementInterval reverts when caller is not owner + function testFuzz_feeSplitterSetFeeDisbursementInterval_whenNotOwner_reverts(address _caller) public { + vm.assume(_caller != _owner); + + vm.prank(_caller); + vm.expectRevert(IFeeSplitter.FeeSplitter_OnlyProxyAdminOwner.selector); + feeSplitter.setFeeDisbursementInterval(48 hours); + } + + /// @notice Test setFeeDisbursementInterval reverts when interval is zero + function test_feeSplitterSetFeeDisbursementInterval_whenIntervalZero_reverts() public { + vm.prank(_owner); + vm.expectRevert(IFeeSplitter.FeeSplitter_FeeDisbursementIntervalCannotBeZero.selector); + feeSplitter.setFeeDisbursementInterval(0); + } + + /// @notice Test setFeeDisbursementInterval reverts when interval is too long + function testFuzz_feeSplitterSetFeeDisbursementInterval_whenIntervalTooLong_reverts(uint256 _disbursementInterval) + public + { + _disbursementInterval = bound(_disbursementInterval, 365 days + 1, type(uint128).max); + + vm.prank(_owner); + vm.expectRevert(IFeeSplitter.FeeSplitter_ExceedsMaxFeeDisbursementTime.selector); + feeSplitter.setFeeDisbursementInterval(uint128(_disbursementInterval)); + } + + /// @notice Test successful setFeeDisbursementInterval + function testFuzz_feeSplitterSetFeeDisbursementInterval_succeeds(uint128 _newInterval) public { + _newInterval = uint128(bound(_newInterval, 1, 365 days)); + + vm.expectEmit(address(feeSplitter)); + emit FeeDisbursementIntervalUpdated(feeSplitter.feeDisbursementInterval(), _newInterval); + + vm.prank(_owner); + feeSplitter.setFeeDisbursementInterval(_newInterval); + + assertEq(feeSplitter.feeDisbursementInterval(), _newInterval); + } +} diff --git a/packages/contracts-bedrock/test/L2/FeeSplitterVaults.t.sol b/packages/contracts-bedrock/test/L2/FeeSplitterVaults.t.sol new file mode 100644 index 0000000000000..4aa79e97bcc70 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/FeeSplitterVaults.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Testing +import { Test } from "forge-std/Test.sol"; +import { FeeSplitterForTest } from "test/mocks/FeeSplitterForTest.sol"; + +// Interfaces +import { IFeeSplitter } from "interfaces/L2/IFeeSplitter.sol"; + +/// @title FeeSplitterVaults_Test +/// @notice Test contract for the FeeSplitter contract with vaults. +/// @dev This test is done in a different file given we need the 0.8.25 compiler version to import the FeeSplitter +/// implementation and modify it. +contract FeeSplitterVaults_Receive_Test is Test { + FeeSplitterForTest feeSplitter; + address[4] internal _feeVaults; + + function setUp() public { + FeeSplitterForTest feeSplitterImpl = new FeeSplitterForTest(); + feeSplitter = FeeSplitterForTest(payable(Predeploys.FEE_SPLITTER)); + + vm.etch(Predeploys.FEE_SPLITTER, address(feeSplitterImpl).code); + vm.etch(Predeploys.SEQUENCER_FEE_WALLET, vm.getDeployedCode("SequencerFeeVault.sol")); + vm.etch(Predeploys.BASE_FEE_VAULT, vm.getDeployedCode("BaseFeeVault.sol")); + vm.etch(Predeploys.OPERATOR_FEE_VAULT, vm.getDeployedCode("OperatorFeeVault.sol")); + vm.etch(Predeploys.L1_FEE_VAULT, vm.getDeployedCode("L1FeeVault.sol")); + + _feeVaults[0] = Predeploys.SEQUENCER_FEE_WALLET; + _feeVaults[1] = Predeploys.BASE_FEE_VAULT; + _feeVaults[2] = Predeploys.OPERATOR_FEE_VAULT; + _feeVaults[3] = Predeploys.L1_FEE_VAULT; + } + + /// @notice Test that receive function reverts when sender is an approved vault but not currently disbursing + /// @param _amount The amount of ETH to send. + /// @dev This test simulates the disbursement context on each vault and then goes + /// through each vault and sends ETH to the fee splitter, expecting the receive function to revert + function testFuzz_feeSplitterReceive_whenNotCurrentVault_reverts(uint128 _amount) public { + for (uint256 i = 0; i < _feeVaults.length; i++) { + address _disbursingVault = _feeVaults[i]; + feeSplitter.setTransientDisbursingAddress(_disbursingVault); + for (uint256 j = 0; j < _feeVaults.length; j++) { + address _selectedVault = _feeVaults[j]; + vm.deal(_selectedVault, _amount); + + if (_selectedVault != _disbursingVault) { + vm.expectRevert(IFeeSplitter.FeeSplitter_SenderNotCurrentVault.selector); + } + + vm.prank(_selectedVault); + payable(address(feeSplitter)).call{ value: _amount }(""); + } + } + } +} diff --git a/packages/contracts-bedrock/test/L2/FeeVault.t.sol b/packages/contracts-bedrock/test/L2/FeeVault.t.sol new file mode 100644 index 0000000000000..edbd1c0064df7 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/FeeVault.t.sol @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; +import { Reverter } from "test/mocks/Callers.sol"; + +// Interfaces +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; +import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; + +// Libraries +import { Hashing } from "src/libraries/Hashing.sol"; +import { Types } from "src/libraries/Types.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +/// @title FeeVault_Uncategorized_Test +/// @notice Abstract test contract for fee feeVault testing. +/// Subclasses can override the feeVault-specific variables. +abstract contract FeeVault_Uncategorized_Test is CommonTest { + // Variables that can be overridden by concrete test contracts + address recipient; + IFeeVault feeVault; + string feeVaultName; + uint256 minWithdrawalAmount; + Types.WithdrawalNetwork withdrawalNetwork; + + /// @notice Helper function to set up L2 withdrawal configuration. + function _setupL2Withdrawal() internal { + // Set the withdrawal network to L2 + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + feeVault.setWithdrawalNetwork(Types.WithdrawalNetwork.L2); + } + + /// @notice Tests that the initialize function succeeds. + function test_initialize_succeeds() external view { + assertEq(feeVault.recipient(), recipient); + assertEq(feeVault.minWithdrawalAmount(), minWithdrawalAmount); + assertEq(uint8(feeVault.withdrawalNetwork()), uint8(withdrawalNetwork)); + } + + /// @notice Tests that the initialize function reverts if the contract is already initialized. + function test_initialize_reinitialization_reverts() external { + _setupL2Withdrawal(); + + vm.expectRevert(IFeeVault.InvalidInitialization.selector); + feeVault.initialize(recipient, minWithdrawalAmount, Types.WithdrawalNetwork.L1); + } + + /// @notice Tests that the immutable values match the storage getters. + function test_immutableMatchesStorageVariables_succeeds() external view { + assertEq(feeVault.RECIPIENT(), feeVault.recipient()); + assertEq(feeVault.MIN_WITHDRAWAL_AMOUNT(), feeVault.minWithdrawalAmount()); + assertEq(uint8(feeVault.WITHDRAWAL_NETWORK()), uint8(feeVault.withdrawalNetwork())); + } + + /// @notice Tests that the fee feeVault is able to receive ETH. + function test_receive_succeeds() external { + uint256 balance = address(feeVault).balance; + + vm.prank(alice); + (bool success,) = address(feeVault).call{ value: 100 }(hex""); + + assertEq(success, true); + assertEq(address(feeVault).balance, balance + 100); + } + + /// @notice Tests that `withdraw` reverts if the balance is less than the minimum withdrawal + /// amount. + function testFuzz_withdraw_notEnough_reverts(uint256 _minWithdrawalAmount) external { + // Set the minimum withdrawal amount + _minWithdrawalAmount = bound(_minWithdrawalAmount, 1, type(uint256).max); + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + feeVault.setMinWithdrawalAmount(_minWithdrawalAmount); + + // Set the balance to be less than the minimum withdrawal amount + vm.deal(address(feeVault), _minWithdrawalAmount - 1); + + vm.expectRevert("FeeVault: withdrawal amount must be greater than minimum withdrawal amount"); + feeVault.withdraw(); + } + + /// @notice Tests that `withdraw` successfully initiates a withdrawal to L1. + function test_withdraw_toL1_succeeds() external { + // Setup L1 withdrawal + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + feeVault.setWithdrawalNetwork(Types.WithdrawalNetwork.L1); + + // Set recipient + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + feeVault.setRecipient(recipient); + + // Set minimum withdrawal amount + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + feeVault.setMinWithdrawalAmount(minWithdrawalAmount); + + // Set the balance to be greater than the minimum withdrawal amount + uint256 amount = feeVault.minWithdrawalAmount() + 1; + vm.deal(address(feeVault), amount); + + // No ether has been withdrawn yet + assertEq(feeVault.totalProcessed(), 0); + + vm.expectEmit(address(address(feeVault))); + emit Withdrawal(address(feeVault).balance, recipient, address(this)); + vm.expectEmit(address(address(feeVault))); + emit Withdrawal(address(feeVault).balance, recipient, address(this), Types.WithdrawalNetwork.L1); + + // The entire feeVault's balance is withdrawn + vm.expectCall( + Predeploys.L2_TO_L1_MESSAGE_PASSER, + address(feeVault).balance, + abi.encodeCall(IL2ToL1MessagePasser.initiateWithdrawal, (recipient, 400_000, hex"")) + ); + + // The message is passed to the correct recipient + vm.expectEmit(Predeploys.L2_TO_L1_MESSAGE_PASSER); + emit MessagePassed( + l2ToL1MessagePasser.messageNonce(), + address(feeVault), + recipient, + amount, + 400_000, + hex"", + Hashing.hashWithdrawal( + Types.WithdrawalTransaction({ + nonce: l2ToL1MessagePasser.messageNonce(), + sender: address(feeVault), + target: recipient, + value: amount, + gasLimit: 400_000, + data: hex"" + }) + ) + ); + + feeVault.withdraw(); + + // The withdrawal was successful + assertEq(feeVault.totalProcessed(), amount); + assertEq(address(feeVault).balance, 0); + assertEq(Predeploys.L2_TO_L1_MESSAGE_PASSER.balance, amount); + } + + /// @notice Tests that `withdraw` successfully initiates a withdrawal to L2. + function test_withdraw_toL2_succeeds() public { + _setupL2Withdrawal(); + + uint256 amount = feeVault.minWithdrawalAmount() + 1; + vm.deal(address(feeVault), amount); + + // No ether has been withdrawn yet + assertEq(feeVault.totalProcessed(), 0); + + vm.expectEmit(address(address(feeVault))); + emit Withdrawal(address(feeVault).balance, feeVault.RECIPIENT(), address(this)); + vm.expectEmit(address(address(feeVault))); + emit Withdrawal(address(feeVault).balance, feeVault.RECIPIENT(), address(this), Types.WithdrawalNetwork.L2); + + // The entire feeVault's balance is withdrawn + vm.expectCall(recipient, address(feeVault).balance, bytes("")); + + uint256 withdrawnAmount = feeVault.withdraw(); + + // The withdrawal was successful + assertEq(withdrawnAmount, amount); + assertEq(feeVault.totalProcessed(), amount); + assertEq(address(feeVault).balance, 0); + assertEq(recipient.balance, amount); + } + + /// @notice Tests that `withdraw` fails if the Recipient reverts. This also serves to simulate + /// a situation where insufficient gas is provided to the RECIPIENT. + function test_withdraw_toL2recipientReverts_fails() external { + _setupL2Withdrawal(); + + uint256 amount = feeVault.minWithdrawalAmount(); + + vm.deal(address(feeVault), amount); + // No ether has been withdrawn yet + assertEq(feeVault.totalProcessed(), 0); + + // Ensure the RECIPIENT reverts + vm.etch(feeVault.RECIPIENT(), type(Reverter).runtimeCode); + + // The entire feeVault's balance is withdrawn + vm.expectCall(recipient, address(feeVault).balance, bytes("")); + vm.expectRevert("FeeVault: failed to send ETH to L2 fee recipient"); + feeVault.withdraw(); + assertEq(feeVault.totalProcessed(), 0); + } + + /// @notice Tests that the owner can successfully set minimum withdrawal amount with fuzz testing. + function testFuzz_setMinWithdrawalAmount_succeeds(uint256 _newMinWithdrawalAmount) external { + address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); + + vm.prank(owner); + IFeeVault(payable(address(feeVault))).setMinWithdrawalAmount(_newMinWithdrawalAmount); + + // Verify the value was updated + assertEq(feeVault.minWithdrawalAmount(), _newMinWithdrawalAmount); + } + + /// @notice Tests that non-owner cannot set minimum withdrawal amount with fuzz testing. + function testFuzz_setMinWithdrawalAmount_onlyOwner_reverts(address _caller, uint256 _newAmount) external { + address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); + vm.assume(_caller != owner); + + uint256 initialAmount = feeVault.minWithdrawalAmount(); + + vm.prank(_caller); + vm.expectRevert(IFeeVault.FeeVault_OnlyProxyAdminOwner.selector); + IFeeVault(payable(address(feeVault))).setMinWithdrawalAmount(_newAmount); + + // Verify the value and boolean flag were NOT changed + assertEq(feeVault.minWithdrawalAmount(), initialAmount); + } + + /// @notice Tests that the owner can successfully set recipient with fuzz testing. + function testFuzz_setRecipient_succeeds(address _newRecipient) external { + address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); + + vm.prank(owner); + IFeeVault(payable(address(feeVault))).setRecipient(_newRecipient); + + // Verify the value was updated + assertEq(feeVault.recipient(), _newRecipient); + } + + /// @notice Tests that non-owner cannot set recipient with fuzz testing. + function testFuzz_setRecipient_onlyOwner_reverts(address _caller, address _newRecipient) external { + address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); + vm.assume(_caller != owner); + + address initialRecipient = feeVault.recipient(); + + vm.prank(_caller); + vm.expectRevert(IFeeVault.FeeVault_OnlyProxyAdminOwner.selector); + IFeeVault(payable(address(feeVault))).setRecipient(_newRecipient); + + // Verify the value and boolean flag were NOT changed + assertEq(feeVault.recipient(), initialRecipient); + } + + /// @notice Tests that the owner can successfully set withdrawal network with fuzz testing. + function testFuzz_setWithdrawalNetwork_succeeds(uint8 _networkValue) external { + // Bound to valid enum values (0 = L1, 1 = L2) + _networkValue = uint8(bound(_networkValue, 0, 1)); + Types.WithdrawalNetwork newNetwork = Types.WithdrawalNetwork(_networkValue); + + address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); + + vm.prank(owner); + IFeeVault(payable(address(feeVault))).setWithdrawalNetwork(newNetwork); + + // Verify the value was updated + assertEq(uint8(feeVault.withdrawalNetwork()), uint8(newNetwork)); + } + + /// @notice Tests that non-owner cannot set withdrawal network with fuzz testing. + function testFuzz_setWithdrawalNetwork_onlyOwner_reverts(address _caller, uint8 _networkValue) external { + address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); + vm.assume(_caller != owner); + + // Bound to valid enum values + _networkValue = uint8(bound(_networkValue, 0, 1)); + Types.WithdrawalNetwork newNetwork = Types.WithdrawalNetwork(_networkValue); + + Types.WithdrawalNetwork initialNetwork = feeVault.withdrawalNetwork(); + + vm.prank(_caller); + vm.expectRevert(IFeeVault.FeeVault_OnlyProxyAdminOwner.selector); + IFeeVault(payable(address(feeVault))).setWithdrawalNetwork(newNetwork); + + // Verify the value and boolean flag were NOT changed + assertEq(uint8(feeVault.withdrawalNetwork()), uint8(initialNetwork)); + } +} diff --git a/packages/contracts-bedrock/test/L2/L1FeeVault.t.sol b/packages/contracts-bedrock/test/L2/L1FeeVault.t.sol index e0c2174c5189a..3e9202546b54e 100644 --- a/packages/contracts-bedrock/test/L2/L1FeeVault.t.sol +++ b/packages/contracts-bedrock/test/L2/L1FeeVault.t.sol @@ -1,12 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { CommonTest } from "test/setup/CommonTest.sol"; +// Interfaces +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; // Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { FeeVault_Uncategorized_Test } from "test/L2/FeeVault.t.sol"; import { Types } from "src/libraries/Types.sol"; import { SemverComp } from "src/libraries/SemverComp.sol"; +import { CommonTest } from "test/setup/CommonTest.sol"; /// @title L1FeeVault_Version_Test /// @notice Tests the `version` function of the `L1FeeVault` contract. @@ -17,16 +20,16 @@ contract L1FeeVault_Version_Test is CommonTest { } } -/// @title L1FeeVault_Constructor_Test -/// @notice Tests the `constructor` of the `L1FeeVault` contract. -contract L1FeeVault_Constructor_Test is CommonTest { - /// @notice Tests that the constructor sets the correct values. - function test_constructor_l1FeeVault_succeeds() external view { - assertEq(l1FeeVault.RECIPIENT(), deploy.cfg().l1FeeVaultRecipient()); - assertEq(l1FeeVault.recipient(), deploy.cfg().l1FeeVaultRecipient()); - assertEq(l1FeeVault.MIN_WITHDRAWAL_AMOUNT(), deploy.cfg().l1FeeVaultMinimumWithdrawalAmount()); - assertEq(l1FeeVault.minWithdrawalAmount(), deploy.cfg().l1FeeVaultMinimumWithdrawalAmount()); - assertEq(uint8(l1FeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L1)); - assertEq(uint8(l1FeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L1)); +/// @title L1FeeVault_Uncategorized_Test +/// @notice Test contract for the L1FeeVault contract's functionality +contract L1FeeVault_Uncategorized_Test is FeeVault_Uncategorized_Test { + /// @dev Sets up the test suite. + function setUp() public virtual override { + super.setUp(); + recipient = deploy.cfg().l1FeeVaultRecipient(); + feeVaultName = "L1FeeVault"; + minWithdrawalAmount = deploy.cfg().l1FeeVaultMinimumWithdrawalAmount(); + feeVault = IFeeVault(payable(Predeploys.L1_FEE_VAULT)); + withdrawalNetwork = Types.WithdrawalNetwork(uint8(deploy.cfg().l1FeeVaultWithdrawalNetwork())); } } diff --git a/packages/contracts-bedrock/test/L2/L1Withdrawer.t.sol b/packages/contracts-bedrock/test/L2/L1Withdrawer.t.sol new file mode 100644 index 0000000000000..865e7177f2300 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/L1Withdrawer.t.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { CommonTest } from "test/setup/CommonTest.sol"; +import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { IL1Withdrawer } from "interfaces/L2/IL1Withdrawer.sol"; +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +/// @title L1Withdrawer_TestInit +/// @notice Base test contract with initialization for `L1Withdrawer` tests. +contract L1Withdrawer_TestInit is CommonTest { + // Events + event WithdrawalInitiated(address indexed recipient, uint256 amount); + event FundsReceived(address indexed sender, uint256 amount, uint256 newBalance); + event MinWithdrawalAmountUpdated(uint256 oldMinWithdrawalAmount, uint256 newMinWithdrawalAmount); + event RecipientUpdated(address oldRecipient, address newRecipient); + event WithdrawalGasLimitUpdated(uint32 oldWithdrawalGasLimit, uint32 newWithdrawalGasLimit); + + // Test state + uint256 minWithdrawalAmount = 10 ether; + uint32 withdrawalGasLimit = 1_000_000; + + uint32 internal constant MIN_WITHDRAWAL_GAS_LIMIT = 800_000; + + /// @notice Test setup. + function setUp() public virtual override { + // Enable revenue sharing before calling parent setUp + super.enableRevenueShare(); + super.setUp(); + } +} + +contract L1Withdrawer_Constructor_Test is L1Withdrawer_TestInit { + function testFuzz_constructor_succeeds( + uint256 _minWithdrawalAmount, + address _recipient, + uint32 _withdrawalGasLimit + ) + external + { + _withdrawalGasLimit = uint32(bound(uint256(_withdrawalGasLimit), MIN_WITHDRAWAL_GAS_LIMIT, type(uint32).max)); + + IL1Withdrawer withdrawer = IL1Withdrawer( + DeployUtils.create1({ + _name: "L1Withdrawer", + _args: DeployUtils.encodeConstructor( + abi.encodeCall(IL1Withdrawer.__constructor__, (_minWithdrawalAmount, _recipient, _withdrawalGasLimit)) + ) + }) + ); + + assertEq(withdrawer.minWithdrawalAmount(), _minWithdrawalAmount); + assertEq(withdrawer.recipient(), _recipient); + assertEq(withdrawer.withdrawalGasLimit(), _withdrawalGasLimit); + } +} + +/// @title L1Withdrawer_Receive_Test +/// @notice Tests the receive function of the `L1Withdrawer` contract. +contract L1Withdrawer_Receive_Test is L1Withdrawer_TestInit { + function testFuzz_receive_belowThreshold_succeeds(uint256 _amount) external { + _amount = bound(_amount, 0, minWithdrawalAmount - 1); + + vm.deal(address(this), _amount); + + vm.expectEmit(address(l1Withdrawer)); + emit FundsReceived(address(this), _amount, _amount); + + (bool success,) = address(l1Withdrawer).call{ value: _amount }(""); + + assertTrue(success); + assertEq(address(l1Withdrawer).balance, _amount); + assertEq(address(Predeploys.L2_CROSS_DOMAIN_MESSENGER).balance, 0); + } + + function testFuzz_receive_atOrAboveThreshold_succeeds(uint256 _sendAmount) external { + _sendAmount = bound(_sendAmount, minWithdrawalAmount, type(uint256).max); + + vm.deal(address(this), _sendAmount); + + vm.expectEmit(address(l1Withdrawer)); + emit FundsReceived(address(this), _sendAmount, _sendAmount); + + vm.expectEmit(address(l1Withdrawer)); + emit WithdrawalInitiated(l1FeesDepositor, _sendAmount); + + vm.expectCall( + Predeploys.L2_CROSS_DOMAIN_MESSENGER, + _sendAmount, + abi.encodeCall( + ICrossDomainMessenger.sendMessage, (l1FeesDepositor, hex"", l1Withdrawer.withdrawalGasLimit()) + ) + ); + + (bool success,) = address(l1Withdrawer).call{ value: _sendAmount }(""); + + assertTrue(success); + assertEq(address(l1Withdrawer).balance, 0); + // Note: CrossDomainMessenger forwards to L2ToL1MessagePasser, so balance goes there + assertEq(address(Predeploys.L2_TO_L1_MESSAGE_PASSER).balance, _sendAmount); + } + + function testFuzz_receive_multipleDeposits_succeeds(uint256 _firstAmount, uint256 _secondAmount) external { + // First amount should not exceed minWithdrawalAmount (so it doesn't trigger withdrawal) + _firstAmount = bound(_firstAmount, 0, minWithdrawalAmount - 1); + + // Second amount should ensure total reaches threshold to trigger withdrawal + _secondAmount = bound(_secondAmount, minWithdrawalAmount - _firstAmount, type(uint256).max - _firstAmount); + + uint256 totalAmount = _firstAmount + _secondAmount; + + // First deposit (should not trigger withdrawal) + vm.deal(address(this), _firstAmount); + + vm.expectEmit(address(l1Withdrawer)); + emit FundsReceived(address(this), _firstAmount, _firstAmount); + + (bool success1,) = address(l1Withdrawer).call{ value: _firstAmount }(""); + assertTrue(success1); + assertEq(address(l1Withdrawer).balance, _firstAmount); + assertEq(address(Predeploys.L2_CROSS_DOMAIN_MESSENGER).balance, 0); + + // Second deposit (will trigger withdrawal since total >= minWithdrawalAmount) + vm.deal(address(this), _secondAmount); + + vm.expectEmit(address(l1Withdrawer)); + emit FundsReceived(address(this), _secondAmount, totalAmount); + + vm.expectEmit(address(l1Withdrawer)); + emit WithdrawalInitiated(l1FeesDepositor, totalAmount); + + vm.expectCall( + Predeploys.L2_CROSS_DOMAIN_MESSENGER, + totalAmount, + abi.encodeCall( + ICrossDomainMessenger.sendMessage, (l1FeesDepositor, hex"", l1Withdrawer.withdrawalGasLimit()) + ) + ); + + (bool success2,) = address(l1Withdrawer).call{ value: _secondAmount }(""); + assertTrue(success2); + + // Verify withdrawal occurred + assertEq(address(l1Withdrawer).balance, 0); + assertEq(address(Predeploys.L2_TO_L1_MESSAGE_PASSER).balance, totalAmount); + } +} + +/// @title L1Withdrawer_SetMinWithdrawalAmount_Test +/// @notice Tests the setMinWithdrawalAmount function of the `L1Withdrawer` contract. +contract L1Withdrawer_SetMinWithdrawalAmount_Test is L1Withdrawer_TestInit { + function testFuzz_setMinWithdrawalAmount_asOwner_succeeds(uint256 _newMinWithdrawalAmount) external { + address owner = proxyAdmin.owner(); + + vm.expectEmit(address(l1Withdrawer)); + emit MinWithdrawalAmountUpdated(minWithdrawalAmount, _newMinWithdrawalAmount); + + vm.prank(owner); + l1Withdrawer.setMinWithdrawalAmount(_newMinWithdrawalAmount); + + assertEq(l1Withdrawer.minWithdrawalAmount(), _newMinWithdrawalAmount); + } + + function testFuzz_setMinWithdrawalAmount_asNonOwner_reverts(address _caller) external { + address owner = proxyAdmin.owner(); + vm.assume(_caller != owner); + + uint256 newMinWithdrawalAmount = 2 ether; + + vm.expectRevert(IL1Withdrawer.L1Withdrawer_OnlyProxyAdminOwner.selector); + vm.prank(_caller); + l1Withdrawer.setMinWithdrawalAmount(newMinWithdrawalAmount); + + assertEq(l1Withdrawer.minWithdrawalAmount(), minWithdrawalAmount); + } +} + +/// @title L1Withdrawer_SetRecipient_Test +/// @notice Tests the setRecipient function of the `L1Withdrawer` contract. +contract L1Withdrawer_SetRecipient_Test is L1Withdrawer_TestInit { + function testFuzz_setRecipient_asOwner_succeeds(address _newRecipient) external { + address owner = proxyAdmin.owner(); + + vm.expectEmit(address(l1Withdrawer)); + emit RecipientUpdated(l1FeesDepositor, _newRecipient); + + vm.prank(owner); + l1Withdrawer.setRecipient(_newRecipient); + + assertEq(l1Withdrawer.recipient(), _newRecipient); + } + + function testFuzz_setRecipient_asNonOwner_reverts(address _caller) external { + address owner = proxyAdmin.owner(); + vm.assume(_caller != owner); + + address newRecipient = makeAddr("newRecipient"); + + vm.expectRevert(IL1Withdrawer.L1Withdrawer_OnlyProxyAdminOwner.selector); + vm.prank(_caller); + l1Withdrawer.setRecipient(newRecipient); + + assertEq(l1Withdrawer.recipient(), l1FeesDepositor); + } +} + +/// @title L1Withdrawer_SetWithdrawalGasLimit_Test +/// @notice Tests the setWithdrawalGasLimit function of the `L1Withdrawer` contract. +contract L1Withdrawer_SetWithdrawalGasLimit_Test is L1Withdrawer_TestInit { + function testFuzz_setWithdrawalGasLimit_asOwner_succeeds(uint32 _newWithdrawalGasLimit) external { + address owner = proxyAdmin.owner(); + + _newWithdrawalGasLimit = + uint32(bound(uint256(_newWithdrawalGasLimit), MIN_WITHDRAWAL_GAS_LIMIT, type(uint32).max)); + + vm.expectEmit(address(l1Withdrawer)); + emit WithdrawalGasLimitUpdated(l1Withdrawer.withdrawalGasLimit(), _newWithdrawalGasLimit); + + vm.prank(owner); + l1Withdrawer.setWithdrawalGasLimit(_newWithdrawalGasLimit); + + assertEq(l1Withdrawer.withdrawalGasLimit(), _newWithdrawalGasLimit); + } + + function testFuzz_setWithdrawalGasLimit_asNonOwner_reverts(address _caller) external { + address owner = proxyAdmin.owner(); + vm.assume(_caller != owner); + + uint32 newWithdrawalGasLimit = 250_000; + + vm.expectRevert(IL1Withdrawer.L1Withdrawer_OnlyProxyAdminOwner.selector); + vm.prank(_caller); + l1Withdrawer.setWithdrawalGasLimit(newWithdrawalGasLimit); + + assertEq(l1Withdrawer.withdrawalGasLimit(), l1Withdrawer.withdrawalGasLimit()); + } +} diff --git a/packages/contracts-bedrock/test/L2/LegacyFeeSplitter.t.sol b/packages/contracts-bedrock/test/L2/LegacyFeeSplitter.t.sol new file mode 100644 index 0000000000000..a876aa4c2d754 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/LegacyFeeSplitter.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { FeeSplitter_TestInit } from "test/L2/FeeSplitter.t.sol"; +import { LegacyFeeSplitter } from "test/mocks/LegacyFeeSplitter.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +/// @title LegacyFeeSplitter_DisburseFees_Test +/// @notice Test contract for the LegacyFeeSplitter contract's functionality +contract LegacyFeeSplitter_DisburseFees_Test is FeeSplitter_TestInit { + LegacyFeeSplitter public legacyFeeSplitter; + + function setUp() public override { + super.setUp(); + + legacyFeeSplitter = new LegacyFeeSplitter(); + + // Setup the legacy splitter as the recipient in the vaults + address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); + + vm.startPrank(owner); + IFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)).setRecipient(address(legacyFeeSplitter)); + IFeeVault(payable(Predeploys.BASE_FEE_VAULT)).setRecipient(address(legacyFeeSplitter)); + IFeeVault(payable(Predeploys.L1_FEE_VAULT)).setRecipient(address(legacyFeeSplitter)); + IFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)).setRecipient(address(legacyFeeSplitter)); + vm.stopPrank(); + } + + function test_legacyFeeSplitterDisburseFees_succeeds( + uint256 _sequencerBalance, + uint256 _baseBalance, + uint256 _l1Balance, + uint256 _operatorBalance + ) + public + { + _sequencerBalance = bound( + _sequencerBalance, + IFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)).minWithdrawalAmount(), + type(uint128).max + ); + + _baseBalance = + bound(_baseBalance, IFeeVault(payable(Predeploys.BASE_FEE_VAULT)).minWithdrawalAmount(), type(uint128).max); + + _l1Balance = + bound(_l1Balance, IFeeVault(payable(Predeploys.L1_FEE_VAULT)).minWithdrawalAmount(), type(uint128).max); + + _operatorBalance = bound( + _operatorBalance, IFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)).minWithdrawalAmount(), type(uint128).max + ); + + // Setup mock fee vaults + _mockFeeVaultForSuccessfulWithdrawalWithSplitter( + address(legacyFeeSplitter), Predeploys.SEQUENCER_FEE_WALLET, uint256(_sequencerBalance) + ); + _mockFeeVaultForSuccessfulWithdrawalWithSplitter( + address(legacyFeeSplitter), Predeploys.BASE_FEE_VAULT, uint256(_baseBalance) + ); + _mockFeeVaultForSuccessfulWithdrawalWithSplitter( + address(legacyFeeSplitter), Predeploys.L1_FEE_VAULT, uint256(_l1Balance) + ); + _mockFeeVaultForSuccessfulWithdrawalWithSplitter( + address(legacyFeeSplitter), Predeploys.OPERATOR_FEE_VAULT, uint256(_operatorBalance) + ); + + assertEq(address(legacyFeeSplitter).balance, 0); + legacyFeeSplitter.disburseFees(); + assertEq(address(legacyFeeSplitter).balance, _sequencerBalance + _baseBalance + _l1Balance + _operatorBalance); + } +} diff --git a/packages/contracts-bedrock/test/L2/OperatorFeeVault.t.sol b/packages/contracts-bedrock/test/L2/OperatorFeeVault.t.sol index 7ef57067bb9c0..9f09f567a2ec4 100644 --- a/packages/contracts-bedrock/test/L2/OperatorFeeVault.t.sol +++ b/packages/contracts-bedrock/test/L2/OperatorFeeVault.t.sol @@ -1,13 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { CommonTest } from "test/setup/CommonTest.sol"; +// Interfaces +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; // Libraries -import { Types } from "src/libraries/Types.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; +import { FeeVault_Uncategorized_Test } from "test/L2/FeeVault.t.sol"; +import { Types } from "src/libraries/Types.sol"; import { SemverComp } from "src/libraries/SemverComp.sol"; +import { CommonTest } from "test/setup/CommonTest.sol"; + +/// @title OperatorFeeVault_Uncategorized_Test +/// @notice Test contract for the OperatorFeeVault contract's functionality +contract OperatorFeeVault_Uncategorized_Test is FeeVault_Uncategorized_Test { + /// @dev Sets up the test suite. + function setUp() public virtual override { + super.setUp(); + recipient = deploy.cfg().operatorFeeVaultRecipient(); + feeVaultName = "OperatorFeeVault"; + minWithdrawalAmount = deploy.cfg().operatorFeeVaultMinimumWithdrawalAmount(); + feeVault = IFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)); + withdrawalNetwork = Types.WithdrawalNetwork(uint8(deploy.cfg().operatorFeeVaultWithdrawalNetwork())); + } +} /// @title OperatorFeeVault_Version_Test /// @notice Tests the `version` function of the `OperatorFeeVault` contract. @@ -17,17 +33,3 @@ contract OperatorFeeVault_Version_Test is CommonTest { SemverComp.parse(operatorFeeVault.version()); } } - -/// @title OperatorFeeVault_Constructor_Test -/// @notice Tests the `constructor` of the `OperatorFeeVault` contract. -contract OperatorFeeVault_Constructor_Test is CommonTest { - /// @notice Tests that the constructor sets the correct values. - function test_constructor_succeeds() external view { - assertEq(operatorFeeVault.RECIPIENT(), Predeploys.BASE_FEE_VAULT); - assertEq(operatorFeeVault.recipient(), Predeploys.BASE_FEE_VAULT); - assertEq(operatorFeeVault.MIN_WITHDRAWAL_AMOUNT(), 0); - assertEq(operatorFeeVault.minWithdrawalAmount(), 0); - assertEq(uint8(operatorFeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L2)); - assertEq(uint8(operatorFeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L2)); - } -} diff --git a/packages/contracts-bedrock/test/L2/RevenueSharingIntegration.t.sol b/packages/contracts-bedrock/test/L2/RevenueSharingIntegration.t.sol new file mode 100644 index 0000000000000..777b0ffa9426c --- /dev/null +++ b/packages/contracts-bedrock/test/L2/RevenueSharingIntegration.t.sol @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { CommonTest } from "test/setup/CommonTest.sol"; +import { ISharesCalculator } from "interfaces/L2/ISharesCalculator.sol"; +import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Types } from "src/libraries/Types.sol"; +import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; + +/// @title RevenueSharingIntegration_Test +/// @notice Integration tests for the complete revenue sharing system including +/// FeeSplitter, SuperchainRevSharesCalculator, L1Withdrawer. +contract RevenueSharingIntegration_Test is CommonTest { + /// @notice Basis points scale from SuperchainRevSharesCalculator + uint32 internal constant BASIS_POINT_SCALE = 10_000; + uint32 internal constant GROSS_SHARE_BPS = 250; // 2.5% + uint32 internal constant NET_SHARE_BPS = 1_500; // 15% + uint256 internal disbursementInterval; + + event FeesDisbursed(ISharesCalculator.ShareInfo[] shareInfo, uint256 grossRevenue); + event FeesReceived(address indexed sender, uint256 amount); + event WithdrawalInitiated(address indexed recipient, uint256 amount); + event FundsReceived(address indexed sender, uint256 amount, uint256 newBalance); + + function setUp() public override { + // Enable revenue sharing before calling parent setUp + super.enableRevenueShare(); + super.setUp(); + + disbursementInterval = feeSplitter.feeDisbursementInterval(); + } + + /// @notice Configure all vaults to withdraw to FeeSplitter on L2 + function _configureVaultsForFeeSplitter() private { + // Get the ProxyAdmin owner to configure vaults + address proxyAdminOwner = proxyAdmin.owner(); + + // Configure all vaults to withdraw to FeeSplitter on L2 + vm.startPrank(proxyAdminOwner); + IFeeVault(payable(address(sequencerFeeVault))).setRecipient(address(feeSplitter)); + IFeeVault(payable(address(sequencerFeeVault))).setWithdrawalNetwork(Types.WithdrawalNetwork.L2); + IFeeVault(payable(address(sequencerFeeVault))).setMinWithdrawalAmount(0); + + IFeeVault(payable(address(baseFeeVault))).setRecipient(address(feeSplitter)); + IFeeVault(payable(address(baseFeeVault))).setWithdrawalNetwork(Types.WithdrawalNetwork.L2); + IFeeVault(payable(address(baseFeeVault))).setMinWithdrawalAmount(0); + + IFeeVault(payable(address(l1FeeVault))).setRecipient(address(feeSplitter)); + IFeeVault(payable(address(l1FeeVault))).setWithdrawalNetwork(Types.WithdrawalNetwork.L2); + IFeeVault(payable(address(l1FeeVault))).setMinWithdrawalAmount(0); + + IFeeVault(payable(address(operatorFeeVault))).setRecipient(address(feeSplitter)); + IFeeVault(payable(address(operatorFeeVault))).setWithdrawalNetwork(Types.WithdrawalNetwork.L2); + IFeeVault(payable(address(operatorFeeVault))).setMinWithdrawalAmount(0); + vm.stopPrank(); + } + + /// @notice Helper to fund vaults + function _fundVaults(uint256 _sequencerFees, uint256 _baseFees, uint256 _l1Fees, uint256 _operatorFees) private { + vm.deal(address(sequencerFeeVault), _sequencerFees); + vm.deal(address(baseFeeVault), _baseFees); + vm.deal(address(l1FeeVault), _l1Fees); + vm.deal(address(operatorFeeVault), _operatorFees); + } + + /// @notice Helper to assert the state of all accounts in the revenue sharing flow + /// @param sequencerFeeBalance Expected balance of sequencer fee vault + /// @param baseFeeBalance Expected balance of base fee vault + /// @param l1FeeBalance Expected balance of L1 fee vault + /// @param operatorFeeBalance Expected balance of operator fee vault + /// @param l1WithdrawerBalance Expected balance of L1Withdrawer + /// @param chainFeesRecipientBalance Expected balance of ChainFeesRecipient + function _assertFullFlowState( + uint256 sequencerFeeBalance, + uint256 baseFeeBalance, + uint256 l1FeeBalance, + uint256 operatorFeeBalance, + uint256 l1WithdrawerBalance, + uint256 chainFeesRecipientBalance + ) + private + view + { + // Assert vault balances + assertEq(address(sequencerFeeVault).balance, sequencerFeeBalance, "Incorrect sequencer fee vault balance"); + assertEq(address(baseFeeVault).balance, baseFeeBalance, "Incorrect base fee vault balance"); + assertEq(address(l1FeeVault).balance, l1FeeBalance, "Incorrect L1 fee vault balance"); + assertEq(address(operatorFeeVault).balance, operatorFeeBalance, "Incorrect operator fee vault balance"); + + // Assert recipient balances + assertEq(address(l1Withdrawer).balance, l1WithdrawerBalance, "Incorrect L1Withdrawer balance"); + assertEq(address(chainFeesRecipient).balance, chainFeesRecipientBalance, "Incorrect ChainFeesRecipient balance"); + } + + // Full Revenue Sharing Integration Flow Test + // Vaults: S=Sequencer, B=Base, L=L1, O=Operator + // RevSharesCalculator recipients: L1Withdrawer (share), ChainFeesRecipient (remainder) + // Thresholds: L1Withdrawer=10 ETH + // _________________________________________________________________________________ + // | Vaults (S/B/L/O) | L1Withdrawer | ChainFeesRec | Notes | + // |================================================================================| + // | Initial state | + // |------------------|--------------|--------------|--------------------------------| + // | 0/0/0/0 | 0 | 0 | - | + // |------------------|--------------|--------------|--------------------------------| + // | 1. Fund vaults: S=10, B=8, L=2, O=5 ETH | + // |------------------|--------------|--------------|--------------------------------| + // | 10/8/2/5 | 0 | 0 | - | + // |------------------|--------------|--------------|--------------------------------| + // | 2. Call feeSplitter.disburseFees() | + // | L1Withdrawer receives 3.45 ETH < 10 ETH threshold | + // |------------------|--------------|--------------|--------------------------------| + // | 0/0/0/0 | 3.45 | 21.55 | Accumulating | + // |------------------|--------------|--------------|--------------------------------| + // | 3. Fund vaults: S=40, B=30, L=10, O=20 ETH | + // |------------------|--------------|--------------|--------------------------------| + // | 40/30/10/20 | 3.45 | 21.55 | - | + // |------------------|--------------|--------------|--------------------------------| + // | 4. Call feeSplitter.disburseFees() | + // | L1Withdrawer balance: 3.45 + 13.5 = 16.95 ETH > 10 ETH threshold | + // | Triggers withdrawal | + // |------------------|--------------|--------------|--------------------------------| + // | 0/0/0/0 | 0 | 108.05 | L2→L1 triggered | + // |------------------|--------------|--------------|--------------------------------| + // | 5. Fund vaults: S=5, B=5, L=90, O=0 ETH (high L1 fees, gross share > net) | + // |------------------|--------------|--------------|--------------------------------| + // | 5/5/90/0 | 0 | 108.05 | - | + // |------------------|--------------|--------------|--------------------------------| + // | 6. Call feeSplitter.disburseFees() | + // | L1Withdrawer receives 2.5 ETH < 10 ETH threshold, accumulates | + // |------------------|--------------|--------------|--------------------------------| + // | 0/0/0/0 | 2.5 | 205.55 | Accumulating | + // |__________________|______________|______________|________________________________| + function test_revenueSharing_fullFlow_succeeds() public { + // Configure vaults to withdraw to FeeSplitter + _configureVaultsForFeeSplitter(); + + // Get recipient addresses + address shareRecipient = superchainRevSharesCalculator.shareRecipient(); + address remainderRecipient = superchainRevSharesCalculator.remainderRecipient(); + + // Fund vaults with test amounts + uint256[4] memory fees; + fees[0] = 10 ether; // sequencer + fees[1] = 8 ether; // base + fees[2] = 2 ether; // l1 + fees[3] = 5 ether; // operator + + // Step 1: Fund vaults with small amounts + _fundVaults(fees[0], fees[1], fees[2], fees[3]); + + // Step 2: First disbursement - should accumulate in L1Withdrawer + vm.warp(block.timestamp + disbursementInterval + 1); + feeSplitter.disburseFees(); + + // Calculate expected values: Gross=25, Net=23, Share=max(0.625, 3.45)=3.45 + uint256 expectedShare1 = (23 ether * uint256(NET_SHARE_BPS)) / BASIS_POINT_SCALE; // 3.45 ETH (net > gross) + uint256 expectedRemainder1 = 25 ether - expectedShare1; // 21.55 ETH + + // Assert state + // Vaults: 0/0/0/0 + //L1Withdrawer: 3.45 + //ChainFeesRecipient: 21.55 + _assertFullFlowState(0, 0, 0, 0, expectedShare1, expectedRemainder1); + + // Store remainder balance for later comparison + uint256 remainderAfterFirst = remainderRecipient.balance; + + // Step 3: Fund vaults with larger amounts + fees[0] = 40 ether; // sequencer + fees[1] = 30 ether; // base + fees[2] = 10 ether; // l1 + fees[3] = 20 ether; // operator + + _fundVaults(fees[0], fees[1], fees[2], fees[3]); + + // Calculate expected values: Gross=100, Net=90, Share=max(2.5, 13.5)=13.5 + uint256 expectedShare2 = (90 ether * uint256(NET_SHARE_BPS)) / BASIS_POINT_SCALE; // 13.5 ETH (net > gross) + uint256 expectedRemainder2 = 100 ether - expectedShare2; // 86.5 ETH + uint256 expectedTotalWithdrawal = expectedShare1 + expectedShare2; // 16.95 ETH + + // Expect L2→L1 withdrawal since 16.95 ETH > 10 ETH threshold + vm.expectCall( + Predeploys.L2_CROSS_DOMAIN_MESSENGER, + expectedTotalWithdrawal, + abi.encodeCall( + ICrossDomainMessenger.sendMessage, (l1Withdrawer.recipient(), hex"", l1Withdrawer.withdrawalGasLimit()) + ) + ); + + // Step 4: Second disbursement - should trigger L2→L1 withdrawal + vm.warp(block.timestamp + disbursementInterval + 1); + feeSplitter.disburseFees(); + + // L2ToL1MessagePasser should hold the withdrawn funds + assertEq( + address(l2ToL1MessagePasser).balance, expectedTotalWithdrawal, "L2ToL1MessagePasser should hold 16.95 ETH" + ); + + // Assert state + // Vaults: 0/0/0/0 + //L1Withdrawer: 3.45 + //ChainFeesRecipient: 21.55 + _assertFullFlowState(0, 0, 0, 0, 0, remainderAfterFirst + expectedRemainder2); + + // Store remainder balance for final comparison + uint256 remainderAfterSecond = remainderRecipient.balance; + + // Step 5: Fund vaults again with high L1 fees to make gross share > net share + fees[0] = 5 ether; // sequencer + fees[1] = 5 ether; // base + fees[2] = 90 ether; // l1 (high L1 fees) + fees[3] = 0 ether; // operator + + _fundVaults(fees[0], fees[1], fees[2], fees[3]); + + // Step 6: Third disbursement - gross share should be chosen, no withdrawal triggered + + // Calculate expected values: Gross=100, Net=10, Share=max(2.5, 1.5)=2.5 + uint256 expectedShare3 = (100 ether * uint256(GROSS_SHARE_BPS)) / BASIS_POINT_SCALE; // 2.5 ETH + + vm.warp(block.timestamp + disbursementInterval + 1); + feeSplitter.disburseFees(); + + //L2ToL1MessagePasser should still hold only the previous withdrawal (16.95 ETH) + // The 2.5 ETH stays in L1Withdrawer as it's below threshold + assertEq( + address(l2ToL1MessagePasser).balance, + expectedTotalWithdrawal, + "L2ToL1MessagePasser should still hold 16.95 ETH" + ); + assertEq(shareRecipient.balance, expectedShare3, "L1Withdrawer should have 2.5 ETH"); + + // Final assertions: 0/0/0/0 | 2.5 | 205.55 | + // Total remainder: 21.55 + 86.5 + 97.5 = 205.55 ETH + uint256 finalRemainder = remainderAfterSecond + (100 ether - expectedShare3); + _assertFullFlowState(0, 0, 0, 0, expectedShare3, finalRemainder); + } + + /// @notice Fuzz test for the revenue sharing calculator and disbursement. + /// @dev Checks max(net, gross) share is chosen and disbursed correctly. + function testFuzz_revenueSharing_calculator_succeeds( + uint256 _sequencerFees, + uint256 _baseFees, + uint256 _operatorFees, + uint256 _l1Fees, + uint256 _sequencerFees2, + uint256 _baseFees2, + uint256 _operatorFees2, + uint256 _l1Fees2 + ) + public + { + // Bound inputs to prevent overflow and ensure reasonable test ranges + _sequencerFees = bound(_sequencerFees, 0, 100 ether); + _baseFees = bound(_baseFees, 0, 100 ether); + _operatorFees = bound(_operatorFees, 0, 100 ether); + _l1Fees = bound(_l1Fees, 0, 100 ether); + + _sequencerFees2 = bound(_sequencerFees2, 0, 100 ether); + _baseFees2 = bound(_baseFees2, 0, 100 ether); + _operatorFees2 = bound(_operatorFees2, 0, 100 ether); + _l1Fees2 = bound(_l1Fees2, 0, 100 ether); + + // Handle case where grossShare == 0 + // It is 0 when it sums up to less than 40 because of the GROSS_SHARE_BPS = 250 and BASIS_POINT_SCALE = 10_000 + if (_l1Fees + _sequencerFees + _baseFees + _operatorFees < 40) { + vm.expectRevert(ISuperchainRevSharesCalculator.SharesCalculator_ZeroGrossShare.selector); + superchainRevSharesCalculator.getRecipientsAndAmounts(_sequencerFees, _baseFees, _operatorFees, _l1Fees); + return; + } + + // Configure vaults for disbursement + _configureVaultsForFeeSplitter(); + + { + // Get share info from calculator first + ISharesCalculator.ShareInfo[] memory shareInfo = + superchainRevSharesCalculator.getRecipientsAndAmounts(_sequencerFees, _baseFees, _operatorFees, _l1Fees); + + // Calculate expected values + uint256 grossRevenue = _sequencerFees + _baseFees + _operatorFees + _l1Fees; + uint256 grossShare = (grossRevenue * uint256(GROSS_SHARE_BPS)) / BASIS_POINT_SCALE; + uint256 netShare = ((grossRevenue - _l1Fees) * uint256(NET_SHARE_BPS)) / BASIS_POINT_SCALE; + uint256 expectedShare = grossShare > netShare ? grossShare : netShare; + + // Assert calculator returns correct amounts + assertEq(shareInfo[0].amount, expectedShare, "Share recipient should get max(grossShare, netShare)"); + assertEq( + shareInfo[0].recipient, + superchainRevSharesCalculator.shareRecipient(), + "Share recipient address incorrect" + ); + assertEq(shareInfo[1].amount, grossRevenue - expectedShare, "Remainder recipient should get gross - share"); + assertEq( + shareInfo[1].recipient, + superchainRevSharesCalculator.remainderRecipient(), + "Remainder recipient address incorrect" + ); + + // Fund vaults and perform disbursement + _fundVaults(_sequencerFees, _baseFees, _l1Fees, _operatorFees); + + uint256 l1WithdrawerBalanceBefore = address(l1Withdrawer).balance; + uint256 remainderRecipientBalanceBefore = address(chainFeesRecipient).balance; + uint256 totalShareBalanceAfter = l1WithdrawerBalanceBefore + expectedShare; + bool willTriggerWithdrawal = totalShareBalanceAfter >= l1Withdrawer.minWithdrawalAmount(); + + // Disburse fees + vm.warp(block.timestamp + disbursementInterval + 1); + + if (willTriggerWithdrawal) { + vm.expectEmit(true, false, false, true); + emit WithdrawalInitiated(l1Withdrawer.recipient(), totalShareBalanceAfter); + } + + feeSplitter.disburseFees(); + + // Assert balances + assertEq( + address(chainFeesRecipient).balance, + remainderRecipientBalanceBefore + (grossRevenue - expectedShare), + "Remainder recipient should receive expected remainder" + ); + + if (willTriggerWithdrawal) { + assertEq(address(l1Withdrawer).balance, 0, "L1Withdrawer should be empty after withdrawal"); + assertEq( + address(l2ToL1MessagePasser).balance, + totalShareBalanceAfter, + "L2ToL1MessagePasser should hold the withdrawn share amount" + ); + } else { + assertEq( + address(l1Withdrawer).balance, + totalShareBalanceAfter, + "L1Withdrawer should hold the share amount when below threshold" + ); + } + } + + // Assert all vaults are drained after first disbursement + assertEq( + address(sequencerFeeVault).balance, 0, "Sequencer fee vault should be drained after first disbursement" + ); + assertEq(address(baseFeeVault).balance, 0, "Base fee vault should be drained after first disbursement"); + assertEq(address(l1FeeVault).balance, 0, "L1 fee vault should be drained after first disbursement"); + assertEq(address(operatorFeeVault).balance, 0, "Operator fee vault should be drained after first disbursement"); + + // ========== SECOND DISBURSEMENT ========== + + // Handle case where grossShare == 0 + // It is 0 when it sums up to less than 40 because of the GROSS_SHARE_BPS = 250 and BASIS_POINT_SCALE = 10_000 + if (_l1Fees2 + _sequencerFees2 + _baseFees2 + _operatorFees2 < 40) { + vm.expectRevert(ISuperchainRevSharesCalculator.SharesCalculator_ZeroGrossShare.selector); + superchainRevSharesCalculator.getRecipientsAndAmounts(_sequencerFees2, _baseFees2, _operatorFees2, _l1Fees2); + return; + } + + { + // Get share info from calculator for second disbursement + ISharesCalculator.ShareInfo[] memory shareInfo2 = superchainRevSharesCalculator.getRecipientsAndAmounts( + _sequencerFees2, _baseFees2, _operatorFees2, _l1Fees2 + ); + + // Calculate expected values for second disbursement + uint256 grossRevenue2 = _sequencerFees2 + _baseFees2 + _operatorFees2 + _l1Fees2; + uint256 grossShare2 = (grossRevenue2 * uint256(GROSS_SHARE_BPS)) / BASIS_POINT_SCALE; + uint256 netShare2 = ((grossRevenue2 - _l1Fees2) * uint256(NET_SHARE_BPS)) / BASIS_POINT_SCALE; + uint256 expectedShare2 = grossShare2 > netShare2 ? grossShare2 : netShare2; + + // Assert calculator returns correct amounts for second disbursement + assertEq( + shareInfo2[0].amount, + expectedShare2, + "Share recipient should get max(grossShare, netShare) for second disbursement" + ); + assertEq( + shareInfo2[1].amount, + grossRevenue2 - expectedShare2, + "Remainder recipient should get gross - share for second disbursement" + ); + + // Fund vaults for second disbursement + _fundVaults(_sequencerFees2, _baseFees2, _l1Fees2, _operatorFees2); + + uint256 l1WithdrawerBalanceBefore2 = address(l1Withdrawer).balance; + uint256 remainderRecipientBalanceBefore2 = address(chainFeesRecipient).balance; + uint256 l2ToL1MessagePasserBalanceBefore2 = address(l2ToL1MessagePasser).balance; + uint256 totalShareBalanceAfter2 = l1WithdrawerBalanceBefore2 + expectedShare2; + bool willTriggerWithdrawal2 = totalShareBalanceAfter2 >= l1Withdrawer.minWithdrawalAmount(); + + // Disburse fees for second time + vm.warp(block.timestamp + disbursementInterval + 1); + + if (willTriggerWithdrawal2) { + vm.expectEmit(true, false, false, true); + emit WithdrawalInitiated(l1Withdrawer.recipient(), totalShareBalanceAfter2); + } + + feeSplitter.disburseFees(); + + // Assert balances + assertEq( + address(chainFeesRecipient).balance, + remainderRecipientBalanceBefore2 + (grossRevenue2 - expectedShare2), + "Remainder recipient should receive expected remainder from second disbursement" + ); + + if (willTriggerWithdrawal2) { + assertEq(address(l1Withdrawer).balance, 0, "L1Withdrawer should be empty after second withdrawal"); + assertEq( + address(l2ToL1MessagePasser).balance, + l2ToL1MessagePasserBalanceBefore2 + totalShareBalanceAfter2, + "L2ToL1MessagePasser should hold the total withdrawn amount after second disbursement" + ); + } else { + assertEq( + address(l1Withdrawer).balance, + totalShareBalanceAfter2, + "L1Withdrawer should hold accumulated share amount when below threshold after second disbursement" + ); + } + + // Assert all vaults are drained after second disbursement + assertEq( + address(sequencerFeeVault).balance, 0, "Sequencer fee vault should be drained after second disbursement" + ); + assertEq(address(baseFeeVault).balance, 0, "Base fee vault should be drained after second disbursement"); + assertEq(address(l1FeeVault).balance, 0, "L1 fee vault should be drained after second disbursement"); + assertEq( + address(operatorFeeVault).balance, 0, "Operator fee vault should be drained after second disbursement" + ); + } + } +} diff --git a/packages/contracts-bedrock/test/L2/SequencerFeeVault.t.sol b/packages/contracts-bedrock/test/L2/SequencerFeeVault.t.sol index ba72e946990d9..5ef59377710e8 100644 --- a/packages/contracts-bedrock/test/L2/SequencerFeeVault.t.sol +++ b/packages/contracts-bedrock/test/L2/SequencerFeeVault.t.sol @@ -1,190 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing -import { CommonTest } from "test/setup/CommonTest.sol"; -import { Reverter } from "test/mocks/Callers.sol"; -import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; - -// Contracts +// Interfaces +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; import { ISequencerFeeVault } from "interfaces/L2/ISequencerFeeVault.sol"; // Libraries -import { Hashing } from "src/libraries/Hashing.sol"; -import { Types } from "src/libraries/Types.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; - -/// @title SequencerFeeVault_TestInit -/// @notice Reusable test initialization for `SequencerFeeVault` tests. -abstract contract SequencerFeeVault_TestInit is CommonTest { - address recipient; +import { FeeVault_Uncategorized_Test } from "test/L2/FeeVault.t.sol"; +import { Types } from "src/libraries/Types.sol"; +/// @title SequencerFeeVault_Uncategorized_Test +/// @notice Test contract for the SequencerFeeVault contract's functionality +contract SequencerFeeVault_Uncategorized_Test is FeeVault_Uncategorized_Test { /// @dev Sets up the test suite. function setUp() public virtual override { super.setUp(); recipient = deploy.cfg().sequencerFeeVaultRecipient(); + feeVaultName = "SequencerFeeVault"; + minWithdrawalAmount = deploy.cfg().sequencerFeeVaultMinimumWithdrawalAmount(); + feeVault = IFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)); + withdrawalNetwork = Types.WithdrawalNetwork(uint8(deploy.cfg().sequencerFeeVaultWithdrawalNetwork())); } -} - -/// @title SequencerFeeVault_Constructor_Test -/// @notice Tests the `constructor` function of the `SequencerFeeVault` contract. -contract SequencerFeeVault_Constructor_Test is SequencerFeeVault_TestInit { - /// @notice Tests that the l1 fee wallet is correct. - function test_constructor_succeeds() external view { - assertEq(sequencerFeeVault.l1FeeWallet(), recipient); - assertEq(sequencerFeeVault.RECIPIENT(), recipient); - assertEq(sequencerFeeVault.recipient(), recipient); - assertEq(sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT(), deploy.cfg().sequencerFeeVaultMinimumWithdrawalAmount()); - assertEq(sequencerFeeVault.minWithdrawalAmount(), deploy.cfg().sequencerFeeVaultMinimumWithdrawalAmount()); - assertEq(uint8(sequencerFeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L1)); - assertEq(uint8(sequencerFeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L1)); - } -} - -/// @title SequencerFeeVault_Receive_Test -/// @notice Tests the `receive` function of the `SequencerFeeVault` contract. -contract SequencerFeeVault_Receive_Test is SequencerFeeVault_TestInit { - /// @notice Tests that the fee vault is able to receive ETH. - function test_receive_succeeds() external { - uint256 balance = address(sequencerFeeVault).balance; - - vm.prank(alice); - (bool success,) = address(sequencerFeeVault).call{ value: 100 }(hex""); - - assertEq(success, true); - assertEq(address(sequencerFeeVault).balance, balance + 100); - } -} - -/// @title SequencerFeeVault_Withdraw_Test -/// @notice Tests the `withdraw` function of the `SequencerFeeVault` contract. -contract SequencerFeeVault_Withdraw_Test is SequencerFeeVault_TestInit { - /// @notice Helper function to set up L2 withdrawal configuration. - function _setupL2Withdrawal() internal { - // Alter the deployment to use WithdrawalNetwork.L2 - vm.etch( - EIP1967Helper.getImplementation(Predeploys.SEQUENCER_FEE_WALLET), - address( - DeployUtils.create1({ - _name: "SequencerFeeVault", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - ISequencerFeeVault.__constructor__, - ( - deploy.cfg().sequencerFeeVaultRecipient(), - deploy.cfg().sequencerFeeVaultMinimumWithdrawalAmount(), - Types.WithdrawalNetwork.L2 - ) - ) - ) - }) - ).code - ); - - recipient = deploy.cfg().sequencerFeeVaultRecipient(); - } - - /// @notice Tests that `withdraw` reverts if the balance is less than the minimum withdrawal - /// amount. - function test_withdraw_notEnough_reverts() external { - assert(address(sequencerFeeVault).balance < sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT()); - - vm.expectRevert("FeeVault: withdrawal amount must be greater than minimum withdrawal amount"); - sequencerFeeVault.withdraw(); - } - - /// @notice Tests that `withdraw` successfully initiates a withdrawal to L1. - function test_withdraw_toL1_succeeds() external { - uint256 amount = sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT() + 1; - vm.deal(address(sequencerFeeVault), amount); - - // No ether has been withdrawn yet - assertEq(sequencerFeeVault.totalProcessed(), 0); - - vm.expectEmit(address(Predeploys.SEQUENCER_FEE_WALLET)); - emit Withdrawal(address(sequencerFeeVault).balance, recipient, address(this)); - vm.expectEmit(address(Predeploys.SEQUENCER_FEE_WALLET)); - emit Withdrawal(address(sequencerFeeVault).balance, recipient, address(this), Types.WithdrawalNetwork.L1); - - // The entire vault's balance is withdrawn - vm.expectCall(Predeploys.L2_TO_L1_MESSAGE_PASSER, address(sequencerFeeVault).balance, hex""); - - // The message is passed to the correct recipient - vm.expectEmit(Predeploys.L2_TO_L1_MESSAGE_PASSER); - emit MessagePassed( - l2ToL1MessagePasser.messageNonce(), - address(sequencerFeeVault), - recipient, - amount, - 400_000, - hex"", - Hashing.hashWithdrawal( - Types.WithdrawalTransaction({ - nonce: l2ToL1MessagePasser.messageNonce(), - sender: address(sequencerFeeVault), - target: recipient, - value: amount, - gasLimit: 400_000, - data: hex"" - }) - ) - ); - - sequencerFeeVault.withdraw(); - - // The withdrawal was successful - assertEq(sequencerFeeVault.totalProcessed(), amount); - assertEq(address(sequencerFeeVault).balance, 0); - assertEq(Predeploys.L2_TO_L1_MESSAGE_PASSER.balance, amount); - } - - /// @notice Tests that `withdraw` successfully initiates a withdrawal to L2. - function test_withdraw_toL2_succeeds() external { - _setupL2Withdrawal(); - - uint256 amount = sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT() + 1; - vm.deal(address(sequencerFeeVault), amount); - - // No ether has been withdrawn yet - assertEq(sequencerFeeVault.totalProcessed(), 0); - - vm.expectEmit(address(Predeploys.SEQUENCER_FEE_WALLET)); - emit Withdrawal(address(sequencerFeeVault).balance, sequencerFeeVault.RECIPIENT(), address(this)); - vm.expectEmit(address(Predeploys.SEQUENCER_FEE_WALLET)); - emit Withdrawal( - address(sequencerFeeVault).balance, sequencerFeeVault.RECIPIENT(), address(this), Types.WithdrawalNetwork.L2 - ); - - // The entire vault's balance is withdrawn - vm.expectCall(recipient, address(sequencerFeeVault).balance, bytes("")); - - sequencerFeeVault.withdraw(); - - // The withdrawal was successful - assertEq(sequencerFeeVault.totalProcessed(), amount); - assertEq(address(sequencerFeeVault).balance, 0); - assertEq(recipient.balance, amount); - } - - /// @notice Tests that `withdraw` fails if the Recipient reverts. This also serves to simulate - /// a situation where insufficient gas is provided to the RECIPIENT. - function test_withdraw_toL2recipientReverts_fails() external { - _setupL2Withdrawal(); - - uint256 amount = sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT(); - - vm.deal(address(sequencerFeeVault), amount); - // No ether has been withdrawn yet - assertEq(sequencerFeeVault.totalProcessed(), 0); - - // Ensure the RECIPIENT reverts - vm.etch(sequencerFeeVault.RECIPIENT(), type(Reverter).runtimeCode); - // The entire vault's balance is withdrawn - vm.expectCall(recipient, address(sequencerFeeVault).balance, bytes("")); - vm.expectRevert("FeeVault: failed to send ETH to L2 fee recipient"); - sequencerFeeVault.withdraw(); - assertEq(sequencerFeeVault.totalProcessed(), 0); + function test_constructor_l1FeeWallet_succeeds() external view { + assertEq(ISequencerFeeVault(payable(address(feeVault))).l1FeeWallet(), recipient); } } diff --git a/packages/contracts-bedrock/test/L2/SuperchainRevSharesCalculator.t.sol b/packages/contracts-bedrock/test/L2/SuperchainRevSharesCalculator.t.sol new file mode 100644 index 0000000000000..a067339b1e9e3 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/SuperchainRevSharesCalculator.t.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Interfaces +import { ISharesCalculator } from "interfaces/L2/ISharesCalculator.sol"; +import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; + +// Libraries + +/// @notice Base setup contract for SuperchainRevSharesCalculator tests. +contract SuperchainRevSharesCalculator_TestInit is CommonTest { + uint256 internal constant BASIS_POINT_SCALE = 10_000; + uint256 internal constant GROSS_SHARE_BPS = 250; + uint256 internal constant NET_SHARE_BPS = 1_500; + + address payable shareRecipient; + address payable remainderRecipient; + + event ShareRecipientUpdated(address indexed oldShareRecipient, address indexed newShareRecipient); + event RemainderRecipientUpdated(address indexed oldRemainderRecipient, address indexed newRemainderRecipient); + + function setUp() public virtual override { + // Enable revenue sharing before calling parent setUp + super.enableRevenueShare(); + super.setUp(); + + shareRecipient = payable(address(l1Withdrawer)); + remainderRecipient = payable(deploy.cfg().chainFeesRecipient()); + } +} + +/// @notice Tests for SuperchainRevSharesCalculator constructor. +contract SuperchainRevSharesCalculator_Constructor_Test is SuperchainRevSharesCalculator_TestInit { + /// @notice Tests that constructor + function test_constructor_succeeds() external view { + // Verify constants are set correctly on the deployed calculator + assertEq(superchainRevSharesCalculator.version(), "1.0.0"); + assertEq(superchainRevSharesCalculator.BASIS_POINT_SCALE(), BASIS_POINT_SCALE); + assertEq(superchainRevSharesCalculator.GROSS_SHARE_BPS(), GROSS_SHARE_BPS); + assertEq(superchainRevSharesCalculator.NET_SHARE_BPS(), NET_SHARE_BPS); + + // Verify share and remainder recipients are set + assertEq(address(superchainRevSharesCalculator.shareRecipient()), address(shareRecipient)); + assertEq(address(superchainRevSharesCalculator.remainderRecipient()), address(remainderRecipient)); + } +} + +/// @notice Tests for SuperchainRevSharesCalculator setShareRecipient function success cases. +contract SuperchainRevSharesCalculator_SetShareRecipient_Test is SuperchainRevSharesCalculator_TestInit { + /// @notice Tests that setShareRecipient reverts when not called by ProxyAdmin owner. + function testFuzz_setShareRecipient_notProxyAdminOwner_reverts( + address _caller, + address payable _newShareRecipient + ) + external + { + vm.assume(_caller != proxyAdminOwner); + + vm.expectRevert(ISuperchainRevSharesCalculator.SharesCalculator_OnlyProxyAdminOwner.selector); + vm.prank(_caller); + superchainRevSharesCalculator.setShareRecipient(_newShareRecipient); + } + + /// @notice Tests that setShareRecipient updates recipient and emits event. + function testFuzz_setShareRecipient_succeeds(address payable _newShareRecipient) external { + vm.expectEmit(address(superchainRevSharesCalculator)); + emit ShareRecipientUpdated(shareRecipient, _newShareRecipient); + + vm.prank(proxyAdminOwner); + superchainRevSharesCalculator.setShareRecipient(_newShareRecipient); + + assertEq(address(superchainRevSharesCalculator.shareRecipient()), address(_newShareRecipient)); + } +} + +/// @notice Tests for SuperchainRevSharesCalculator setRemainderRecipient function success cases. +contract SuperchainRevSharesCalculator_SetRemainderRecipient_Test is SuperchainRevSharesCalculator_TestInit { + /// @notice Tests that setRemainderRecipient reverts when not called by ProxyAdmin owner. + function testFuzz_setRemainderRecipient_notProxyAdminOwner_reverts( + address _caller, + address payable _newRemainderRecipient + ) + external + { + vm.assume(_caller != proxyAdminOwner); + + vm.expectRevert(ISuperchainRevSharesCalculator.SharesCalculator_OnlyProxyAdminOwner.selector); + vm.prank(_caller); + superchainRevSharesCalculator.setRemainderRecipient(_newRemainderRecipient); + } + + /// @notice Tests that setRemainderRecipient updates recipient and emits event. + function testFuzz_setRemainderRecipient_succeeds(address payable _newRemainderRecipient) external { + vm.expectEmit(address(superchainRevSharesCalculator)); + emit RemainderRecipientUpdated(remainderRecipient, _newRemainderRecipient); + + vm.prank(proxyAdminOwner); + superchainRevSharesCalculator.setRemainderRecipient(_newRemainderRecipient); + + assertEq(address(superchainRevSharesCalculator.remainderRecipient()), address(_newRemainderRecipient)); + } +} + +/// @notice Tests for SuperchainRevSharesCalculator getRecipientsAndAmounts function. +contract SuperchainRevSharesCalculator_getRecipientsAndAmounts_Test is SuperchainRevSharesCalculator_TestInit { + /// @notice Test that getRecipientsAndAmounts reverts when gross share is calculated to be 0. + function testFuzz_getRecipientsAndAmounts_zeroGrossShare_reverts( + uint256 _sequencerFees, + uint256 _baseFees, + uint256 _operatorFees, + uint256 _l1Fees + ) + external + { + // Bound each fee to ensure total revenue < 40 to make gross share = 0 + // With GROSS_SHARE_BPS = 250 and BASIS_POINT_SCALE = 10_000: + // grossShare = (totalRevenue * 250) / 10_000 = 0 when totalRevenue < 40 + _sequencerFees = bound(_sequencerFees, 1, 10); + _baseFees = bound(_baseFees, 1, 10); + _operatorFees = bound(_operatorFees, 1, 10); + _l1Fees = bound(_l1Fees, 1, 9); + + // Verify that gross share would be 0 due to integer division + uint256 totalRevenue = _sequencerFees + _baseFees + _operatorFees + _l1Fees; + uint256 grossShare = (totalRevenue * uint256(superchainRevSharesCalculator.GROSS_SHARE_BPS())) + / uint256(superchainRevSharesCalculator.BASIS_POINT_SCALE()); + assertEq(grossShare, 0, "Gross share should be 0"); + + // Expect the function to revert with the correct error + vm.expectRevert(ISuperchainRevSharesCalculator.SharesCalculator_ZeroGrossShare.selector); + superchainRevSharesCalculator.getRecipientsAndAmounts(_sequencerFees, _baseFees, _operatorFees, _l1Fees); + } + + /// @notice Fuzz test for cases where gross share is higher than net share. + function testFuzz_getRecipientsAndAmounts_grossShareHigher_succeeds( + uint256 _sequencerFees, + uint256 _baseFees, + uint256 _operatorFees, + uint256 _l1Fees + ) + external + view + { + // Use smaller bounds to prevent overflow + _sequencerFees = bound(_sequencerFees, 10000, type(uint112).max); + _baseFees = bound(_baseFees, 10000, type(uint112).max); + _operatorFees = bound(_operatorFees, 10000, type(uint112).max); + + // Calculate other fees (without L1 fees) + uint256 otherFees = _sequencerFees + _baseFees + _operatorFees; + + // For gross > net: we need L1 fees to be very high relative to other fees + // Set L1 fees to be 90% of total revenue to ensure gross > net + uint256 minL1Fees = otherFees * 9; // L1 fees = 90% of other fees, so total = 10 * otherFees, L1 = 9 * otherFees + _l1Fees = bound(_l1Fees, minL1Fees, minL1Fees + 10000); + + ISharesCalculator.ShareInfo[] memory result = + superchainRevSharesCalculator.getRecipientsAndAmounts(_sequencerFees, _baseFees, _operatorFees, _l1Fees); + + // Verify structure + assertEq(result.length, 2); + assertEq(address(result[0].recipient), address(shareRecipient)); + assertEq(address(result[1].recipient), address(remainderRecipient)); + + // Calculate expected values + uint256 totalRevenue = _sequencerFees + _baseFees + _operatorFees + _l1Fees; + uint256 grossShare = (totalRevenue * uint256(superchainRevSharesCalculator.GROSS_SHARE_BPS())) + / uint256(superchainRevSharesCalculator.BASIS_POINT_SCALE()); + uint256 netRevenue = totalRevenue - _l1Fees; + uint256 netShare = (netRevenue * uint256(superchainRevSharesCalculator.NET_SHARE_BPS())) + / uint256(superchainRevSharesCalculator.BASIS_POINT_SCALE()); + + // Verify gross share is indeed higher + assertGt(grossShare, netShare, "Gross share should be higher than net share"); + + // Verify calculations + assertEq(result[0].amount, grossShare); + assertEq(result[1].amount, totalRevenue - grossShare); + + // Verify total conservation + assertEq(result[0].amount + result[1].amount, totalRevenue); + } + + /// @notice Fuzz test for cases where net share is higher than gross share. + function testFuzz_getRecipientsAndAmounts_netShareHigher_succeeds( + uint256 _sequencerFees, + uint256 _baseFees, + uint256 _operatorFees, + uint256 _l1Fees + ) + external + view + { + // Use smaller bounds to prevent overflow + _sequencerFees = bound(_sequencerFees, 10000, type(uint112).max); + _baseFees = bound(_baseFees, 10000, type(uint112).max); + _operatorFees = bound(_operatorFees, 10000, type(uint112).max); + + // Calculate other fees (without L1 fees) + uint256 otherFees = _sequencerFees + _baseFees + _operatorFees; + + // For net > gross: we need L1 fees to be very low relative to other fees + // Set L1 fees to be 10% of other fees to ensure net > gross + uint256 maxL1Fees = otherFees / 10; // L1 fees = 10% of other fees + _l1Fees = bound(_l1Fees, 1, maxL1Fees); + + ISharesCalculator.ShareInfo[] memory result = + superchainRevSharesCalculator.getRecipientsAndAmounts(_sequencerFees, _baseFees, _operatorFees, _l1Fees); + + // Verify structure + assertEq(result.length, 2); + assertEq(address(result[0].recipient), address(shareRecipient)); + assertEq(address(result[1].recipient), address(remainderRecipient)); + + // Calculate expected values + uint256 totalRevenue = _sequencerFees + _baseFees + _operatorFees + _l1Fees; + uint256 grossShare = (totalRevenue * uint256(superchainRevSharesCalculator.GROSS_SHARE_BPS())) + / uint256(superchainRevSharesCalculator.BASIS_POINT_SCALE()); + uint256 netRevenue = totalRevenue - _l1Fees; + uint256 netShare = (netRevenue * uint256(superchainRevSharesCalculator.NET_SHARE_BPS())) + / uint256(superchainRevSharesCalculator.BASIS_POINT_SCALE()); + + // Verify net share is indeed higher + assertGt(netShare, grossShare, "Net share should be higher than gross share"); + + // Verify calculations + assertEq(result[0].amount, netShare); + assertEq(result[1].amount, totalRevenue - netShare); + + // Verify total conservation + assertEq(result[0].amount + result[1].amount, totalRevenue); + } + + /// @notice Comprehensive fuzz test for calculation logic. + function testFuzz_getRecipientsAndAmounts_succeeds( + uint256 _sequencerFees, + uint256 _baseFees, + uint256 _operatorFees, + uint256 _l1Fees + ) + external + view + { + // Use uint112 to prevent overflow when adding & 10000 to prevent 0 fee revert + _sequencerFees = bound(_sequencerFees, 10000, type(uint112).max); + _baseFees = bound(_baseFees, 10000, type(uint112).max); + _operatorFees = bound(_operatorFees, 10000, type(uint112).max); + _l1Fees = bound(_l1Fees, 10000, type(uint112).max); + + ISharesCalculator.ShareInfo[] memory result = + superchainRevSharesCalculator.getRecipientsAndAmounts(_sequencerFees, _baseFees, _operatorFees, _l1Fees); + + // Verify structure + assertEq(result.length, 2); + assertEq(address(result[0].recipient), address(shareRecipient)); + assertEq(address(result[1].recipient), address(remainderRecipient)); + + // Calculate expected values + uint256 totalRevenue = _sequencerFees + _baseFees + _operatorFees + _l1Fees; + uint256 grossShare = (totalRevenue * uint256(superchainRevSharesCalculator.GROSS_SHARE_BPS())) + / uint256(superchainRevSharesCalculator.BASIS_POINT_SCALE()); + uint256 netRevenue = totalRevenue - _l1Fees; + uint256 netShare = (netRevenue * uint256(superchainRevSharesCalculator.NET_SHARE_BPS())) + / uint256(superchainRevSharesCalculator.BASIS_POINT_SCALE()); + uint256 expectedShareAmount = grossShare > netShare ? grossShare : netShare; + + // Verify calculations + assertEq(result[0].amount, expectedShareAmount); + assertEq(result[1].amount, totalRevenue - expectedShareAmount); + + // Verify total conservation + assertEq(result[0].amount + result[1].amount, totalRevenue); + } +} diff --git a/packages/contracts-bedrock/test/invariants/FeeSplit.t.sol b/packages/contracts-bedrock/test/invariants/FeeSplit.t.sol new file mode 100644 index 0000000000000..f8d36eb6b1973 --- /dev/null +++ b/packages/contracts-bedrock/test/invariants/FeeSplit.t.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { StdUtils } from "forge-std/StdUtils.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { CommonTest } from "test/setup/CommonTest.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { IFeeSplitter } from "interfaces/L2/IFeeSplitter.sol"; +import { IL1Withdrawer } from "interfaces/L2/IL1Withdrawer.sol"; +import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; + +/// @notice A struct to keep track of the state when a disburse call fails +struct DisburseFailureState { + uint256 sequencerFeeVaultBalance; + uint256 sequencerFeeVaultMinWithdrawalAmount; + uint256 baseFeeVaultBalance; + uint256 baseFeeVaultMinWithdrawalAmount; + uint256 l1FeeVaultBalance; + uint256 l1FeeVaultMinWithdrawalAmount; + uint256 operatorFeeVaultBalance; + uint256 operatorFeeVaultMinWithdrawalAmount; + uint256 attemptTimestamp; + bytes reason; +} + +/// @title Handler to call the disburseFees function +contract FeeSplitter_Disburser is StdUtils { + /// @notice Vm instance + Vm internal vm; + + /// @notice FeeSplitter contract + IFeeSplitter public feeSplitter; + + IL1Withdrawer public l1Withdrawer; + + /// @notice Flag to track if a disburseFees() call failed + bool public txFailed; + + /// @notice Keep track of the balances and timestamp of a failed disbursement + DisburseFailureState internal failureState; + + /// @notice Aggregate of the vault balances disbursed + uint256 public ghost_grossRevenueDisbursed; + + /// @notice Keep track of the last aggregated fee disbursed + uint256 public ghost_lastDisbursementAmount; + + /// @notice Keep track of the l1withdrawer should have withdrawn + bool public l1withdrawerShouldHaveWithdrawn; + + constructor(Vm _vm, IFeeSplitter _feeSplitter, IL1Withdrawer _l1Withdrawer) { + vm = _vm; + feeSplitter = _feeSplitter; + l1Withdrawer = _l1Withdrawer; + } + + /// @notice Get the failure state (convenience, to keep the struct) + function getFailureState() external view returns (DisburseFailureState memory failureState_) { + failureState_ = failureState; + } + + /// @notice handler for FeeSplitter.disburseFees() + /// @dev It update important ghost var, in both success and failure cases: + /// - success: update the overall amount disbursed (ie add the sum of the vaults balances before disbursement) + /// - failure: update the failure state (all vault balances and the current timestamp) + function disburse() public { + uint256 _sequencerFees = address(Predeploys.SEQUENCER_FEE_WALLET).balance; + uint256 _baseFees = address(Predeploys.BASE_FEE_VAULT).balance; + uint256 _l1Fees = address(Predeploys.L1_FEE_VAULT).balance; + uint256 _operatorFees = address(Predeploys.OPERATOR_FEE_VAULT).balance; + uint256 _aggregateVaultsBalances = _sequencerFees + _baseFees + _l1Fees + _operatorFees; + + uint256 _l1withdrawerBalanceBeforeDisbursement = address(l1Withdrawer).balance; + + try feeSplitter.disburseFees() { + // reset the fail flags + txFailed = false; + delete failureState; + + // Check if the l1withdrawer should have been triggered and empty its balance + uint256 _amountToL1Withdrawer = feeSplitter.sharesCalculator().getRecipientsAndAmounts( + _sequencerFees, _baseFees, _operatorFees, _l1Fees + )[0].amount; + + if ( + _l1withdrawerBalanceBeforeDisbursement + _amountToL1Withdrawer + >= IL1Withdrawer(payable(l1Withdrawer)).minWithdrawalAmount() + ) { + l1withdrawerShouldHaveWithdrawn = true; + } else { + l1withdrawerShouldHaveWithdrawn = false; + } + + ghost_grossRevenueDisbursed += _aggregateVaultsBalances; + } catch (bytes memory _reason) { + // keep track of the failing state + txFailed = true; + failureState.sequencerFeeVaultBalance = address(Predeploys.SEQUENCER_FEE_WALLET).balance; + failureState.sequencerFeeVaultMinWithdrawalAmount = + IFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)).minWithdrawalAmount(); + failureState.baseFeeVaultBalance = address(Predeploys.BASE_FEE_VAULT).balance; + failureState.baseFeeVaultMinWithdrawalAmount = + IFeeVault(payable(Predeploys.BASE_FEE_VAULT)).minWithdrawalAmount(); + failureState.l1FeeVaultBalance = address(Predeploys.L1_FEE_VAULT).balance; + failureState.l1FeeVaultMinWithdrawalAmount = + IFeeVault(payable(Predeploys.L1_FEE_VAULT)).minWithdrawalAmount(); + failureState.operatorFeeVaultBalance = address(Predeploys.OPERATOR_FEE_VAULT).balance; + failureState.operatorFeeVaultMinWithdrawalAmount = + IFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)).minWithdrawalAmount(); + failureState.attemptTimestamp = block.timestamp; + failureState.reason = _reason; + } + } +} + +/// @title Handler to set arbitrary preconditions (balance and block timestamp) +contract FeeSplitter_Preconditions is CommonTest { + /// @notice modify the min amount to withdraw from a vault + /// @dev We include the case where min amount is 0 (ie no minimum to withdraw) + /// @param _minAmount The seed of the min amount to withdraw from a vault + /// @param _vaultIndex The seed of the vault's index to set the min amount to withdraw from + function setMinAmount(uint256 _minAmount, uint256 _vaultIndex) public { + _vaultIndex = bound(_vaultIndex, 0, 3); + + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + + if (_vaultIndex == 0) { + IFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)).setMinWithdrawalAmount(_minAmount); + } else if (_vaultIndex == 1) { + IFeeVault(payable(Predeploys.BASE_FEE_VAULT)).setMinWithdrawalAmount(_minAmount); + } else if (_vaultIndex == 2) { + IFeeVault(payable(Predeploys.L1_FEE_VAULT)).setMinWithdrawalAmount(_minAmount); + } else if (_vaultIndex == 3) { + IFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)).setMinWithdrawalAmount(_minAmount); + } + } + + /// @notice Warp the block timestamp + /// @param _seconds The seed of the seconds to warp the block timestamp by + function warp(uint256 _seconds) public { + _seconds = bound(_seconds, 0, 10 days); + vm.warp(block.timestamp + _seconds); + } + + /// @notice Add collected fee to a vault + /// @param _amount The seed of amount to add to the vault + /// @param _vaultIndex The seed of the vault's index to add the fee to + /// @dev The net and gross revenue have an upper bound to avoid overflows in the shares calculator + function addCollectedFeeToVault(uint256 _amount, uint256 _vaultIndex) public { + _vaultIndex = bound(_vaultIndex, 0, 3); + _amount = bound(_amount, 0, 100 ether); + + if (_vaultIndex == 0) { + vm.deal(address(Predeploys.SEQUENCER_FEE_WALLET), _amount); + } else if (_vaultIndex == 1) { + vm.deal(address(Predeploys.BASE_FEE_VAULT), _amount); + } else if (_vaultIndex == 2) { + vm.deal(address(Predeploys.L1_FEE_VAULT), _amount); + } else if (_vaultIndex == 3) { + vm.deal(address(Predeploys.OPERATOR_FEE_VAULT), _amount); + } + } +} +/// @title Handler to set the call distribution bias for setMinAmount +/// @notice This bias the distribution of calls to setMinAmount, favoring 75% of the calls to set the min amount to 0 +/// as it is the "vanilla" case. +/// @dev See https://getfoundry.sh/forge/advanced-testing/invariant-testing#function-call-probability-distribution +/// We favor this over a single wrapper with a seed to branch to keep distinct edges/selectors while using a corpus + +contract FeeSplitter_CallDistributionBias is FeeSplitter_Preconditions { + uint256 public amountZeroCalls; + uint256 public amountNotZeroCalls; + + function setMinAmountZero1(uint256 _vaultIndex) public { + amountZeroCalls++; + setMinAmount(0, _vaultIndex); + } + + function setMinAmountZero2(uint256 _vaultIndex) public { + amountZeroCalls++; + setMinAmount(0, _vaultIndex); + } + + function setMinAmountZero3(uint256 _vaultIndex) public { + amountZeroCalls++; + setMinAmount(0, _vaultIndex); + } + + function setMinAmountNotZero(uint256 _minAmount, uint256 _vaultIndex) public { + amountNotZeroCalls++; + setMinAmount(_minAmount, _vaultIndex); + } +} + +/// @title Invariants for the FeeSplitter +/// @notice The invariants tested are: +/// - no dust accumulation in the FeeSplitter +/// - total disbursed fees should always be equal to the sum of the vault balances before disbursement +/// - disburseFees can only revert if either one of the vault has a balance below it's minimum withdrawal amount +/// or if the disbursement interval has not been reached yet +/// @dev These invariants are covering the system formed by: +/// FeeSplitter, 4 FeeVault's, SuperchainRevSharesCalculator, L1Withdrawer +contract FeeSplitter_Invariant is CommonTest { + /// @notice Handler for disbursing fees + FeeSplitter_Disburser public disburser; + + /// @notice Handler to set test preconditions + FeeSplitter_Preconditions public preconditions; + + /// @notice Handler to set the call distribution bias + FeeSplitter_CallDistributionBias public callDistributionBias; + + /// @notice Setup: enable the revenue share, deploy handlers and target them. + function setUp() public override { + super.enableRevenueShare(); + super.setUp(); + + disburser = new FeeSplitter_Disburser(vm, feeSplitter, l1Withdrawer); + preconditions = new FeeSplitter_Preconditions(); + callDistributionBias = new FeeSplitter_CallDistributionBias(); + + targetContract(address(disburser)); + + targetContract(address(callDistributionBias)); + bytes4[] memory selectors = new bytes4[](4); + selectors[0] = FeeSplitter_CallDistributionBias.setMinAmountZero1.selector; + selectors[1] = FeeSplitter_CallDistributionBias.setMinAmountZero2.selector; + selectors[2] = FeeSplitter_CallDistributionBias.setMinAmountZero3.selector; + selectors[3] = FeeSplitter_CallDistributionBias.setMinAmountNotZero.selector; + targetSelector(FuzzSelector({ addr: address(callDistributionBias), selectors: selectors })); + + targetContract(address(preconditions)); + selectors = new bytes4[](2); + selectors[0] = FeeSplitter_Preconditions.warp.selector; + selectors[1] = FeeSplitter_Preconditions.addCollectedFeeToVault.selector; + targetSelector(FuzzSelector({ addr: address(preconditions), selectors: selectors })); + } + + /// @notice Invariant: The fee splitter balance should always be 0 + /// @dev This invariant doesn't account for direct forced transfers (eg selfdestruct) + function invariant_noDust() external view { + assertEq(address(disburser.feeSplitter()).balance, 0); + } + + /// @notice Invariant: The l1withdrawer should always transfer its whole balance if it reaches the threshold + function invariant_l1withdrawerWithdrawn() external view { + if (disburser.l1withdrawerShouldHaveWithdrawn()) { + assertEq(address(l1Withdrawer).balance, 0); + } + } + + /// @notice Invariant: The total disbursed fees should always be equal to the sum of the vault balances before + /// disbursement + /// @dev This invariant can also be expressed as "disburseFees is only successful if all vaults can transfer the + /// fee/all or nothing (0 threshold is accepted)" as, otherwise, some funds would still be in the vaults, + /// invalidating the equality + function invariant_balanceConservation() external view { + assertEq( + disburser.ghost_grossRevenueDisbursed(), + address(l1Withdrawer).balance + Predeploys.L2_TO_L1_MESSAGE_PASSER.balance + + address(chainFeesRecipient).balance + ); + } + + /// @notice Invariants: these are revert invariants, disburseFees can only revert if either one of the vault + /// has a balance below it's minimum withdrawal amount (no other revert conditions are possible for the vault) + /// or if the disbursement interval has not been reached yet (this is making the assumption the recipient are + /// NOT reverting when receiving the fees). + /// @dev This invariant is also testing the "no partial disbursement", as the previous one. + function invariant_disburseReverts() external view { + if (disburser.txFailed()) { + DisburseFailureState memory _failureState = disburser.getFailureState(); + + uint256 _grossRevenue = _failureState.sequencerFeeVaultBalance + _failureState.baseFeeVaultBalance + + _failureState.l1FeeVaultBalance + _failureState.operatorFeeVaultBalance; + + // either one of the vaults is below the minimum withdrawal amount + bool _vaultBelowMinimum = ( + _failureState.sequencerFeeVaultBalance < _failureState.sequencerFeeVaultMinWithdrawalAmount + || _failureState.baseFeeVaultBalance < _failureState.baseFeeVaultMinWithdrawalAmount + || _failureState.l1FeeVaultBalance < _failureState.l1FeeVaultMinWithdrawalAmount + || _failureState.operatorFeeVaultBalance < _failureState.operatorFeeVaultMinWithdrawalAmount + ) + && keccak256(_failureState.reason) + == keccak256( + abi.encodeWithSignature( + "Error(string)", "FeeVault: withdrawal amount must be greater than minimum withdrawal amount" + ) + ); + + // not enough time since last disbursement + bool _tooEarly = _failureState.attemptTimestamp + < disburser.feeSplitter().lastDisbursementTime() + disburser.feeSplitter().feeDisbursementInterval() + && bytes4(_failureState.reason) == IFeeSplitter.FeeSplitter_DisbursementIntervalNotReached.selector; + + // no revenue at all + bool _noRevenue = + _grossRevenue == 0 && bytes4(_failureState.reason) == IFeeSplitter.FeeSplitter_NoFeesCollected.selector; + + // rounding down error in the shares calculator + bool _noSharesCalculator = (_grossRevenue * 250) < 10000 + && bytes4(_failureState.reason) == ISuperchainRevSharesCalculator.SharesCalculator_ZeroGrossShare.selector; + + assertTrue(_vaultBelowMinimum || _tooEarly || _noRevenue || _noSharesCalculator); + } + } + + /// @notice After invariant: log the call distribution bias + /// @dev This could be an assertion, but only works significant with big calldepth (or keeping a counter accross all + /// runs, which needs a file to write to) + function afterInvariant() external { + uint256 _totalCalls = callDistributionBias.amountZeroCalls() + callDistributionBias.amountNotZeroCalls(); + + if (_totalCalls > 0) { + emit log_named_uint( + "% of calls setting the min amount to zero in this run: ", + callDistributionBias.amountZeroCalls() * 100 / _totalCalls + ); + } + } +} diff --git a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol index 548c178050fd9..936ec8f000c51 100644 --- a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol +++ b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol @@ -28,17 +28,25 @@ abstract contract Predeploys_TestInit is CommonTest { return _addr == Predeploys.L1_MESSAGE_SENDER; } - /// @notice Returns true if the predeploy is initializable. - function _isInitializable(address _addr) internal pure returns (bool) { + /// @notice Returns true if the predeploy is initializable and uses OpenZeppelin v4 storage pattern. + /// These contracts have _initialized in the regular storage layout. + function _isInitializableV4(address _addr) internal pure returns (bool) { return _addr == Predeploys.L2_CROSS_DOMAIN_MESSENGER || _addr == Predeploys.L2_STANDARD_BRIDGE - || _addr == Predeploys.L2_ERC721_BRIDGE || _addr == Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY; + || _addr == Predeploys.L2_ERC721_BRIDGE || _addr == Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY + || _addr == Predeploys.FEE_SPLITTER; + } + + /// @notice Returns true if the predeploy is initializable and uses OpenZeppelin v5 namespaced storage (EIP-7201). + /// These contracts store _initialized in a namespaced slot, not in the regular storage layout. + function _isInitializableV5(address _addr) internal pure returns (bool) { + return _addr == Predeploys.SEQUENCER_FEE_WALLET || _addr == Predeploys.BASE_FEE_VAULT + || _addr == Predeploys.L1_FEE_VAULT || _addr == Predeploys.OPERATOR_FEE_VAULT; } /// @notice Returns true if the predeploy uses immutables. function _usesImmutables(address _addr) internal pure returns (bool) { - return _addr == Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == Predeploys.SEQUENCER_FEE_WALLET - || _addr == Predeploys.BASE_FEE_VAULT || _addr == Predeploys.L1_FEE_VAULT - || _addr == Predeploys.OPERATOR_FEE_VAULT || _addr == Predeploys.EAS || _addr == Predeploys.GOVERNANCE_TOKEN; + return _addr == Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == Predeploys.EAS + || _addr == Predeploys.GOVERNANCE_TOKEN; } /// @notice Internal test function for predeploys validation across different forks. @@ -98,10 +106,20 @@ abstract contract Predeploys_TestInit is CommonTest { assertEq(implAddr.code, supposedCode, "proxy implementation contract should match contract source"); } - if (_isInitializable(addr)) { + if (_isInitializableV4(addr)) { assertTrue(ForgeArtifacts.isInitialized({ _name: cname, _address: addr })); assertTrue(ForgeArtifacts.isInitialized({ _name: cname, _address: implAddr })); } + + if (_isInitializableV5(addr)) { + assertTrue( + ForgeArtifacts.isInitializedV5(addr), string.concat("V5 proxy not initialized: ", vm.toString(addr)) + ); + assertTrue( + ForgeArtifacts.isInitializedV5(implAddr), + string.concat("V5 implementation not initialized: ", vm.toString(implAddr)) + ); + } } } } diff --git a/packages/contracts-bedrock/test/mocks/FeeSplitterForTest.sol b/packages/contracts-bedrock/test/mocks/FeeSplitterForTest.sol new file mode 100644 index 0000000000000..be67f2ed3829f --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/FeeSplitterForTest.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Contracts +import { FeeSplitter } from "src/L2/FeeSplitter.sol"; + +/// @title FeeSplitterForTest +/// @notice Test contract for the FeeSplitter contract. +/// @dev Makes the setTransientDisbursingAddress function public for testing purposes. +contract FeeSplitterForTest is FeeSplitter { + function setTransientDisbursingAddress(address _allowedCaller) external { + _setTransientDisbursingAddress(_allowedCaller); + } +} diff --git a/packages/contracts-bedrock/test/mocks/LegacyFeeSplitter.sol b/packages/contracts-bedrock/test/mocks/LegacyFeeSplitter.sol new file mode 100644 index 0000000000000..571801edc2b07 --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/LegacyFeeSplitter.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Predeploys } from "src/libraries/Predeploys.sol"; + +interface ILegacyFeeVault { + function withdraw() external; +} + +/// @title LegacyFeeSplitter +/// @notice A simple contract meant to be used to withdraw fees from FeeVault(s) using the old interface. +/// @dev This contract is not used for production, but rather for testing purposes as it lacks safe guards and error +/// handling. +contract LegacyFeeSplitter { + receive() external payable { } + + function disburseFees() external { + _feeVaultWithdrawal(payable(Predeploys.SEQUENCER_FEE_WALLET)); + _feeVaultWithdrawal(payable(Predeploys.BASE_FEE_VAULT)); + _feeVaultWithdrawal(payable(Predeploys.L1_FEE_VAULT)); + _feeVaultWithdrawal(payable(Predeploys.OPERATOR_FEE_VAULT)); + } + + function _feeVaultWithdrawal(address payable _feeVault) internal { + bytes memory _calldata = abi.encodeCall(ILegacyFeeVault.withdraw, ()); + (bool success,) = _feeVault.call(_calldata); + require(success, "LegacyFeeSplitter: fee vault withdrawal failed"); + } +} diff --git a/packages/contracts-bedrock/test/mocks/MaliciousMockFeeVault.sol b/packages/contracts-bedrock/test/mocks/MaliciousMockFeeVault.sol new file mode 100644 index 0000000000000..92816438c4912 --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/MaliciousMockFeeVault.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Types } from "src/libraries/Types.sol"; + +/// @notice Mock fee vault that returns a different withdrawal amount than what it transfers +contract MaliciousMockFeeVault { + address public immutable RECIPIENT; + uint256 public immutable ACTUAL_TRANSFER_AMOUNT; + uint256 public immutable CLAIMED_WITHDRAWAL_AMOUNT; + + constructor(address payable _recipient, uint256 _actualTransferAmount, uint256 _claimedWithdrawalAmount) { + RECIPIENT = _recipient; + ACTUAL_TRANSFER_AMOUNT = _actualTransferAmount; + CLAIMED_WITHDRAWAL_AMOUNT = _claimedWithdrawalAmount; + } + + receive() external payable { } + + function withdrawalNetwork() external pure returns (Types.WithdrawalNetwork) { + return Types.WithdrawalNetwork.L2; + } + + function recipient() external view returns (address) { + return RECIPIENT; + } + + function withdraw() external returns (uint256) { + // Transfer the actual amount + (bool success,) = RECIPIENT.call{ value: ACTUAL_TRANSFER_AMOUNT }(""); + require(success, "MaliciousMockFeeVault: failed to send ETH"); + + // But lie about how much was transferred + return CLAIMED_WITHDRAWAL_AMOUNT; + } +} diff --git a/packages/contracts-bedrock/test/mocks/MockFeeVault.sol b/packages/contracts-bedrock/test/mocks/MockFeeVault.sol new file mode 100644 index 0000000000000..4ab57603c0bf5 --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/MockFeeVault.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Types } from "src/libraries/Types.sol"; + +/// @notice Simple mock FeeVault for testing that actually transfers ETH +contract MockFeeVault { + uint256 public immutable MIN_WITHDRAWAL_AMOUNT; + address public immutable RECIPIENT; + Types.WithdrawalNetwork public immutable WITHDRAWAL_NETWORK; + + event Withdrawal(uint256 value, address to, address from); + event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); + + constructor(address payable _recipient, uint256 _minWithdrawalAmount, Types.WithdrawalNetwork _withdrawalNetwork) { + RECIPIENT = _recipient; + MIN_WITHDRAWAL_AMOUNT = _minWithdrawalAmount; + WITHDRAWAL_NETWORK = _withdrawalNetwork; + } + + receive() external payable { } + + function withdrawalNetwork() external view returns (Types.WithdrawalNetwork) { + return WITHDRAWAL_NETWORK; + } + + function minWithdrawalAmount() external view returns (uint256) { + return MIN_WITHDRAWAL_AMOUNT; + } + + function recipient() external view returns (address) { + return RECIPIENT; + } + + function withdraw() external returns (uint256) { + require( + address(this).balance >= MIN_WITHDRAWAL_AMOUNT, + "FeeVault: withdrawal amount must be greater than minimum withdrawal amount" + ); + + uint256 value = address(this).balance; + + emit Withdrawal(value, RECIPIENT, msg.sender); + emit Withdrawal(value, RECIPIENT, msg.sender, WITHDRAWAL_NETWORK); + + if (WITHDRAWAL_NETWORK == Types.WithdrawalNetwork.L2) { + (bool success,) = RECIPIENT.call{ value: value }(""); + require(success, "FeeVault: failed to send ETH to L2 fee recipient"); + } + + return value; + } +} + +/// @title MockLegacyFeeVault +/// @notice Mock fee vault contract that simulates legacy vaults without WITHDRAWAL_NETWORK function +contract MockLegacyFeeVault { + address public constant RECIPIENT = address(0x1234567890123456789012345678901234567890); + uint256 public constant MIN_WITHDRAWAL_AMOUNT = 0.01 ether; + + // No WITHDRAWAL_NETWORK() implementation + + receive() external payable { } +} diff --git a/packages/contracts-bedrock/test/mocks/ReentrantMockFeeVault.sol b/packages/contracts-bedrock/test/mocks/ReentrantMockFeeVault.sol new file mode 100644 index 0000000000000..dc2b4916c902b --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/ReentrantMockFeeVault.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Types } from "src/libraries/Types.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; + +/// @notice Mock fee vault that attempts to trigger withdrawal from a different vault during its own withdrawal. +/// This demonstrates the attack vector where a malicious vault tries to exploit the disbursing context +/// to allow unauthorized withdrawals from other vaults. +contract ReentrantMockFeeVault { + address public immutable RECIPIENT; + uint256 public immutable WITHDRAWAL_AMOUNT; + address payable public immutable TARGET_VAULT; + + constructor(address payable _recipient, uint256 _withdrawalAmount, address payable _targetVault) { + RECIPIENT = _recipient; + WITHDRAWAL_AMOUNT = _withdrawalAmount; + TARGET_VAULT = _targetVault; + } + + receive() external payable { } + + function withdrawalNetwork() external pure returns (Types.WithdrawalNetwork) { + return Types.WithdrawalNetwork.L2; + } + + function recipient() external view returns (address) { + return RECIPIENT; + } + + function withdraw() external returns (uint256) { + // Attempt to trigger a withdrawal from a different vault + // This should fail with the new stricter check and propagate the revert + if (TARGET_VAULT != address(0)) { + IFeeVault(TARGET_VAULT).withdraw(); + } + + return WITHDRAWAL_AMOUNT; + } +} diff --git a/packages/contracts-bedrock/test/mocks/RevertingRecipient.sol b/packages/contracts-bedrock/test/mocks/RevertingRecipient.sol new file mode 100644 index 0000000000000..b3d4c01178f97 --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/RevertingRecipient.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +/// @notice Helper recipient that always reverts on receiving ETH +contract RevertingRecipient { + receive() external payable { + revert("RevertingRecipient: cannot accept ETH"); + } +} diff --git a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol index ffd6a60d4d126..5c043befb6621 100644 --- a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol @@ -6,15 +6,20 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { L2Genesis } from "scripts/L2Genesis.s.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { LATEST_FORK } from "scripts/libraries/Config.sol"; - +import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; import { ISequencerFeeVault } from "interfaces/L2/ISequencerFeeVault.sol"; import { IBaseFeeVault } from "interfaces/L2/IBaseFeeVault.sol"; import { IL1FeeVault } from "interfaces/L2/IL1FeeVault.sol"; +import { IOperatorFeeVault } from "interfaces/L2/IOperatorFeeVault.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IOptimismMintableERC721Factory } from "interfaces/L2/IOptimismMintableERC721Factory.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IGasPriceOracle } from "interfaces/L2/IGasPriceOracle.sol"; +import { IFeeSplitter } from "interfaces/L2/IFeeSplitter.sol"; +import { IL1Withdrawer } from "interfaces/L2/IL1Withdrawer.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; +import { Types } from "src/libraries/Types.sol"; /// @title L2Genesis_TestInit /// @notice Reusable test initialization for `L2Genesis` tests. @@ -23,7 +28,7 @@ abstract contract L2Genesis_TestInit is Test { L2Genesis internal genesis; - function setUp() public { + function setUp() public virtual { genesis = new L2Genesis(); } @@ -61,22 +66,74 @@ abstract contract L2Genesis_TestInit is Test { assertGt(Predeploys.GOVERNANCE_TOKEN.code.length, 0); } - function testVaults() internal view { + function testVaultsWithoutRevenueShare() internal view { IBaseFeeVault baseFeeVault = IBaseFeeVault(payable(Predeploys.BASE_FEE_VAULT)); IL1FeeVault l1FeeVault = IL1FeeVault(payable(Predeploys.L1_FEE_VAULT)); ISequencerFeeVault sequencerFeeVault = ISequencerFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)); + IOperatorFeeVault operatorFeeVault = IOperatorFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)); + assertEq(baseFeeVault.RECIPIENT(), input.baseFeeVaultRecipient); assertEq(baseFeeVault.recipient(), input.baseFeeVaultRecipient); assertEq(baseFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.baseFeeVaultMinimumWithdrawalAmount); + assertEq(baseFeeVault.minWithdrawalAmount(), input.baseFeeVaultMinimumWithdrawalAmount); assertEq(uint8(baseFeeVault.WITHDRAWAL_NETWORK()), uint8(input.baseFeeVaultWithdrawalNetwork)); + assertEq(uint8(baseFeeVault.withdrawalNetwork()), uint8(input.baseFeeVaultWithdrawalNetwork)); + assertEq(l1FeeVault.RECIPIENT(), input.l1FeeVaultRecipient); assertEq(l1FeeVault.recipient(), input.l1FeeVaultRecipient); assertEq(l1FeeVault.MIN_WITHDRAWAL_AMOUNT(), input.l1FeeVaultMinimumWithdrawalAmount); + assertEq(l1FeeVault.minWithdrawalAmount(), input.l1FeeVaultMinimumWithdrawalAmount); assertEq(uint8(l1FeeVault.WITHDRAWAL_NETWORK()), uint8(input.l1FeeVaultWithdrawalNetwork)); + assertEq(uint8(l1FeeVault.withdrawalNetwork()), uint8(input.l1FeeVaultWithdrawalNetwork)); + assertEq(sequencerFeeVault.RECIPIENT(), input.sequencerFeeVaultRecipient); assertEq(sequencerFeeVault.recipient(), input.sequencerFeeVaultRecipient); assertEq(sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.sequencerFeeVaultMinimumWithdrawalAmount); + assertEq(sequencerFeeVault.minWithdrawalAmount(), input.sequencerFeeVaultMinimumWithdrawalAmount); assertEq(uint8(sequencerFeeVault.WITHDRAWAL_NETWORK()), uint8(input.sequencerFeeVaultWithdrawalNetwork)); + assertEq(uint8(sequencerFeeVault.withdrawalNetwork()), uint8(input.sequencerFeeVaultWithdrawalNetwork)); + + assertEq(operatorFeeVault.RECIPIENT(), input.operatorFeeVaultRecipient); + assertEq(operatorFeeVault.recipient(), input.operatorFeeVaultRecipient); + assertEq(operatorFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.operatorFeeVaultMinimumWithdrawalAmount); + assertEq(operatorFeeVault.minWithdrawalAmount(), input.operatorFeeVaultMinimumWithdrawalAmount); + assertEq(uint8(operatorFeeVault.WITHDRAWAL_NETWORK()), uint8(input.operatorFeeVaultWithdrawalNetwork)); + assertEq(uint8(operatorFeeVault.withdrawalNetwork()), uint8(input.operatorFeeVaultWithdrawalNetwork)); + } + + function testVaultsWithRevenueShare() internal view { + IFeeVault baseFeeVault = IFeeVault(payable(Predeploys.BASE_FEE_VAULT)); + IFeeVault l1FeeVault = IFeeVault(payable(Predeploys.L1_FEE_VAULT)); + IFeeVault sequencerFeeVault = IFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)); + IFeeVault operatorFeeVault = IFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)); + + assertEq(baseFeeVault.recipient(), Predeploys.FEE_SPLITTER); + assertEq(baseFeeVault.RECIPIENT(), Predeploys.FEE_SPLITTER); + assertEq(baseFeeVault.MIN_WITHDRAWAL_AMOUNT(), 0); + assertEq(baseFeeVault.minWithdrawalAmount(), 0); + assertEq(uint8(baseFeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L2)); + assertEq(uint8(baseFeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L2)); + + assertEq(l1FeeVault.RECIPIENT(), Predeploys.FEE_SPLITTER); + assertEq(l1FeeVault.recipient(), Predeploys.FEE_SPLITTER); + assertEq(l1FeeVault.MIN_WITHDRAWAL_AMOUNT(), 0); + assertEq(l1FeeVault.minWithdrawalAmount(), 0); + assertEq(uint8(l1FeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L2)); + assertEq(uint8(l1FeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L2)); + + assertEq(sequencerFeeVault.RECIPIENT(), Predeploys.FEE_SPLITTER); + assertEq(sequencerFeeVault.recipient(), Predeploys.FEE_SPLITTER); + assertEq(sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT(), 0); + assertEq(sequencerFeeVault.minWithdrawalAmount(), 0); + assertEq(uint8(sequencerFeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L2)); + assertEq(uint8(sequencerFeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L2)); + + assertEq(operatorFeeVault.RECIPIENT(), Predeploys.FEE_SPLITTER); + assertEq(operatorFeeVault.recipient(), Predeploys.FEE_SPLITTER); + assertEq(operatorFeeVault.MIN_WITHDRAWAL_AMOUNT(), 0); + assertEq(operatorFeeVault.minWithdrawalAmount(), 0); + assertEq(uint8(operatorFeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L2)); + assertEq(uint8(operatorFeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L2)); } function testGovernance() internal view { @@ -102,12 +159,33 @@ abstract contract L2Genesis_TestInit is Test { assertEq(gasPriceOracle.isFjord(), true); assertEq(gasPriceOracle.isIsthmus(), true); } + + function testFeeSplitter() internal view { + // Only test if revenue share is enabled + if (!input.useRevenueShare) return; + + // Check that the shares calculator and fee disbursement interval are set on the fee splitter + IFeeSplitter feeSplitter = IFeeSplitter(payable(Predeploys.FEE_SPLITTER)); + assertEq(feeSplitter.feeDisbursementInterval(), 1 days); + + ISuperchainRevSharesCalculator superchainRevSharesCalculator = + ISuperchainRevSharesCalculator(address(feeSplitter.sharesCalculator())); + // Check that the superchain rev shares calculator is properly set + assertEq(superchainRevSharesCalculator.remainderRecipient(), input.chainFeesRecipient); + + // Check the L1Withdrawer is properly set + IL1Withdrawer l1Withdrawer = IL1Withdrawer(superchainRevSharesCalculator.shareRecipient()); + assertEq(l1Withdrawer.minWithdrawalAmount(), 10 ether); + assertEq(l1Withdrawer.recipient(), input.l1FeesDepositor); + assertEq(l1Withdrawer.withdrawalGasLimit(), 1_000_000); + } } /// @title L2Genesis_Run_Test /// @notice Tests the `run` function of the `L2Genesis` contract. contract L2Genesis_Run_Test is L2Genesis_TestInit { - function test_run_succeeds() external { + function setUp() public override { + super.setUp(); input = L2Genesis.Input({ l1ChainID: 1, l2ChainID: 2, @@ -124,19 +202,131 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { l1FeeVaultRecipient: address(0x0000000000000000000000000000000000000007), l1FeeVaultMinimumWithdrawalAmount: 1, l1FeeVaultWithdrawalNetwork: 1, - governanceTokenOwner: address(0x0000000000000000000000000000000000000008), + operatorFeeVaultRecipient: address(0x0000000000000000000000000000000000000008), + operatorFeeVaultMinimumWithdrawalAmount: 1, + operatorFeeVaultWithdrawalNetwork: 1, + governanceTokenOwner: address(0x0000000000000000000000000000000000000009), fork: uint256(LATEST_FORK), deployCrossL2Inbox: true, enableGovernance: true, - fundDevAccounts: true + fundDevAccounts: true, + useRevenueShare: true, + chainFeesRecipient: address(0x000000000000000000000000000000000000000b), + l1FeesDepositor: address(0x000000000000000000000000000000000000000C) }); + } + + function test_run_succeeds() external { genesis.run(input); testProxyAdmin(); testPredeploys(); - testVaults(); + testVaultsWithRevenueShare(); testGovernance(); testFactories(); testForks(); + testFeeSplitter(); + } + + function test_run_withoutRevenueShare_succeeds() external { + input.useRevenueShare = false; + genesis.run(input); + + testProxyAdmin(); + testPredeploys(); + testVaultsWithoutRevenueShare(); + testGovernance(); + testFactories(); + testForks(); + + // Test that FeeSplitter is initialized with address(0) when revenue share is disabled + IFeeSplitter feeSplitter = IFeeSplitter(payable(Predeploys.FEE_SPLITTER)); + assertEq(address(feeSplitter.sharesCalculator()), address(0), "sharesCalculator should be zero address"); + assertEq(feeSplitter.feeDisbursementInterval(), 1 days, "feeDisbursementInterval should be 1 day"); + } + + function test_runWithRevenueShare_zeroChainFeesRecipient_reverts() external { + input.useRevenueShare = true; + input.chainFeesRecipient = address(0); + + vm.expectRevert(L2Genesis.L2Genesis_ChainFeesRecipientCannotBeZero.selector); + genesis.run(input); + } + + function test_runWithRevenueShare_zeroL1FeesDepositor_reverts() external { + input.useRevenueShare = true; + input.l1FeesDepositor = address(0); + + vm.expectRevert(L2Genesis.L2Genesis_L1FeesDepositorCannotBeZero.selector); + genesis.run(input); + } + + function test_runWithRevenueShare_misconfiguredVaults_reverts() external { + // Misconfigured base fee vault + vm.mockCall(Predeploys.BASE_FEE_VAULT, abi.encodeCall(IFeeVault.recipient, ()), abi.encode(address(0))); + + vm.expectRevert(L2Genesis.L2Genesis_MisconfiguredBaseFeeVault.selector); + genesis.run(input); + + vm.clearMockedCalls(); + vm.mockCall( + Predeploys.BASE_FEE_VAULT, + abi.encodeCall(IFeeVault.withdrawalNetwork, ()), + abi.encode(Types.WithdrawalNetwork.L1) + ); + + vm.expectRevert(L2Genesis.L2Genesis_MisconfiguredBaseFeeVault.selector); + genesis.run(input); + + // Misconfigured l1 fee vault + vm.clearMockedCalls(); + vm.mockCall(Predeploys.L1_FEE_VAULT, abi.encodeCall(IFeeVault.recipient, ()), abi.encode(address(0))); + + vm.expectRevert(L2Genesis.L2Genesis_MisconfiguredL1FeeVault.selector); + genesis.run(input); + + vm.clearMockedCalls(); + vm.mockCall( + Predeploys.L1_FEE_VAULT, + abi.encodeCall(IFeeVault.withdrawalNetwork, ()), + abi.encode(Types.WithdrawalNetwork.L1) + ); + + vm.expectRevert(L2Genesis.L2Genesis_MisconfiguredL1FeeVault.selector); + genesis.run(input); + + // Misconfigured sequencer fee vault + vm.clearMockedCalls(); + vm.mockCall(Predeploys.SEQUENCER_FEE_WALLET, abi.encodeCall(IFeeVault.recipient, ()), abi.encode(address(0))); + + vm.expectRevert(L2Genesis.L2Genesis_MisconfiguredSequencerFeeVault.selector); + genesis.run(input); + + vm.clearMockedCalls(); + vm.mockCall( + Predeploys.SEQUENCER_FEE_WALLET, + abi.encodeCall(IFeeVault.withdrawalNetwork, ()), + abi.encode(Types.WithdrawalNetwork.L1) + ); + + vm.expectRevert(L2Genesis.L2Genesis_MisconfiguredSequencerFeeVault.selector); + genesis.run(input); + + // Misconfigured operator fee vault + vm.clearMockedCalls(); + vm.mockCall(Predeploys.OPERATOR_FEE_VAULT, abi.encodeCall(IFeeVault.recipient, ()), abi.encode(address(0))); + + vm.expectRevert(L2Genesis.L2Genesis_MisconfiguredOperatorFeeVault.selector); + genesis.run(input); + + vm.clearMockedCalls(); + vm.mockCall( + Predeploys.OPERATOR_FEE_VAULT, + abi.encodeCall(IFeeVault.withdrawalNetwork, ()), + abi.encode(Types.WithdrawalNetwork.L1) + ); + + vm.expectRevert(L2Genesis.L2Genesis_MisconfiguredOperatorFeeVault.selector); + genesis.run(input); } } diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index 71f55ec386702..8b7dfefcd19e7 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -34,6 +34,7 @@ abstract contract CommonTest is Test, Setup, Events { bool useAltDAOverride; bool useInteropOverride; + bool useRevenueShareOverride; /// @dev This value is only used in forked tests. During forked tests, the default is to perform the upgrade before /// running the tests. @@ -41,6 +42,10 @@ abstract contract CommonTest is Test, Setup, Events { /// itself, rather than simply ensuring that the tests pass after the upgrade. bool useUpgradedFork = true; + // Needed for testing purposes to check the contracts were properly deployed and setup. + address chainFeesRecipient = makeAddr("chainFeesRecipient"); + address l1FeesDepositor = makeAddr("l1FeesDepositor"); + ERC20 L1Token; ERC20 BadL1Token; IOptimismMintableERC20Full L2Token; @@ -71,6 +76,11 @@ abstract contract CommonTest is Test, Setup, Events { if (useInteropOverride) { deploy.cfg().setUseInterop(true); } + if (useRevenueShareOverride) { + deploy.cfg().setUseRevenueShare(true); + deploy.cfg().setChainFeesRecipient(chainFeesRecipient); + deploy.cfg().setL1FeesDepositor(l1FeesDepositor); + } if (useUpgradedFork) { deploy.cfg().setUseUpgradedFork(true); } @@ -200,6 +210,12 @@ abstract contract CommonTest is Test, Setup, Events { useInteropOverride = true; } + /// @dev Enables revenue sharing mode for testing + function enableRevenueShare() public { + _checkNotDeployed("revenue share"); + useRevenueShareOverride = true; + } + /// @dev Disables upgrade mode for testing. By default the fork testing env will be upgraded to the latest /// implementation. This can be used to disable the upgrade which, is useful for tests targeting the upgrade /// process itself. diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 92fb55b6485fd..f50e0c5ff1161 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -62,6 +62,9 @@ import { ISuperchainTokenBridge } from "interfaces/L2/ISuperchainTokenBridge.sol import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ICrossL2Inbox } from "interfaces/L2/ICrossL2Inbox.sol"; +import { IFeeSplitter } from "interfaces/L2/IFeeSplitter.sol"; +import { IL1Withdrawer } from "interfaces/L2/IL1Withdrawer.sol"; +import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; /// @title Setup /// @dev This contact is responsible for setting up the contracts in state. It currently @@ -146,6 +149,9 @@ abstract contract Setup is FeatureFlags { ISuperchainTokenBridge superchainTokenBridge = ISuperchainTokenBridge(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); IOptimismSuperchainERC20Factory l2OptimismSuperchainERC20Factory = IOptimismSuperchainERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); + IFeeSplitter feeSplitter = IFeeSplitter(payable(Predeploys.FEE_SPLITTER)); + IL1Withdrawer l1Withdrawer; + ISuperchainRevSharesCalculator superchainRevSharesCalculator; /// @notice Indicates whether a test is running against a forked production network. function isForkTest() public view returns (bool) { @@ -322,14 +328,27 @@ abstract contract Setup is FeatureFlags { l1FeeVaultRecipient: deploy.cfg().l1FeeVaultRecipient(), l1FeeVaultMinimumWithdrawalAmount: deploy.cfg().l1FeeVaultMinimumWithdrawalAmount(), l1FeeVaultWithdrawalNetwork: deploy.cfg().l1FeeVaultWithdrawalNetwork(), + operatorFeeVaultRecipient: deploy.cfg().operatorFeeVaultRecipient(), + operatorFeeVaultMinimumWithdrawalAmount: deploy.cfg().operatorFeeVaultMinimumWithdrawalAmount(), + operatorFeeVaultWithdrawalNetwork: deploy.cfg().operatorFeeVaultWithdrawalNetwork(), governanceTokenOwner: deploy.cfg().governanceTokenOwner(), fork: uint256(l2Fork), deployCrossL2Inbox: deploy.cfg().useInterop(), enableGovernance: deploy.cfg().enableGovernance(), - fundDevAccounts: deploy.cfg().fundDevAccounts() + fundDevAccounts: deploy.cfg().fundDevAccounts(), + useRevenueShare: deploy.cfg().useRevenueShare(), + chainFeesRecipient: deploy.cfg().chainFeesRecipient(), + l1FeesDepositor: deploy.cfg().l1FeesDepositor() }) ); + if (deploy.cfg().useRevenueShare()) { + superchainRevSharesCalculator = ISuperchainRevSharesCalculator( + address(IFeeSplitter(payable(Predeploys.FEE_SPLITTER)).sharesCalculator()) + ); + l1Withdrawer = IL1Withdrawer(superchainRevSharesCalculator.shareRecipient()); + } + // Set the governance token's owner to be the final system owner address finalSystemOwner = deploy.cfg().finalSystemOwner(); vm.startPrank(governanceToken.owner()); @@ -358,6 +377,7 @@ abstract contract Setup is FeatureFlags { labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); labelPredeploy(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + labelPredeploy(Predeploys.FEE_SPLITTER); // L2 Preinstalls labelPreinstall(Preinstalls.MultiCall3); diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 5318d7d15fdca..b51f6587c2ad4 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -378,7 +378,7 @@ contract Initializer_Test is CommonTest { function test_cannotReinitialize_succeeds() public { // Collect exclusions. uint256 j; - string[] memory excludes = new string[](11); + string[] memory excludes = new string[](12); // Contract is currently not being deployed as part of the standard deployment script. excludes[j++] = "src/L2/OptimismSuperchainERC20.sol"; // Periphery contracts don't get deployed as part of the standard deployment script. @@ -400,6 +400,7 @@ contract Initializer_Test is CommonTest { excludes[j++] = "src/L1/OptimismPortalInterop.sol"; // L2 contract initialization is tested in Predeploys.t.sol excludes[j++] = "src/L2/*"; + excludes[j++] = "src/L1/FeesDepositor.sol"; // Get all contract names in the src directory, minus the excluded contracts. string[] memory contractNames = ForgeArtifacts.getContractNames("src/*", excludes); diff --git a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol index 51c2fce266786..b12bade2a9fc9 100644 --- a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol +++ b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol @@ -5,6 +5,9 @@ import { Test } from "forge-std/Test.sol"; import { IOptimismSuperchainERC20 } from "interfaces/L2/IOptimismSuperchainERC20.sol"; import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; +import { Types } from "src/libraries/Types.sol"; + /// @title InitializerOZv5_Test /// @dev Ensures that the `initialize()` function on contracts cannot be called more than /// once. Tests the contracts inheriting from `Initializable` from OpenZeppelin Contracts v5. @@ -41,6 +44,58 @@ contract InitializerOZv5_Test is Test { initCalldata: abi.encodeCall(IOptimismSuperchainERC20.initialize, (address(0), "", "", 18)) }) ); + + // BaseFeeVault + contracts.push( + InitializeableContract({ + target: address( + DeployUtils.create1({ + _name: "BaseFeeVault", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IFeeVault.__constructor__, ())) + }) + ), + initCalldata: abi.encodeCall(IFeeVault.initialize, (address(0), 0, Types.WithdrawalNetwork.L1)) + }) + ); + + // OperatorFeeVault + contracts.push( + InitializeableContract({ + target: address( + DeployUtils.create1({ + _name: "OperatorFeeVault", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IFeeVault.__constructor__, ())) + }) + ), + initCalldata: abi.encodeCall(IFeeVault.initialize, (address(0), 0, Types.WithdrawalNetwork.L1)) + }) + ); + + // SequencerFeeVault + contracts.push( + InitializeableContract({ + target: address( + DeployUtils.create1({ + _name: "SequencerFeeVault", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IFeeVault.__constructor__, ())) + }) + ), + initCalldata: abi.encodeCall(IFeeVault.initialize, (address(0), 0, Types.WithdrawalNetwork.L1)) + }) + ); + + // L1FeeVault + contracts.push( + InitializeableContract({ + target: address( + DeployUtils.create1({ + _name: "L1FeeVault", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IFeeVault.__constructor__, ())) + }) + ), + initCalldata: abi.encodeCall(IFeeVault.initialize, (address(0), 0, Types.WithdrawalNetwork.L1)) + }) + ); } /// @notice Tests that: