diff --git a/op-bindings/predeploys/addresses.go b/op-bindings/predeploys/addresses.go index 6d1b75e3bf4c..7f58883f2179 100644 --- a/op-bindings/predeploys/addresses.go +++ b/op-bindings/predeploys/addresses.go @@ -6,6 +6,7 @@ import "github.com/ethereum/go-ethereum/common" // This needs to be kept in sync with @eth-optimism/contracts-ts/wagmi.config.ts which also specifies this // To improve robustness and maintainability contracts-bedrock should export all addresses const ( + RevenueSharer = "0x4200000000000000000000000000000000000024" L2ToL1MessagePasser = "0x4200000000000000000000000000000000000016" DeployerWhitelist = "0x4200000000000000000000000000000000000002" WETH9 = "0x4200000000000000000000000000000000000006" @@ -39,6 +40,7 @@ const ( ) var ( + RevenueSharerAddr = common.HexToAddress(RevenueSharer) L2ToL1MessagePasserAddr = common.HexToAddress(L2ToL1MessagePasser) DeployerWhitelistAddr = common.HexToAddress(DeployerWhitelist) WETH9Addr = common.HexToAddress(WETH9) @@ -75,6 +77,7 @@ var ( ) func init() { + Predeploys["RevenueSharer"] = &Predeploy{Address: RevenueSharerAddr} Predeploys["L2ToL1MessagePasser"] = &Predeploy{Address: L2ToL1MessagePasserAddr} Predeploys["DeployerWhitelist"] = &Predeploy{Address: DeployerWhitelistAddr} Predeploys["WETH9"] = &Predeploy{Address: WETH9Addr, ProxyDisabled: true} diff --git a/packages/contracts-bedrock/deploy-config/hardhat.json b/packages/contracts-bedrock/deploy-config/hardhat.json index cc5571117e58..cb57f85799b7 100644 --- a/packages/contracts-bedrock/deploy-config/hardhat.json +++ b/packages/contracts-bedrock/deploy-config/hardhat.json @@ -27,12 +27,14 @@ "baseFeeVaultRecipient": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", "l1FeeVaultRecipient": "0x71bE63f3384f5fb98995898A86B02Fb2426c5788", "sequencerFeeVaultRecipient": "0xfabb0ac9d68b0b445fb7357272ff202c5651694a", - "baseFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", - "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", - "sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", - "baseFeeVaultWithdrawalNetwork": 0, - "l1FeeVaultWithdrawalNetwork": 0, - "sequencerFeeVaultWithdrawalNetwork": 0, + "revenueShareRecipient": "0xa3d596EAfaB6B13Ab18D40FaE1A962700C84ADEa", + "revenueShareRemainderRecipient": "0xa3d596EAfaB6B13Ab18D40FaE1A962700C84ADEa", + "baseFeeVaultMinimumWithdrawalAmount": "0x0", + "l1FeeVaultMinimumWithdrawalAmount": "0x0", + "sequencerFeeVaultMinimumWithdrawalAmount": "0x0", + "baseFeeVaultWithdrawalNetwork": 1, + "l1FeeVaultWithdrawalNetwork": 1, + "sequencerFeeVaultWithdrawalNetwork": 1, "enableGovernance": true, "governanceTokenName": "Optimism", "governanceTokenSymbol": "OP", @@ -65,4 +67,4 @@ "daResolveWindow": 100, "daBondSize": 1000, "daResolverRefundPercentage": 50 -} +} \ No newline at end of file diff --git a/packages/contracts-bedrock/scripts/Artifacts.s.sol b/packages/contracts-bedrock/scripts/Artifacts.s.sol index 253989b25ebf..2a59e878d342 100644 --- a/packages/contracts-bedrock/scripts/Artifacts.s.sol +++ b/packages/contracts-bedrock/scripts/Artifacts.s.sol @@ -107,7 +107,9 @@ abstract contract Artifacts { } bytes32 digest = keccak256(bytes(_name)); - if (digest == keccak256(bytes("L2CrossDomainMessenger"))) { + if (digest == keccak256(bytes("RevenueSharer"))) { + return payable(Predeploys.REVENUE_SHARER); + } else if (digest == keccak256(bytes("L2CrossDomainMessenger"))) { return payable(Predeploys.L2_CROSS_DOMAIN_MESSENGER); } else if (digest == keccak256(bytes("L2ToL1MessagePasser"))) { return payable(Predeploys.L2_TO_L1_MESSAGE_PASSER); diff --git a/packages/contracts-bedrock/scripts/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/DeployConfig.s.sol index 4321373c101b..fdca7c2252f9 100644 --- a/packages/contracts-bedrock/scripts/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/DeployConfig.s.sol @@ -42,6 +42,8 @@ contract DeployConfig is Script { address public sequencerFeeVaultRecipient; uint256 public sequencerFeeVaultMinimumWithdrawalAmount; uint256 public sequencerFeeVaultWithdrawalNetwork; + address payable public revenueShareRecipient; + address payable public revenueShareRemainderRecipient; string public governanceTokenName; string public governanceTokenSymbol; address public governanceTokenOwner; @@ -110,6 +112,8 @@ contract DeployConfig is Script { sequencerFeeVaultRecipient = stdJson.readAddress(_json, "$.sequencerFeeVaultRecipient"); sequencerFeeVaultMinimumWithdrawalAmount = stdJson.readUint(_json, "$.sequencerFeeVaultMinimumWithdrawalAmount"); sequencerFeeVaultWithdrawalNetwork = stdJson.readUint(_json, "$.sequencerFeeVaultWithdrawalNetwork"); + revenueShareRecipient = payable(stdJson.readAddress(_json, "$.revenueShareRecipient")); + revenueShareRemainderRecipient = payable(stdJson.readAddress(_json, "$.revenueShareRemainderRecipient")); governanceTokenName = stdJson.readString(_json, "$.governanceTokenName"); governanceTokenSymbol = stdJson.readString(_json, "$.governanceTokenSymbol"); governanceTokenOwner = stdJson.readAddress(_json, "$.governanceTokenOwner"); diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 387520f907e8..03104e11dd26 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -16,6 +16,7 @@ import { GasPriceOracle } from "src/L2/GasPriceOracle.sol"; import { L2StandardBridge } from "src/L2/L2StandardBridge.sol"; import { L2ERC721Bridge } from "src/L2/L2ERC721Bridge.sol"; import { SequencerFeeVault } from "src/L2/SequencerFeeVault.sol"; +import { RevenueSharer } from "src/L2/RevenueSharer.sol"; import { OptimismMintableERC20Factory } from "src/universal/OptimismMintableERC20Factory.sol"; import { OptimismMintableERC721Factory } from "src/universal/OptimismMintableERC721Factory.sol"; import { BaseFeeVault } from "src/L2/BaseFeeVault.sol"; @@ -214,9 +215,17 @@ contract L2Genesis is Deployer { // 1B,1C,1D,1E,1F: not used. setSchemaRegistry(); // 20 setEAS(); // 21 + setRevenueSharer(); // 24 setGovernanceToken(); // 42: OP (not behind a proxy) } + function setRevenueSharer() public { + RevenueSharer(Predeploys.REVENUE_SHARER).initialize({ + _beneficiary: cfg.revenueShareRecipient(), + _l1Wallet: cfg.revenueShareRemainderRecipient() + }); + } + function setProxyAdmin() public { // Note the ProxyAdmin implementation itself is behind a proxy that owns itself. address impl = _setImplementationCode(Predeploys.PROXY_ADMIN); diff --git a/packages/contracts-bedrock/src/L2/RevenueSharer.sol b/packages/contracts-bedrock/src/L2/RevenueSharer.sol new file mode 100644 index 000000000000..db63cc3769f3 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/RevenueSharer.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +import { L2StandardBridge } from "src/L2/L2StandardBridge.sol"; +import { FeeVault } from "src/universal/FeeVault.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { SafeCall } from "src/libraries/SafeCall.sol"; + +error OnlyFeeVaults(); +error ZeroAddress(string); +error UnexpectedFeeVaultWithdrawalNetwork(); +error UnexpectedFeeVaultRecipient(); +error FailedToShare(); + +/// @custom:proxied +/// @custom:predeploy 0x4200000000000000000000000000000000000024 +/// @title RevenueSharer +/// @dev Withdraws funds from system FeeVault contracts, +/// pays a share of revenue to a designated Beneficiary +/// and sends the remainder to a configurable adddress on L1. +contract RevenueSharer is Initializable { + /*////////////////////////////////////////////////////////////// + Constants + //////////////////////////////////////////////////////////////*/ + /** + * @dev The basis point scale which revenue share splits are denominated in. + */ + uint32 public constant BASIS_POINT_SCALE = 10_000; + /** + * @dev The minimum gas limit for the FeeDisburser withdrawal transaction to L1. + */ + uint32 public constant WITHDRAWAL_MIN_GAS = 35_000; + /** + * @dev The percentage coeffieicnt of revenue denominated in basis points that is used in + * Optimism revenue share calculation. + */ + uint256 public constant REVENUE_COEFFICIENT_BASIS_POINTS = 1_500; + /** + * @dev The percentage coefficient of profit denominated in bass points that is used in + * Optimism revenue share calculation. + */ + uint256 public constant PROFIT_COEFFICIENT_BASIS_POINTS = 250; + + /*////////////////////////////////////////////////////////////// + Immutables + //////////////////////////////////////////////////////////////*/ + /** + * @dev The address of the Optimism wallet that will receive Optimism's revenue share. + */ + address payable public BENEFICIARY; + /** + * @dev The address of the L1 wallet that will receive the OP chain runner's share of fees. + */ + address public L1_WALLET; + + /*////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + /** + * @dev Emitted when fees are disbursed. + * @param _share The amount of fees shared to the Beneficiary. + * @param _total The total funds distributed. + */ + event FeesDisbursed(uint256 _share, uint256 _total); + /** + * @dev Emitted when fees are received from FeeVaults. + * @param _sender The FeeVault that sent the fees. + * @param _amount The amount of fees received. + */ + event FeesReceived(address indexed _sender, uint256 _amount); + + /*////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + /** + * @dev Constructor for the FeeDisburser contract which validates and sets immutable variables. + * @param _beneficiary The address which receives the revenue share. + * @param _l1Wallet The L1 address which receives the remainder of the revenue. + */ + constructor(address payable _beneficiary, address payable _l1Wallet) { + initialize(_beneficiary, _l1Wallet); + } + + function initialize(address payable _beneficiary, address payable _l1Wallet) public initializer { + if (_beneficiary == address(0)) revert ZeroAddress("_beneficiary"); + if (_l1Wallet == address(0)) revert ZeroAddress("_l1Wallet"); + BENEFICIARY = _beneficiary; + L1_WALLET = _l1Wallet; + } + + /*////////////////////////////////////////////////////////////// + External Functions + //////////////////////////////////////////////////////////////*/ + /** + * @dev Withdraws funds from FeeVaults, sends Optimism their revenue share, and withdraws remaining funds to L1. + */ + function execute() external virtual { + // Pull in revenue + uint256 d = feeVaultWithdrawal(Predeploys.L1_FEE_VAULT); + uint256 b = feeVaultWithdrawal(Predeploys.BASE_FEE_VAULT); + uint256 q = feeVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET); + + // Compute expenditure + uint256 e = getL1FeeExpenditure(); + + // Compute revenue and profit + uint256 r = d + b + q; // revenue + uint256 p = r - e; // profit + + // Compute revenue share + uint256 s = Math.max( + REVENUE_COEFFICIENT_BASIS_POINTS * r / BASIS_POINT_SCALE, + PROFIT_COEFFICIENT_BASIS_POINTS * p / BASIS_POINT_SCALE + ); // share + uint256 remainder = r - s; + + // Send Beneficiary their revenue share on L2 + if (!SafeCall.send(BENEFICIARY, gasleft(), s)) { + revert FailedToShare(); + } + + // Send remaining funds to L1 wallet on L1 + L2StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).bridgeETHTo{ value: remainder }( + L1_WALLET, WITHDRAWAL_MIN_GAS, bytes("") + ); + + emit FeesDisbursed(s, r); + } + + /** + * @dev Returns the RevenueSharer's best estimate of L1 Fee expenditure for the current accounting period. + * @dev TODO this just returns zero for now, until L1 Fee Expenditure can be tracked on L2. + */ + function getL1FeeExpenditure() public pure returns (uint256) { + return 0; + } + + /** + * @dev Receives ETH fees withdrawn from L2 FeeVaults. + * @dev Will revert if ETH is not sent from L2 FeeVaults. + */ + receive() external payable virtual { + if ( + msg.sender != Predeploys.SEQUENCER_FEE_WALLET && msg.sender != Predeploys.BASE_FEE_VAULT + && msg.sender != Predeploys.L1_FEE_VAULT + ) { + revert OnlyFeeVaults(); + } + emit FeesReceived(msg.sender, msg.value); + } + + /*////////////////////////////////////////////////////////////// + Internal Functions + //////////////////////////////////////////////////////////////*/ + /** + * @dev Withdraws fees from a FeeVault and returns the amount withdrawn. + * @param _feeVault The address of the FeeVault to withdraw from. + * @dev Withdrawal will only occur if the given FeeVault's balance is greater than or equal to + * the minimum withdrawal amount. + */ + function feeVaultWithdrawal(address payable _feeVault) internal returns (uint256) { + if (FeeVault(_feeVault).WITHDRAWAL_NETWORK() != FeeVault.WithdrawalNetwork.L2) { + revert UnexpectedFeeVaultWithdrawalNetwork(); + } + if (FeeVault(_feeVault).RECIPIENT() != address(this)) revert UnexpectedFeeVaultRecipient(); + uint256 initial_balance = address(this).balance; + // The following line will call back into the receive() function on this contract, + // causing all of the ether from the fee vault to move to this contract: + FeeVault(_feeVault).withdraw(); + return address(this).balance - initial_balance; + } +} diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index 0fc2d3935caa..daf9b5858876 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -8,6 +8,9 @@ library Predeploys { /// @notice Number of predeploy-namespace addresses reserved for protocol usage. uint256 internal constant PREDEPLOY_COUNT = 2048; + /// @notice Address of the RevenueSharer predeploy. + address payable internal constant REVENUE_SHARER = payable(0x4200000000000000000000000000000000000024); + /// @custom:legacy /// @notice Address of the LegacyMessagePasser predeploy. Deprecate. Use the updated /// L2ToL1MessagePasser contract instead. @@ -37,7 +40,7 @@ library Predeploys { address internal constant L2_STANDARD_BRIDGE = 0x4200000000000000000000000000000000000010; //// @notice Address of the SequencerFeeWallet predeploy. - address internal constant SEQUENCER_FEE_WALLET = 0x4200000000000000000000000000000000000011; + address payable internal constant SEQUENCER_FEE_WALLET = payable(0x4200000000000000000000000000000000000011); /// @notice Address of the OptimismMintableERC20Factory predeploy. address internal constant OPTIMISM_MINTABLE_ERC20_FACTORY = 0x4200000000000000000000000000000000000012; @@ -63,10 +66,10 @@ library Predeploys { address internal constant PROXY_ADMIN = 0x4200000000000000000000000000000000000018; /// @notice Address of the BaseFeeVault predeploy. - address internal constant BASE_FEE_VAULT = 0x4200000000000000000000000000000000000019; + address payable internal constant BASE_FEE_VAULT = payable(0x4200000000000000000000000000000000000019); /// @notice Address of the L1FeeVault predeploy. - address internal constant L1_FEE_VAULT = 0x420000000000000000000000000000000000001A; + address payable internal constant L1_FEE_VAULT = payable(0x420000000000000000000000000000000000001A); /// @notice Address of the SchemaRegistry predeploy. address internal constant SCHEMA_REGISTRY = 0x4200000000000000000000000000000000000020; @@ -111,6 +114,7 @@ library Predeploys { if (_addr == GOVERNANCE_TOKEN) return "GovernanceToken"; if (_addr == LEGACY_ERC20_ETH) return "LegacyERC20ETH"; if (_addr == CROSS_L2_INBOX) return "CrossL2Inbox"; + if (_addr == REVENUE_SHARER) return "RevenueSharer"; revert("Predeploys: unnamed predeploy"); } @@ -126,7 +130,8 @@ library Predeploys { || _addr == SEQUENCER_FEE_WALLET || _addr == OPTIMISM_MINTABLE_ERC20_FACTORY || _addr == L1_BLOCK_NUMBER || _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 == SCHEMA_REGISTRY || _addr == EAS || _addr == GOVERNANCE_TOKEN; + || _addr == L1_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS || _addr == GOVERNANCE_TOKEN + || _addr == REVENUE_SHARER; } function isPredeployNamespace(address _addr) internal pure returns (bool) { diff --git a/packages/contracts-bedrock/test/L2/RevenueSharer.t.sol b/packages/contracts-bedrock/test/L2/RevenueSharer.t.sol new file mode 100644 index 000000000000..9a622ac6c485 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/RevenueSharer.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Target contract dependencies +import { FeeVault } from "src/universal/FeeVault.sol"; +import { SequencerFeeVault } from "src/L2/SequencerFeeVault.sol"; +import { BaseFeeVault } from "src/L2/BaseFeeVault.sol"; +import { L1FeeVault } from "src/L2/L1FeeVault.sol"; + +// Target contract +import { RevenueSharer } from "src/L2/RevenueSharer.sol"; + +contract RevenueSharer_Test is CommonTest { + address recipient; + address remainderRecipient; + + /// @dev Sets up the test suite. + function setUp() public override { + super.setUp(); + recipient = deploy.cfg().revenueShareRecipient(); + remainderRecipient = deploy.cfg().revenueShareRemainderRecipient(); + } + + /// @dev Tests that the l1 fee wallet is correct. + function test_constructor_succeeds() external { + // TODO + } + + /// @dev Tests that the fee vault is able to receive ETH. + function test_execute_succeeds() external { + // Deal some ETH to the fee vaults, sanity check the results + vm.deal(address(sequencerFeeVault), 120); + assertEq(address(sequencerFeeVault).balance, 120); + + vm.deal(address(l1FeeVault), 70); + assertEq(address(l1FeeVault).balance, 70); + + vm.deal(address(baseFeeVault), 110); + assertEq(address(baseFeeVault).balance, 110); + + // Setup assertion that an event will be emitted + // vm.expectEmit(Predeploys.SEQUENCER_FEE_WALLET); + // emit FeesDisbursed(45, 300); + + // Execute + revenueSharer.execute(); + + // Assert 15% of revenue flows to beneficiary + assertEq(recipient.balance, 45); + + // Assert 85% of revenue flows to other party + assertEq(remainderRecipient.balance, 255); + + // Assert RevenueSharer does not accumulate ETH + assertEq(address(revenueSharer).balance, 0); + + // Assert FeeVaults are depleted + assertEq(address(sequencerFeeVault).balance, 0); + assertEq(address(l1FeeVault).balance, 0); + assertEq(address(baseFeeVault).balance, 0); + } +} diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index d585e5aff4a6..1276f0446e64 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -11,6 +11,7 @@ import { L2ERC721Bridge } from "src/L2/L2ERC721Bridge.sol"; import { BaseFeeVault } from "src/L2/BaseFeeVault.sol"; import { SequencerFeeVault } from "src/L2/SequencerFeeVault.sol"; import { L1FeeVault } from "src/L2/L1FeeVault.sol"; +import { RevenueSharer } from "src/L2/RevenueSharer.sol"; import { GasPriceOracle } from "src/L2/GasPriceOracle.sol"; import { L1Block } from "src/L2/L1Block.sol"; import { LegacyMessagePasser } from "src/legacy/LegacyMessagePasser.sol"; @@ -86,6 +87,7 @@ contract Setup { BaseFeeVault baseFeeVault = BaseFeeVault(payable(Predeploys.BASE_FEE_VAULT)); SequencerFeeVault sequencerFeeVault = SequencerFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)); L1FeeVault l1FeeVault = L1FeeVault(payable(Predeploys.L1_FEE_VAULT)); + RevenueSharer revenueSharer = RevenueSharer(Predeploys.REVENUE_SHARER); GasPriceOracle gasPriceOracle = GasPriceOracle(Predeploys.GAS_PRICE_ORACLE); L1Block l1Block = L1Block(Predeploys.L1_BLOCK_ATTRIBUTES); LegacyMessagePasser legacyMessagePasser = LegacyMessagePasser(Predeploys.LEGACY_MESSAGE_PASSER); @@ -204,6 +206,7 @@ contract Setup { labelPredeploy(Predeploys.GOVERNANCE_TOKEN); labelPredeploy(Predeploys.EAS); labelPredeploy(Predeploys.SCHEMA_REGISTRY); + labelPredeploy(Predeploys.REVENUE_SHARER); // L2 Preinstalls labelPreinstall(Preinstalls.MultiCall3);