Skip to content
Merged
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
201 changes: 201 additions & 0 deletions src/template/RevShareSetup.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import {VmSafe} from "forge-std/Vm.sol";
import {stdToml} from "forge-std/StdToml.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import {OPCMTaskBase} from "src/tasks/types/OPCMTaskBase.sol";
import {Action} from "src/libraries/MultisigTypes.sol";
import {MultisigTaskPrinter} from "src/libraries/MultisigTaskPrinter.sol";
import {RevShareContractsUpgrader} from "src/RevShareContractsUpgrader.sol";
import {FeeSplitterSetup} from "src/libraries/FeeSplitterSetup.sol";

/// @notice Task for setting up revenue sharing on OP Stack chains after predeploys upgrade.
contract RevShareSetup is OPCMTaskBase {
using stdToml for string;
using EnumerableSet for EnumerableSet.AddressSet;

/// @notice RevShareContractsUpgrader address
address public REV_SHARE_UPGRADER;

/// @notice RevShare configurations
RevShareContractsUpgrader.RevShareConfig[] internal revShareConfigs;

/// @notice Names in the SimpleAddressRegistry that are expected to be written during this task.
function _taskStorageWrites() internal pure virtual override returns (string[] memory) {
return new string[](0);
}

/// @notice Returns an array of strings that refer to contract names in the address registry.
function _taskBalanceChanges() internal view virtual override returns (string[] memory) {
return new string[](0);
}

/// @notice Sets the allowed storage accesses - override to add portal addresses
function _setAllowedStorageAccesses() internal virtual override {
super._setAllowedStorageAccesses();
// Add portal addresses as they will have storage writes from depositTransaction calls
for (uint256 i; i < revShareConfigs.length; i++) {
_allowedStorageAccesses.add(revShareConfigs[i].portal);
}
}

/// @notice Sets up the template with configurations from a TOML file.
function _templateSetup(string memory taskConfigFilePath, address) internal override {
string memory tomlContent = vm.readFile(taskConfigFilePath);

// Load RevShareContractsUpgrader address from TOML
REV_SHARE_UPGRADER = tomlContent.readAddress(".revShareUpgrader");
require(REV_SHARE_UPGRADER != address(0), "RevShareContractsUpgrader address cannot be zero");
require(REV_SHARE_UPGRADER.code.length > 0, "RevShareContractsUpgrader has no code");
vm.label(REV_SHARE_UPGRADER, "RevShareContractsUpgrader");

// Set RevShareContractsUpgrader as the allowed target for delegatecall
OPCM_TARGETS.push(REV_SHARE_UPGRADER);

// Load flattened arrays from TOML
address[] memory portals = abi.decode(tomlContent.parseRaw(".portals"), (address[]));
address[] memory chainFeesRecipients = abi.decode(tomlContent.parseRaw(".chainFeesRecipients"), (address[]));
uint256[] memory minWithdrawalAmounts =
abi.decode(tomlContent.parseRaw(".l1WithdrawerMinWithdrawalAmounts"), (uint256[]));
address[] memory l1WithdrawerRecipients =
abi.decode(tomlContent.parseRaw(".l1WithdrawerRecipients"), (address[]));
uint256[] memory gasLimits = abi.decode(tomlContent.parseRaw(".l1WithdrawerGasLimits"), (uint256[]));

// Validate all arrays have the same length
require(portals.length > 0, "No configs found");
require(
portals.length == chainFeesRecipients.length && portals.length == minWithdrawalAmounts.length
&& portals.length == l1WithdrawerRecipients.length && portals.length == gasLimits.length,
"Config arrays length mismatch"
);
Comment thread
0xDiscotech marked this conversation as resolved.

// Validate individual configuration values and check for duplicates
for (uint256 i; i < portals.length; i++) {
// Validate portal address
require(portals[i] != address(0), string.concat("Portal address cannot be zero at index ", vm.toString(i)));
require(portals[i].code.length > 0, string.concat("Portal has no code at index ", vm.toString(i)));

// Check for duplicate portals
for (uint256 j; j < i; j++) {
require(portals[i] != portals[j], string.concat("Duplicate portal address at index ", vm.toString(i)));
}

// Validate chain fees recipient
require(
chainFeesRecipients[i] != address(0),
string.concat("Chain fees recipient cannot be zero at index ", vm.toString(i))
);

// Validate L1 withdrawer recipient
require(
l1WithdrawerRecipients[i] != address(0),
string.concat("L1 withdrawer recipient cannot be zero at index ", vm.toString(i))
);

// Validate gas limit bounds
require(gasLimits[i] > 0, string.concat("Gas limit must be greater than 0 at index ", vm.toString(i)));
require(
gasLimits[i] <= type(uint32).max,
string.concat("Gas limit exceeds uint32 max at index ", vm.toString(i))
);
Comment thread
0xDiscotech marked this conversation as resolved.
}
Comment thread
0xDiscotech marked this conversation as resolved.

// Construct RevShare configs array from flattened arrays
for (uint256 i; i < portals.length; i++) {
revShareConfigs.push(
RevShareContractsUpgrader.RevShareConfig({
portal: portals[i],
l1WithdrawerConfig: FeeSplitterSetup.L1WithdrawerConfig({
minWithdrawalAmount: minWithdrawalAmounts[i],
recipient: l1WithdrawerRecipients[i],
gasLimit: uint32(gasLimits[i])
}),
chainFeesRecipient: chainFeesRecipients[i]
})
);
}
}

/// @notice Builds the actions for executing the operations.
function _build(address) internal override {
// Delegatecall into RevShareContractsUpgrader
// OPCMTaskBase uses Multicall3Delegatecall, so this delegatecall will be captured as an action
(bool success,) =
REV_SHARE_UPGRADER.delegatecall(abi.encodeCall(RevShareContractsUpgrader.setupRevShare, (revShareConfigs)));
require(success, "RevShareSetup: Delegatecall failed");
}

/// @notice This method performs all validations and assertions that verify the calls executed as expected.
function _validate(VmSafe.AccountAccess[] memory, Action[] memory _actions, address) internal view override {
MultisigTaskPrinter.printTitle("Validating delegatecall to RevShareContractsUpgrader");

// For OPCM tasks using delegatecall, we validate that the delegatecall was made correctly.
// The actual portal calls happen inside the delegatecall and are validated by integration tests.

require(_actions.length == 1, "Expected exactly one action");

Action memory action = _actions[0];
require(action.target == REV_SHARE_UPGRADER, "Delegatecall to RevShareContractsUpgrader not found");
require(action.value == 0, "Call value must be 0 for delegatecall");

// Verify it's calling setupRevShare
bytes4 selector = bytes4(action.arguments);
require(
selector == RevShareContractsUpgrader.setupRevShare.selector, "Wrong function selector for delegatecall"
);

// Decode and validate the revShareConfigs argument
// Skip the first 4 bytes (function selector) and decode the rest
RevShareContractsUpgrader.RevShareConfig[] memory configs;
{
bytes memory args = action.arguments;
bytes memory argsWithoutSelector = new bytes(args.length - 4);
for (uint256 j = 4; j < args.length; j++) {
argsWithoutSelector[j - 4] = args[j];
}
configs = abi.decode(argsWithoutSelector, (RevShareContractsUpgrader.RevShareConfig[]));
}

// Validate each config
require(configs.length > 0, "No configs provided");
require(configs.length == revShareConfigs.length, "Config length mismatch");

for (uint256 i; i < configs.length; i++) {
RevShareContractsUpgrader.RevShareConfig memory config = configs[i];

// Validate portal address is not zero
require(config.portal != address(0), "Portal address cannot be zero");

// Validate L1 withdrawer config
require(config.l1WithdrawerConfig.recipient != address(0), "L1 withdrawer recipient cannot be zero");
require(config.l1WithdrawerConfig.gasLimit > 0, "Gas limit must be greater than 0");

// Validate chain fees recipient
require(config.chainFeesRecipient != address(0), "Chain fees recipient cannot be zero");

// Validate config matches the expected config from template setup
require(config.portal == revShareConfigs[i].portal, "Portal address mismatch");
require(
config.l1WithdrawerConfig.minWithdrawalAmount
== revShareConfigs[i].l1WithdrawerConfig.minWithdrawalAmount,
"Min withdrawal amount mismatch"
);
require(
config.l1WithdrawerConfig.recipient == revShareConfigs[i].l1WithdrawerConfig.recipient,
"L1 withdrawer recipient mismatch"
);
require(
config.l1WithdrawerConfig.gasLimit == revShareConfigs[i].l1WithdrawerConfig.gasLimit,
"Gas limit mismatch"
);
require(config.chainFeesRecipient == revShareConfigs[i].chainFeesRecipient, "Chain fees recipient mismatch");
}
}

/// @notice Override to return a list of addresses that should not be checked for code length.
function _getCodeExceptions() internal view virtual override returns (address[] memory) {
return new address[](0);
}
}
175 changes: 175 additions & 0 deletions test/integration/IntegrationBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,60 @@ import {Test} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {console2} from "forge-std/console2.sol";
import {AddressAliasHelper} from "@eth-optimism-bedrock/src/vendor/AddressAliasHelper.sol";
import {RevShareContractsUpgrader} from "src/RevShareContractsUpgrader.sol";
import {Predeploys} from "@eth-optimism-bedrock/src/libraries/Predeploys.sol";
import {IFeeSplitter} from "src/interfaces/IFeeSplitter.sol";
import {IFeeVault} from "src/interfaces/IFeeVault.sol";
import {IL1Withdrawer} from "src/interfaces/IL1Withdrawer.sol";
import {ISuperchainRevSharesCalculator} from "src/interfaces/ISuperchainRevSharesCalculator.sol";

