Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Enshrined Revenue Sharing implementation #10320

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions op-bindings/predeploys/addresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -39,6 +40,7 @@ const (
)

var (
RevenueSharerAddr = common.HexToAddress(RevenueSharer)
L2ToL1MessagePasserAddr = common.HexToAddress(L2ToL1MessagePasser)
DeployerWhitelistAddr = common.HexToAddress(DeployerWhitelist)
WETH9Addr = common.HexToAddress(WETH9)
Expand Down Expand Up @@ -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}
Expand Down
16 changes: 9 additions & 7 deletions packages/contracts-bedrock/deploy-config/hardhat.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -65,4 +67,4 @@
"daResolveWindow": 100,
"daBondSize": 1000,
"daResolverRefundPercentage": 50
}
}
4 changes: 3 additions & 1 deletion packages/contracts-bedrock/scripts/Artifacts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions packages/contracts-bedrock/scripts/DeployConfig.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -110,6 +112,8 @@ contract DeployConfig is Script {
sequencerFeeVaultRecipient = stdJson.readAddress(_json, "$.sequencerFeeVaultRecipient");
sequencerFeeVaultMinimumWithdrawalAmount = stdJson.readUint(_json, "$.sequencerFeeVaultMinimumWithdrawalAmount");
sequencerFeeVaultWithdrawalNetwork = stdJson.readUint(_json, "$.sequencerFeeVaultWithdrawalNetwork");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you could add a useRevenueShare = _envOr(_json, ".useRevenueShare", true); that would be great then in the Genesis generation you can know to configure the fee vaults appropriately when its set to true. Will need to add a setter setUseRevenueShare to be able to not break legacy tests, you can see how this is done with plasma, you need to call it in the tests setUp before calling super.setUp

revenueShareRecipient = payable(stdJson.readAddress(_json, "$.revenueShareRecipient"));
revenueShareRemainderRecipient = payable(stdJson.readAddress(_json, "$.revenueShareRemainderRecipient"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you use _envOr with null fallback values? This prevents breaking config and also makes it so that the null values are set when the revenue share is turned off

governanceTokenName = stdJson.readString(_json, "$.governanceTokenName");
governanceTokenSymbol = stdJson.readString(_json, "$.governanceTokenSymbol");
governanceTokenOwner = stdJson.readAddress(_json, "$.governanceTokenOwner");
Expand Down
9 changes: 9 additions & 0 deletions packages/contracts-bedrock/scripts/L2Genesis.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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()
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should be sure to call initialize on the implementation too, with null values


function setProxyAdmin() public {
// Note the ProxyAdmin implementation itself is behind a proxy that owns itself.
address impl = _setImplementationCode(Predeploys.PROXY_ADMIN);
Expand Down
175 changes: 175 additions & 0 deletions packages/contracts-bedrock/src/L2/RevenueSharer.sol
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you added a mermaid diagram to the specs that showed the flow of calls, that would be amazing

// 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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would like us to add a version of SafeCall that only takes 2 arguments so we don't need to pass gasleft. If you don't want to do it as part of this PR thats fine

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;
}
}
13 changes: 9 additions & 4 deletions packages/contracts-bedrock/src/libraries/Predeploys.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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");
}

Expand All @@ -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) {
Expand Down
Loading