/// @title IntegrationBase
/// @notice Base contract for integration tests with L1->L2 deposit transaction relay functionality
abstract contract IntegrationBase is Test {
// Events for testing
event WithdrawalInitiated(address indexed recipient, uint256 amount);

// Fork IDs
uint256 internal _mainnetForkId;
uint256 internal _opMainnetForkId;
uint256 internal _inkMainnetForkId;

// Shared upgrader contract
RevShareContractsUpgrader public revShareUpgrader;

// L1 addresses
address internal constant PROXY_ADMIN_OWNER = 0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A;
address internal constant OP_MAINNET_PORTAL = 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed;
address internal constant INK_MAINNET_PORTAL = 0x5d66C1782664115999C47c9fA5cd031f495D3e4F;
address internal constant REV_SHARE_UPGRADER_ADDRESS = 0x0000000000000000000000000000000000001337;

// L2 predeploys (same across all OP Stack chains)
address internal constant SEQUENCER_FEE_VAULT = 0x4200000000000000000000000000000000000011;
address internal constant OPERATOR_FEE_VAULT = 0x420000000000000000000000000000000000001b;
address internal constant BASE_FEE_VAULT = 0x4200000000000000000000000000000000000019;
address internal constant L1_FEE_VAULT = 0x420000000000000000000000000000000000001A;
address internal constant FEE_SPLITTER = 0x420000000000000000000000000000000000002B;

// Expected deployed contracts (deterministic CREATE2 addresses)
address internal constant OP_L1_WITHDRAWER = 0xB3AeB34b88D73Fb4832f65BEa5Bd865017fB5daC;
address internal constant OP_REV_SHARE_CALCULATOR = 0x3E806Fd8592366E850197FEC8D80608b5526Bba2;

address internal constant INK_L1_WITHDRAWER = 0x70e26B12a578176BccCD3b7e7f58f605871c5eF7;
address internal constant INK_REV_SHARE_CALCULATOR = 0xd7a5307B4Ce92B0269903191007b95dF42552Dfa;

// Test configuration - OP Mainnet
uint256 internal constant OP_MIN_WITHDRAWAL_AMOUNT = 350000;
address internal constant OP_L1_WITHDRAWAL_RECIPIENT = 0x0000000000000000000000000000000000000001;
uint32 internal constant OP_WITHDRAWAL_GAS_LIMIT = 800000;
address internal constant OP_CHAIN_FEES_RECIPIENT = 0x0000000000000000000000000000000000000001;

// Test configuration - Ink Mainnet
uint256 internal constant INK_MIN_WITHDRAWAL_AMOUNT = 500000;
address internal constant INK_L1_WITHDRAWAL_RECIPIENT = 0x0000000000000000000000000000000000000002;
uint32 internal constant INK_WITHDRAWAL_GAS_LIMIT = 1000000;
address internal constant INK_CHAIN_FEES_RECIPIENT = 0x0000000000000000000000000000000000000002;

bool internal constant IS_SIMULATE = true;
/// @notice Relay all deposit transactions from L1 to multiple L2s
/// @param _forkIds Array of fork IDs for each L2 chain
/// @param _isSimulate If true, only process the second half of logs to avoid duplicates.
Expand All @@ -17,6 +67,7 @@ abstract contract IntegrationBase is Test {
/// we only process the final simulation results.
/// @param _portals Array of Portal addresses corresponding to each fork.
/// Only events emitted by each portal will be relayed on its corresponding L2.

function _relayAllMessages(uint256[] memory _forkIds, bool _isSimulate, address[] memory _portals) internal {
require(_forkIds.length == _portals.length, "Fork IDs and portals length mismatch");

Expand Down Expand Up @@ -123,4 +174,128 @@ abstract contract IntegrationBase is Test {
}
return _result;
}

/// @notice Fund all fee vaults with specified amount
/// @param _amount Amount to fund each vault with
/// @param _forkId Fork ID to execute on
function _fundVaults(uint256 _amount, uint256 _forkId) internal {
vm.selectFork(_forkId);
vm.deal(SEQUENCER_FEE_VAULT, _amount);
vm.deal(OPERATOR_FEE_VAULT, _amount);
vm.deal(BASE_FEE_VAULT, _amount);
vm.deal(L1_FEE_VAULT, _amount);
}

/// @notice Assert the state of all L2 contracts after upgrade
/// @param _l1Withdrawer Expected L1Withdrawer address
/// @param _revShareCalculator Expected RevShareCalculator address
/// @param _minWithdrawalAmount Expected minimum withdrawal amount for L1Withdrawer
/// @param _l1Recipient Expected recipient address for L1Withdrawer
/// @param _gasLimit Expected gas limit for L1Withdrawer
/// @param _chainFeesRecipient Expected chain fees recipient (remainder recipient)
function _assertL2State(
address _l1Withdrawer,
address _revShareCalculator,
uint256 _minWithdrawalAmount,
address _l1Recipient,
uint32 _gasLimit,
address _chainFeesRecipient
) internal view {
// L1Withdrawer: check configuration
assertEq(
IL1Withdrawer(_l1Withdrawer).minWithdrawalAmount(),
_minWithdrawalAmount,
"L1Withdrawer minWithdrawalAmount mismatch"
);
assertEq(IL1Withdrawer(_l1Withdrawer).recipient(), _l1Recipient, "L1Withdrawer recipient mismatch");
assertEq(IL1Withdrawer(_l1Withdrawer).withdrawalGasLimit(), _gasLimit, "L1Withdrawer gasLimit mismatch");

// Rev Share Calculator: check it's linked correctly
assertEq(
ISuperchainRevSharesCalculator(_revShareCalculator).shareRecipient(),
_l1Withdrawer,
"Calculator shareRecipient should be L1Withdrawer"
);
assertEq(
ISuperchainRevSharesCalculator(_revShareCalculator).remainderRecipient(),
_chainFeesRecipient,
"Calculator remainderRecipient mismatch"
);

// Fee Splitter: check calculator is set
assertEq(
IFeeSplitter(FEE_SPLITTER).sharesCalculator(),
_revShareCalculator,
"FeeSplitter calculator should be set to RevShareCalculator"
);

// Vaults: recipient should be fee splitter, withdrawal network should be L2, min withdrawal amount 0
_assertFeeVaultsState();
}

/// @notice Assert the configuration of all fee vaults
function _assertFeeVaultsState() internal view {
_assertVaultGetters(SEQUENCER_FEE_VAULT, FEE_SPLITTER, IFeeVault.WithdrawalNetwork.L2, 0);
_assertVaultGetters(OPERATOR_FEE_VAULT, FEE_SPLITTER, IFeeVault.WithdrawalNetwork.L2, 0);
_assertVaultGetters(BASE_FEE_VAULT, FEE_SPLITTER, IFeeVault.WithdrawalNetwork.L2, 0);
_assertVaultGetters(L1_FEE_VAULT, FEE_SPLITTER, IFeeVault.WithdrawalNetwork.L2, 0);
}

/// @notice Assert the configuration of a single fee vault
/// @param _vault The address of the fee vault
/// @param _recipient The expected recipient of the fee vault
/// @param _withdrawalNetwork The expected withdrawal network
/// @param _minWithdrawalAmount The expected minimum withdrawal amount
/// @dev Ensures both the legacy and the new getters return the same value
function _assertVaultGetters(
address _vault,
address _recipient,
IFeeVault.WithdrawalNetwork _withdrawalNetwork,
uint256 _minWithdrawalAmount
) internal view {
// Check new getters
assertEq(IFeeVault(_vault).recipient(), _recipient, "Vault recipient mismatch");
assertEq(
uint256(IFeeVault(_vault).withdrawalNetwork()),
uint256(_withdrawalNetwork),
"Vault withdrawalNetwork mismatch"
);
assertEq(IFeeVault(_vault).minWithdrawalAmount(), _minWithdrawalAmount, "Vault minWithdrawalAmount mismatch");

// Check legacy getters (should return same values)
assertEq(IFeeVault(_vault).RECIPIENT(), _recipient, "Vault RECIPIENT (legacy) mismatch");
assertEq(
uint256(IFeeVault(_vault).WITHDRAWAL_NETWORK()),
uint256(_withdrawalNetwork),
"Vault WITHDRAWAL_NETWORK (legacy) mismatch"
);
assertEq(
IFeeVault(_vault).MIN_WITHDRAWAL_AMOUNT(),
_minWithdrawalAmount,
"Vault MIN_WITHDRAWAL_AMOUNT (legacy) mismatch"
);
}

/// @notice Execute disburseFees and assert that it triggers a withdrawal with the expected amount
/// @param _forkId The fork ID of the chain to test
/// @param _l1WithdrawalRecipient The expected recipient of the withdrawal
/// @param _expectedWithdrawalAmount The expected withdrawal amount
function _executeDisburseAndAssertWithdrawal(
uint256 _forkId,
address _l1WithdrawalRecipient,
uint256 _expectedWithdrawalAmount
) internal {
vm.selectFork(_forkId);
vm.warp(block.timestamp + IFeeSplitter(FEE_SPLITTER).feeDisbursementInterval() + 1);

uint256 balanceBefore = Predeploys.L2_TO_L1_MESSAGE_PASSER.balance;

vm.expectEmit(true, true, true, true);
emit WithdrawalInitiated(_l1WithdrawalRecipient, _expectedWithdrawalAmount);
IFeeSplitter(FEE_SPLITTER).disburseFees();

uint256 balanceAfter = Predeploys.L2_TO_L1_MESSAGE_PASSER.balance;

assertEq(balanceAfter - balanceBefore, _expectedWithdrawalAmount);
}
}
Loading