diff --git a/test/integration/IntegrationBase.t.sol b/test/integration/IntegrationBase.t.sol index b034ed048..e03473a9f 100644 --- a/test/integration/IntegrationBase.t.sol +++ b/test/integration/IntegrationBase.t.sol @@ -4,12 +4,13 @@ pragma solidity 0.8.15; 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 {IL1CrossDomainMessenger} from "@eth-optimism-bedrock/interfaces/L1/IL1CrossDomainMessenger.sol"; +import {ICrossDomainMessenger} from "@eth-optimism-bedrock/interfaces/universal/ICrossDomainMessenger.sol"; +import {StdStorage, stdStorage} from "forge-std/StdStorage.sol"; import {FeeSplitterSetup} from "src/libraries/FeeSplitterSetup.sol"; import {RevShareCommon} from "src/libraries/RevShareCommon.sol"; import {Utils} from "src/libraries/Utils.sol"; -import {RevShareContractsUpgrader} from "src/RevShareContractsUpgrader.sol"; -import {Predeploys} from "@eth-optimism-bedrock/src/libraries/Predeploys.sol"; +import {AddressAliasHelper} from "@eth-optimism-bedrock/src/vendor/AddressAliasHelper.sol"; import {IFeeSplitter} from "src/interfaces/IFeeSplitter.sol"; import {IFeeVault} from "src/interfaces/IFeeVault.sol"; import {IL1Withdrawer} from "src/interfaces/IL1Withdrawer.sol"; @@ -18,8 +19,11 @@ import {ISuperchainRevSharesCalculator} from "src/interfaces/ISuperchainRevShare /// @title IntegrationBase /// @notice Base contract for integration tests with L1->L2 deposit transaction relay functionality abstract contract IntegrationBase is Test { - // Event for testing + using stdStorage for StdStorage; + // Events for testing + event WithdrawalInitiated(address indexed recipient, uint256 amount); + event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData); // Fork IDs uint256 internal _mainnetForkId; @@ -27,15 +31,22 @@ abstract contract IntegrationBase is Test { uint256 internal _inkMainnetForkId; uint256 internal _soneiumMainnetForkId; - // 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 SONEIUM_MAINNET_PORTAL = 0x88e529A6ccd302c948689Cd5156C83D4614FAE92; - address internal constant REV_SHARE_UPGRADER_ADDRESS = 0x0000000000000000000000000000000000001337; + address internal constant OP_MAINNET_L1_MESSENGER = 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1; + address internal constant INK_MAINNET_L1_MESSENGER = 0x69d3Cf86B2Bf1a9e99875B7e2D9B6a84426c171f; + address internal constant SONEIUM_MAINNET_L1_MESSENGER = 0x9CF951E3F74B644e621b36Ca9cea147a78D4c39f; + + // FeesDepositor configuration (triggers deposit to OP when balance >= threshold) + uint256 internal constant FEES_DEPOSITOR_THRESHOLD = 2 ether; + + // FeesDepositor target on OP Mainnet (OPM multisig) + address internal constant OP_MAINNET_FEES_DEPOSITOR_TARGET = 0x16A27462B4D61BDD72CbBabd3E43e11791F7A28c; + + // Simulation flag for task execution + bool internal constant IS_SIMULATE = true; // L2 predeploys (same across all OP Stack chains) address internal constant SEQUENCER_FEE_VAULT = 0x4200000000000000000000000000000000000011; @@ -43,31 +54,75 @@ abstract contract IntegrationBase is Test { address internal constant BASE_FEE_VAULT = 0x4200000000000000000000000000000000000019; address internal constant L1_FEE_VAULT = 0x420000000000000000000000000000000000001A; address internal constant FEE_SPLITTER = 0x420000000000000000000000000000000000002B; + address internal constant L2_CROSS_DOMAIN_MESSENGER = 0x4200000000000000000000000000000000000007; + + // Default L2 sender address + address internal constant DEFAULT_L2_SENDER = 0x000000000000000000000000000000000000dEaD; + + // Extra gas buffer added to the minimum gas limit for the relayMessage function + uint64 internal constant RELAY_GAS_OVERHEAD = 700_000; + + // Gas limit for simple ETH transfers via L1→L2 relay (FeesDepositor → OP L2) + uint256 internal constant L1_TO_L2_ETH_TRANSFER_GAS_LIMIT = 200_000; + + // Counter for unique L1→L2 message nonces (to avoid collisions on forks) + uint240 internal _l1ToL2NonceCounter; + + /// @notice L2 chain configuration for multi-chain integration tests + /// @param forkId Fork ID for this L2 chain + /// @param portal OptimismPortal address on L1 for this chain + /// @param l1Messenger L1CrossDomainMessenger address for this chain + /// @param minWithdrawalAmount Minimum withdrawal amount for L1Withdrawer (wei) + /// @param l1WithdrawalRecipient Target address on L1 that receives withdrawals + /// @param withdrawalGasLimit Gas limit for L2->L1 withdrawal messages + /// @param chainFeesRecipient Chain fees recipient address (85% share) + /// @param name Human-readable chain name for logging + struct L2ChainConfig { + uint256 forkId; + address portal; + address l1Messenger; + uint256 minWithdrawalAmount; + address l1WithdrawalRecipient; + uint32 withdrawalGasLimit; + address chainFeesRecipient; + string name; + } - // Test configuration - Globals - uint256 internal constant DEFAULT_MIN_WITHDRAWAL_AMOUNT = 2 ether; - uint32 internal constant DEFAULT_WITHDRAWAL_GAS_LIMIT = 800000; - address internal constant FEES_DEPOSITOR = 0xed9B99a703BaD32AC96FDdc313c0652e379251Fd; - - // Test configuration - OP Mainnet - uint256 internal constant OP_MIN_WITHDRAWAL_AMOUNT = DEFAULT_MIN_WITHDRAWAL_AMOUNT; - address internal constant OP_L1_WITHDRAWAL_RECIPIENT = FEES_DEPOSITOR; - uint32 internal constant OP_WITHDRAWAL_GAS_LIMIT = DEFAULT_WITHDRAWAL_GAS_LIMIT; - address internal constant OP_CHAIN_FEES_RECIPIENT = 0x16A27462B4D61BDD72CbBabd3E43e11791F7A28c; - - // Test configuration - Ink Mainnet - uint256 internal constant INK_MIN_WITHDRAWAL_AMOUNT = DEFAULT_MIN_WITHDRAWAL_AMOUNT; - address internal constant INK_L1_WITHDRAWAL_RECIPIENT = FEES_DEPOSITOR; - uint32 internal constant INK_WITHDRAWAL_GAS_LIMIT = DEFAULT_WITHDRAWAL_GAS_LIMIT; - address internal constant INK_CHAIN_FEES_RECIPIENT = 0x5f077b4c3509C2c192e50B6654d924Fcb8126A60; - - // Test configuration - Soneium Mainnet - uint256 internal constant SONEIUM_MIN_WITHDRAWAL_AMOUNT = DEFAULT_MIN_WITHDRAWAL_AMOUNT; - address internal constant SONEIUM_L1_WITHDRAWAL_RECIPIENT = FEES_DEPOSITOR; - uint32 internal constant SONEIUM_WITHDRAWAL_GAS_LIMIT = DEFAULT_WITHDRAWAL_GAS_LIMIT; - address internal constant SONEIUM_CHAIN_FEES_RECIPIENT = 0xF07b3169ffF67A8AECdBb18d9761AEeE34591112; + /// @notice Configuration for the chain being tested (L2 -> L1 withdrawal) + /// @param l1ForkId Fork ID for the L1 chain + /// @param l2ForkId Fork ID for the L2 chain being tested + /// @param l1Withdrawer L1Withdrawer contract address on L2 + /// @param l1WithdrawalRecipient Target address on L1 that receives withdrawals (e.g., FeesDepositor) + /// @param expectedWithdrawalAmount Expected ETH amount to be withdrawn + /// @param portal OptimismPortal address on L1 for this chain + /// @param l1Messenger L1CrossDomainMessenger address for this chain + /// @param withdrawalGasLimit Gas limit for the L2->L1 withdrawal message + struct ChainConfig { + uint256 l1ForkId; + uint256 l2ForkId; + address l1Withdrawer; + address l1WithdrawalRecipient; + uint256 expectedWithdrawalAmount; + address portal; + address l1Messenger; + uint32 withdrawalGasLimit; + } + + /// @notice Configuration for OP chain where FeesDepositor forwards funds (L1 -> OP L2) + /// @param opL2ForkId Fork ID for the OP L2 chain + /// @param opL1Messenger L1CrossDomainMessenger address for OP chain + /// @param opPortal OptimismPortal address on L1 for OP chain + /// @param feesDepositorTarget Target address on OP L2 that receives funds from FeesDepositor + struct OPConfig { + uint256 opL2ForkId; + address opL1Messenger; + address opPortal; + address feesDepositorTarget; + } + + // Array to store all L2 chain configurations + L2ChainConfig[] internal l2Chains; - 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. @@ -76,7 +131,6 @@ 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"); @@ -209,10 +263,10 @@ abstract contract IntegrationBase is Test { bytes32 _salt = RevShareCommon.getSalt("SCRevShareCalculator"); return Utils.getCreate2Address(_salt, _initCode, RevShareCommon.CREATE2_DEPLOYER); } - /// @notice Fund all fee vaults with specified amount - /// @param _amount Amount to fund each vault with - /// @param _forkId Fork ID to execute on + /// @notice Fund all fee vaults with a specified amount + /// @param _amount The amount to fund each vault with + /// @param _forkId The fork ID of the chain to fund function _fundVaults(uint256 _amount, uint256 _forkId) internal { vm.selectFork(_forkId); vm.deal(SEQUENCER_FEE_VAULT, _amount); @@ -312,19 +366,166 @@ abstract contract IntegrationBase is Test { } /// @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); + /// @param _chainConfig Configuration for the chain being tested (L2 -> L1 withdrawal) + /// @param _opConfig Configuration for OP chain where FeesDepositor forwards funds (L1 -> OP L2) + function _executeDisburseAndAssertWithdrawal(ChainConfig memory _chainConfig, OPConfig memory _opConfig) internal { + // ==================== Step 1: Init withdrawal (Rev Share L2) ==================== + // Disburse fees on the Rev Share L2 chain, which triggers L1Withdrawer to initiate a withdrawal to L1 + vm.selectFork(_chainConfig.l2ForkId); vm.warp(block.timestamp + IFeeSplitter(FEE_SPLITTER).feeDisbursementInterval() + 1); - vm.expectEmit(true, true, true, true); - emit WithdrawalInitiated(_l1WithdrawalRecipient, _expectedWithdrawalAmount); + vm.expectEmit(true, true, true, true, _chainConfig.l1Withdrawer); + emit WithdrawalInitiated(_chainConfig.l1WithdrawalRecipient, _chainConfig.expectedWithdrawalAmount); IFeeSplitter(FEE_SPLITTER).disburseFees(); + + // ==================== Step 2: Relay withdrawal (L1) ==================== + // Relay the L2->L1 withdrawal message. If amount >= threshold, FeesDepositor will initiate a deposit to OP. + vm.selectFork(_chainConfig.l1ForkId); + + if (_chainConfig.expectedWithdrawalAmount >= FEES_DEPOSITOR_THRESHOLD) { + // ==================== Step 3: Init deposit to OP (L1) ==================== + // When withdrawal is relayed on L1, FeesDepositor receives the ETH and initiates a deposit to OP L2. + // Expect TransactionDeposited event from OP Portal. + // Note: FeesDepositor calls L1CrossDomainMessenger.sendMessage(), which calls OptimismPortal.depositTransaction() + // The 'from' address in TransactionDeposited is the aliased L1CrossDomainMessenger (not the FeesDepositor) + vm.expectEmit(true, true, true, false, _opConfig.opPortal); + emit TransactionDeposited( + AddressAliasHelper.applyL1ToL2Alias(_opConfig.opL1Messenger), // aliased L1CrossDomainMessenger + L2_CROSS_DOMAIN_MESSENGER, // L2 CrossDomainMessenger + 0, + "" + ); + + // Relay the withdrawal message on L1 (triggers FeesDepositor to initiate deposit to OP) + _relayL2ToL1Message( + _chainConfig.portal, + _chainConfig.l1Messenger, + _chainConfig.l1Withdrawer, // sender on L2 + _chainConfig.l1WithdrawalRecipient, // target on L1 + _chainConfig.expectedWithdrawalAmount, // value + _chainConfig.withdrawalGasLimit, // minGasLimit + "" // data (empty for ETH transfer) + ); + + // ==================== Step 4: Relay deposit (Optimism L2) ==================== + // Relay the L1->L2 deposit message on Optimism, delivering ETH to the final recipient + vm.selectFork(_opConfig.opL2ForkId); + + uint256 recipientBalanceBefore = _opConfig.feesDepositorTarget.balance; + + // Relay the L1→L2 message (simple ETH transfer to FeesDepositor target) + address aliasedOpL1Messenger = AddressAliasHelper.applyL1ToL2Alias(_opConfig.opL1Messenger); + _relayL1ToL2Message( + aliasedOpL1Messenger, + _chainConfig.l1WithdrawalRecipient, // sender (FeesDepositor) + _opConfig.feesDepositorTarget, // target (OP fees recipient) + _chainConfig.expectedWithdrawalAmount, + L1_TO_L2_ETH_TRANSFER_GAS_LIMIT, + "" // empty data for ETH transfer + ); + + uint256 recipientBalanceAfter = _opConfig.feesDepositorTarget.balance; + assertEq( + recipientBalanceAfter - recipientBalanceBefore, + _chainConfig.expectedWithdrawalAmount, + "FeesDepositor target should receive the withdrawal amount" + ); + } else { + // Below threshold: FeesDepositor holds the ETH on L1 (no deposit to OP) + // Simply relay the withdrawal to the L1 recipient + uint256 recipientBalanceBefore = _chainConfig.l1WithdrawalRecipient.balance; + + _relayL2ToL1Message( + _chainConfig.portal, + _chainConfig.l1Messenger, + _chainConfig.l1Withdrawer, // sender on L2 + _chainConfig.l1WithdrawalRecipient, // target on L1 + _chainConfig.expectedWithdrawalAmount, // value + _chainConfig.withdrawalGasLimit, // minGasLimit + "" // data (empty for ETH transfer) + ); + + uint256 recipientBalanceAfter = _chainConfig.l1WithdrawalRecipient.balance; + assertEq( + recipientBalanceAfter - recipientBalanceBefore, + _chainConfig.expectedWithdrawalAmount, + "L1 recipient should receive the withdrawal amount" + ); + } + } + + /// @notice Relay a message from L2 to L1 via the CrossDomainMessenger + /// @dev This simulates the L2->L1 message relay by: + /// 1. Getting the message nonce from the L1 messenger + /// 2. Setting the portal's l2Sender to the L2CrossDomainMessenger + /// 3. Dealing ETH to the portal so it can send value with the message + /// 4. Calling relayMessage on the L1CrossDomainMessenger from the portal + /// 5. Resetting the l2Sender back to the default value + /// @param _portal The OptimismPortal address + /// @param _l1Messenger The L1CrossDomainMessenger address + /// @param _sender The sender address on L2 + /// @param _target The target address on L1 + /// @param _value The ETH value to send + /// @param _minGasLimit The minimum gas limit for the message + /// @param _data The message data + function _relayL2ToL1Message( + address _portal, + address _l1Messenger, + address _sender, + address _target, + uint256 _value, + uint256 _minGasLimit, + bytes memory _data + ) internal { + // Get the message nonce from the L1 messenger + uint256 _messageNonce = IL1CrossDomainMessenger(_l1Messenger).messageNonce(); + + // Set the l2Sender on the portal to the L2CrossDomainMessenger + // This is required for the L1CrossDomainMessenger to accept the message + stdstore.target(_portal).sig("l2Sender()").checked_write(L2_CROSS_DOMAIN_MESSENGER); + + // Deal ETH to the portal so it can send value with the message + vm.deal(_portal, _value); + + // Call relayMessage from the portal with the ETH value + vm.prank(_portal); + IL1CrossDomainMessenger(_l1Messenger).relayMessage{gas: _minGasLimit + RELAY_GAS_OVERHEAD, value: _value}( + _messageNonce, _sender, _target, _value, _minGasLimit, _data + ); + + // Reset the l2Sender back to the default value + stdstore.target(_portal).sig("l2Sender()").checked_write(DEFAULT_L2_SENDER); + } + + /// @notice Relay a message from L1 to L2 via the CrossDomainMessenger + /// @dev This simulates the L1->L2 message relay by calling relayMessage on the L2CrossDomainMessenger. + /// Uses a unique nonce based on block.timestamp to avoid collisions with already-relayed messages on forks. + /// @param _aliasedL1Messenger The aliased L1 messenger address (sender on L2) + /// @param _sender The original sender on L1 + /// @param _target The target address on L2 + /// @param _value The ETH value to send + /// @param _minGasLimit The minimum gas limit for the message + /// @param _data The message data + function _relayL1ToL2Message( + address _aliasedL1Messenger, + address _sender, + address _target, + uint256 _value, + uint256 _minGasLimit, + bytes memory _data + ) internal { + // Use a unique nonce to avoid "message already relayed" errors on forked networks. + // The nonce format is: version (16 bits) | nonce (240 bits) + // Version 1 is used for L1->L2 messages. We combine block.timestamp with a counter for uniqueness. + _l1ToL2NonceCounter++; + uint256 _messageNonce = + (uint256(1) << 240) | uint240(uint256(keccak256(abi.encode(block.timestamp, _l1ToL2NonceCounter)))); + vm.deal(_aliasedL1Messenger, _value); + vm.prank(_aliasedL1Messenger); + // OP adds some extra gas for the relayMessage logic + ICrossDomainMessenger(L2_CROSS_DOMAIN_MESSENGER).relayMessage{ + gas: _minGasLimit + RELAY_GAS_OVERHEAD, + value: _value + }(_messageNonce, _sender, _target, _value, _minGasLimit, _data); } } diff --git a/test/integration/RevShareContractsUpgraderIntegration.t.sol b/test/integration/RevShareContractsUpgraderIntegration.t.sol index 0a5ca87cb..244bcb14c 100644 --- a/test/integration/RevShareContractsUpgraderIntegration.t.sol +++ b/test/integration/RevShareContractsUpgraderIntegration.t.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import {RevShareContractsUpgrader} from "src/RevShareContractsUpgrader.sol"; import {RevShareUpgradeAndSetup} from "src/template/RevShareUpgradeAndSetup.sol"; import {IntegrationBase} from "./IntegrationBase.t.sol"; @@ -9,21 +8,45 @@ contract RevShareContractsUpgraderIntegrationTest is IntegrationBase { RevShareUpgradeAndSetup public revShareTask; function setUp() public { - // Create forks for L1 (mainnet) and L2s (Ink and Soneium only - need proxy upgrade) + // Create forks for L1 (mainnet) and L2s (OP, Ink, and Soneium) + // OP Mainnet L2 is needed for relaying L1→L2 deposits from FeesDepositor _mainnetForkId = vm.createFork("http://127.0.0.1:8545"); + _opMainnetForkId = vm.createFork("http://127.0.0.1:9545"); _inkMainnetForkId = vm.createFork("http://127.0.0.1:9546"); _soneiumMainnetForkId = vm.createFork("http://127.0.0.1:9547"); - // Deploy contracts on L1 - vm.selectFork(_mainnetForkId); + // Configure Ink and Soneium chains with production config values + // Values from test/tasks/example/eth/017-revshare-upgrade-and-setup-sony-ink/config.toml + l2Chains.push( + L2ChainConfig({ + forkId: _inkMainnetForkId, + portal: INK_MAINNET_PORTAL, + l1Messenger: INK_MAINNET_L1_MESSENGER, + minWithdrawalAmount: 2 ether, + l1WithdrawalRecipient: 0xed9B99a703BaD32AC96FDdc313c0652e379251Fd, + withdrawalGasLimit: 800000, + chainFeesRecipient: 0x5f077b4c3509C2c192e50B6654d924Fcb8126A60, + name: "Ink Mainnet" + }) + ); - // Deploy RevShareContractsUpgrader and etch at predetermined address - revShareUpgrader = new RevShareContractsUpgrader(); - vm.etch(REV_SHARE_UPGRADER_ADDRESS, address(revShareUpgrader).code); - revShareUpgrader = RevShareContractsUpgrader(REV_SHARE_UPGRADER_ADDRESS); + l2Chains.push( + L2ChainConfig({ + forkId: _soneiumMainnetForkId, + portal: SONEIUM_MAINNET_PORTAL, + l1Messenger: SONEIUM_MAINNET_L1_MESSENGER, + minWithdrawalAmount: 2 ether, + l1WithdrawalRecipient: 0xed9B99a703BaD32AC96FDdc313c0652e379251Fd, + withdrawalGasLimit: 800000, + chainFeesRecipient: 0xF07b3169ffF67A8AECdBb18d9761AEeE34591112, + name: "Soneium Mainnet" + }) + ); - // Deploy RevShareUpgradeAndSetup task revShareTask = new RevShareUpgradeAndSetup(); + + // Switch to mainnet fork for task execution + vm.selectFork(_mainnetForkId); } /// @notice Test the integration of upgradeAndSetupRevShare (Ink and Soneium only - need proxy upgrade) @@ -34,65 +57,73 @@ contract RevShareContractsUpgraderIntegrationTest is IntegrationBase { // Step 2: Execute task simulation revShareTask.simulate("test/tasks/example/eth/017-revshare-upgrade-and-setup-sony-ink/config.toml"); - // Step 3: Relay deposit transactions from L1 to Ink and Soneium - uint256[] memory forkIds = new uint256[](2); - forkIds[0] = _inkMainnetForkId; - forkIds[1] = _soneiumMainnetForkId; + // Step 3: Relay deposit transactions from L1 to all L2s + uint256[] memory forkIds = new uint256[](l2Chains.length); + address[] memory portals = new address[](l2Chains.length); - address[] memory portals = new address[](2); - portals[0] = INK_MAINNET_PORTAL; - portals[1] = SONEIUM_MAINNET_PORTAL; + for (uint256 i = 0; i < l2Chains.length; i++) { + forkIds[i] = l2Chains[i].forkId; + portals[i] = l2Chains[i].portal; + } _relayAllMessages(forkIds, IS_SIMULATE, portals); - // Step 4: Assert the state of the Ink Mainnet contracts - vm.selectFork(_inkMainnetForkId); - address inkL1Withdrawer = _computeL1WithdrawerAddress( - INK_MIN_WITHDRAWAL_AMOUNT, INK_L1_WITHDRAWAL_RECIPIENT, INK_WITHDRAWAL_GAS_LIMIT - ); - address inkRevShareCalculator = _computeRevShareCalculatorAddress(inkL1Withdrawer, INK_CHAIN_FEES_RECIPIENT); - _assertL2State( - inkL1Withdrawer, - inkRevShareCalculator, - INK_MIN_WITHDRAWAL_AMOUNT, - INK_L1_WITHDRAWAL_RECIPIENT, - INK_WITHDRAWAL_GAS_LIMIT, - INK_CHAIN_FEES_RECIPIENT - ); - - // Step 5: Assert the state of the Soneium Mainnet contracts - vm.selectFork(_soneiumMainnetForkId); - address soneiumL1Withdrawer = _computeL1WithdrawerAddress( - SONEIUM_MIN_WITHDRAWAL_AMOUNT, SONEIUM_L1_WITHDRAWAL_RECIPIENT, SONEIUM_WITHDRAWAL_GAS_LIMIT - ); - address soneiumRevShareCalculator = - _computeRevShareCalculatorAddress(soneiumL1Withdrawer, SONEIUM_CHAIN_FEES_RECIPIENT); - _assertL2State( - soneiumL1Withdrawer, - soneiumRevShareCalculator, - SONEIUM_MIN_WITHDRAWAL_AMOUNT, - SONEIUM_L1_WITHDRAWAL_RECIPIENT, - SONEIUM_WITHDRAWAL_GAS_LIMIT, - SONEIUM_CHAIN_FEES_RECIPIENT - ); - - // Step 6: Do a withdrawal flow - - // Fund vaults with amount > minWithdrawalAmount - // It disburses 5 ether to each of the 4 vaults, so total sent is 20 ether per chain - _fundVaults(5 ether, _inkMainnetForkId); - _fundVaults(5 ether, _soneiumMainnetForkId); - - // Disburse fees in all chains and expect the L1Withdrawer to trigger the withdrawal + // Step 4: Assert L2 state for all chains + for (uint256 i = 0; i < l2Chains.length; i++) { + L2ChainConfig memory chain = l2Chains[i]; + + vm.selectFork(chain.forkId); + + address l1Withdrawer = _computeL1WithdrawerAddress( + chain.minWithdrawalAmount, chain.l1WithdrawalRecipient, chain.withdrawalGasLimit + ); + address revShareCalculator = _computeRevShareCalculatorAddress(l1Withdrawer, chain.chainFeesRecipient); + + _assertL2State( + l1Withdrawer, + revShareCalculator, + chain.minWithdrawalAmount, + chain.l1WithdrawalRecipient, + chain.withdrawalGasLimit, + chain.chainFeesRecipient + ); + } + + // Step 5: Fund vaults for all chains + for (uint256 i = 0; i < l2Chains.length; i++) { + _fundVaults(5 ether, l2Chains[i].forkId); + } + + // Step 6: Disburse fees in all chains and assert withdrawals // Expected L1Withdrawer share = 15 ether * 15% = 2.25 ether // It is 15 ether instead of 20 because net revenue doesn't count L1FeeVault's balance // For details on the rev share calculation, check the SuperchainRevSharesCalculator contract. // https://github.com/ethereum-optimism/optimism/blob/f392d4b7e8bc5d1c8d38fcf19c8848764f8bee3b/packages/contracts-bedrock/src/L2/SuperchainRevSharesCalculator.sol#L67-L101 uint256 expectedWithdrawalAmount = 2.25 ether; - _executeDisburseAndAssertWithdrawal(_inkMainnetForkId, INK_L1_WITHDRAWAL_RECIPIENT, expectedWithdrawalAmount); - _executeDisburseAndAssertWithdrawal( - _soneiumMainnetForkId, SONEIUM_L1_WITHDRAWAL_RECIPIENT, expectedWithdrawalAmount - ); + for (uint256 i = 0; i < l2Chains.length; i++) { + L2ChainConfig memory chain = l2Chains[i]; + address l1Withdrawer = _computeL1WithdrawerAddress( + chain.minWithdrawalAmount, chain.l1WithdrawalRecipient, chain.withdrawalGasLimit + ); + _executeDisburseAndAssertWithdrawal( + ChainConfig({ + l1ForkId: _mainnetForkId, + l2ForkId: chain.forkId, + l1Withdrawer: l1Withdrawer, + l1WithdrawalRecipient: chain.l1WithdrawalRecipient, + expectedWithdrawalAmount: expectedWithdrawalAmount, + portal: chain.portal, + l1Messenger: chain.l1Messenger, + withdrawalGasLimit: chain.withdrawalGasLimit + }), + OPConfig({ + opL2ForkId: _opMainnetForkId, + opL1Messenger: OP_MAINNET_L1_MESSENGER, + opPortal: OP_MAINNET_PORTAL, + feesDepositorTarget: OP_MAINNET_FEES_DEPOSITOR_TARGET + }) + ); + } } } diff --git a/test/integration/RevSharePostTaskAssertions.t.sol b/test/integration/RevSharePostTaskAssertions.t.sol new file mode 100644 index 000000000..63f3d2176 --- /dev/null +++ b/test/integration/RevSharePostTaskAssertions.t.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {IntegrationBase} from "./IntegrationBase.t.sol"; +import {IFeeSplitter} from "src/interfaces/IFeeSplitter.sol"; +import {ISuperchainRevSharesCalculator} from "src/interfaces/ISuperchainRevSharesCalculator.sol"; + +/// @title RevSharePostTaskAssertionsTest +/// @notice Integration test for asserting Rev Share contract state after task execution. +/// This test does NOT execute the task simulation or relay L1->L2 messages. +/// It directly asserts the expected state on L2 chains after a real task execution. +/// The L1Withdrawer and calculator addresses are queried directly from the FeeSplitter +/// on-chain, making this test compatible with any deployment mechanism (CREATE2 or genesis). +/// @dev Required environment variables: +/// - RPC_URL: L2 RPC URL to create fork +/// - L1_RPC_URL: L1 RPC URL to create fork (for withdrawal relay tests) +/// - OP_RPC_URL: OP L2 RPC URL for L1→L2 relay tests +/// - OPTIMISM_PORTAL: Portal address for the chain +/// - L1_MESSENGER: L1CrossDomainMessenger address for the chain +/// - OP_L1_MESSENGER: OP L1CrossDomainMessenger address +/// - OP_PORTAL: OP Portal address where FeesDepositor deposits to +/// - FEES_DEPOSITOR_TARGET: Target address that FeesDepositor sends funds to on OP L2 +/// - MIN_WITHDRAWAL_AMOUNT: Expected min withdrawal amount for L1Withdrawer (wei) +/// - L1_WITHDRAWAL_RECIPIENT: Expected L1 withdrawal recipient address +/// - WITHDRAWAL_GAS_LIMIT: Expected gas limit for withdrawals +/// - CHAIN_FEES_RECIPIENT: Expected chain fees recipient address +/// @dev Example command: +/// ```sh +/// RPC_URL="https://revshare-alpha-0.optimism.io" \ +/// L1_RPC_URL="https://ethereum-sepolia-rpc.publicnode.com" \ +/// OP_RPC_URL="https://sepolia.optimism.io" \ +/// OPTIMISM_PORTAL="0x176e57217e8824e26cd0f78cd6de2a0655feb675" \ +/// L1_MESSENGER="0xb24a72a720e0ddec249379dc04bcb1a9c780c7c6" \ +/// OP_L1_MESSENGER="0x58Cc85b8D04EA49cC6DBd3CbFFd00B4B8D6cb3ef" \ +/// OP_PORTAL="0x16Fc5058F25648194471939df75CF27A2fdC48BC" \ +/// FEES_DEPOSITOR_TARGET="0x7ca800c55ad9C745AC84FdeEfaf4522F4Df07577" \ +/// MIN_WITHDRAWAL_AMOUNT="2000000000000000000" \ +/// L1_WITHDRAWAL_RECIPIENT="0xed9B99a703BaD32AC96FDdc313c0652e379251Fd" \ +/// WITHDRAWAL_GAS_LIMIT="800000" \ +/// CHAIN_FEES_RECIPIENT="0x455A1115C97cb0E2b24B064C00a9E13872cC37ca" \ +/// forge test --match-contract RevSharePostTaskAssertionsTest -vvv +/// ``` +contract RevSharePostTaskAssertionsTest is IntegrationBase { + // Fork ID + uint256 internal _l2ForkId; + + // Chain configuration from env vars + address internal _portal; + address internal _l1Messenger; + address internal _opL1Messenger; + address internal _opPortal; + address internal _feesDepositorTarget; + + // Expected values from env vars + uint256 internal _expectedMinWithdrawalAmount; + address internal _expectedL1WithdrawalRecipient; + uint32 internal _expectedWithdrawalGasLimit; + address internal _expectedChainFeesRecipient; + + // RevShare addresses discovered from on-chain state + address internal _calculator; + address internal _l1Withdrawer; + + // Flag to track if env vars are set + bool internal _isEnabled; + + function setUp() public { + // Read env vars with defaults to detect if they're set + string memory rpcUrl = vm.envOr("RPC_URL", string("")); + string memory l1RpcUrl = vm.envOr("L1_RPC_URL", string("")); + string memory opRpcUrl = vm.envOr("OP_RPC_URL", string("")); + _portal = vm.envOr("OPTIMISM_PORTAL", address(0)); + _l1Messenger = vm.envOr("L1_MESSENGER", address(0)); + _opL1Messenger = vm.envOr("OP_L1_MESSENGER", address(0)); + _opPortal = vm.envOr("OP_PORTAL", address(0)); + _feesDepositorTarget = vm.envOr("FEES_DEPOSITOR_TARGET", address(0)); + + // Expected values to verify against on-chain state + _expectedMinWithdrawalAmount = vm.envOr("MIN_WITHDRAWAL_AMOUNT", uint256(0)); + _expectedL1WithdrawalRecipient = vm.envOr("L1_WITHDRAWAL_RECIPIENT", address(0)); + _expectedWithdrawalGasLimit = uint32(vm.envOr("WITHDRAWAL_GAS_LIMIT", uint256(0))); + _expectedChainFeesRecipient = vm.envOr("CHAIN_FEES_RECIPIENT", address(0)); + + // Check if all required env vars are set (combined to avoid stack too deep) + _isEnabled = bytes(rpcUrl).length > 0 && bytes(l1RpcUrl).length > 0 && bytes(opRpcUrl).length > 0 + && _portal != address(0) && _l1Messenger != address(0) && _opL1Messenger != address(0) + && _opPortal != address(0) && _feesDepositorTarget != address(0) && _expectedMinWithdrawalAmount != 0 + && _expectedL1WithdrawalRecipient != address(0) && _expectedWithdrawalGasLimit != 0 + && _expectedChainFeesRecipient != address(0); + + if (_isEnabled) { + _mainnetForkId = vm.createFork(l1RpcUrl); + _opMainnetForkId = vm.createFork(opRpcUrl); + _l2ForkId = vm.createFork(rpcUrl); + + // Query RevShare addresses from on-chain state + vm.selectFork(_l2ForkId); + _calculator = IFeeSplitter(FEE_SPLITTER).sharesCalculator(); + require(_calculator != address(0), "FeeSplitter calculator not set - RevShare not configured"); + + _l1Withdrawer = address(ISuperchainRevSharesCalculator(_calculator).shareRecipient()); + require(_l1Withdrawer != address(0), "Calculator shareRecipient not set"); + } + } + + /// @notice Assert the Rev Share contract state on the L2 chain + function test_assertRevShareState() public { + if (!_isEnabled) { + vm.skip(true); + } + vm.selectFork(_l2ForkId); + + _assertL2State( + _l1Withdrawer, + _calculator, + _expectedMinWithdrawalAmount, + _expectedL1WithdrawalRecipient, + _expectedWithdrawalGasLimit, + _expectedChainFeesRecipient + ); + } + + /// @notice Test the withdrawal flow on the L2 chain - tests both below and above threshold paths + // Fund vaults so that: + // - First disburse: share < minWithdrawalAmount (below threshold, no withdrawal) + // - Second disburse: total >= minWithdrawalAmount (triggers withdrawal) + function test_withdrawalFlow() public { + if (!_isEnabled) { + vm.skip(true); + } + + // ==================== PART 1: Below threshold - no withdrawal ==================== + vm.selectFork(_l2ForkId); + + // Fund vaults to get ~half threshold as share + // L1Withdrawer share = netRevenue * 15% = vaultFunding * 3 * 15 / 100 = vaultFunding * 45 / 100 + uint256 firstVaultFunding = (_expectedMinWithdrawalAmount * 100) / 90; + _fundVaults(firstVaultFunding, _l2ForkId); + + // Warp time to allow disbursement + vm.warp(block.timestamp + IFeeSplitter(FEE_SPLITTER).feeDisbursementInterval() + 1); + + // Record L1Withdrawer balance before + uint256 l1WithdrawerBalanceBefore = _l1Withdrawer.balance; + + // Disburse fees - should NOT trigger withdrawal (below threshold) + IFeeSplitter(FEE_SPLITTER).disburseFees(); + + // Verify funds accumulated in L1Withdrawer (no withdrawal triggered) + uint256 l1WithdrawerBalanceAfter = _l1Withdrawer.balance; + uint256 expectedFirstShare = (firstVaultFunding * 3 * 15) / 100; + assertEq( + l1WithdrawerBalanceAfter - l1WithdrawerBalanceBefore, + expectedFirstShare, + "L1Withdrawer should have received expected share" + ); + + // ==================== PART 2: At threshold - withdrawal triggers ==================== + + // Calculate how much more we need to reach the threshold + uint256 remainingToThreshold = _expectedMinWithdrawalAmount - l1WithdrawerBalanceAfter; + // Fund vaults to get at least the remaining amount as share + // share = vaultFunding * 45 / 100, so vaultFunding = share * 100 / 45 + // Round up to ensure we exceed threshold: (a + b - 1) / b + uint256 secondVaultFunding = ((remainingToThreshold * 100) + 44) / 45; + _fundVaults(secondVaultFunding, _l2ForkId); + + // Warp time again + vm.warp(block.timestamp + IFeeSplitter(FEE_SPLITTER).feeDisbursementInterval() + 1); + + // Calculate expected withdrawal amount (current balance + new share) + // share = netRevenue * 15% = vaultFunding * 3 * 15 / 100 + uint256 secondShare = (secondVaultFunding * 3 * 15) / 100; + uint256 expectedWithdrawalAmount = l1WithdrawerBalanceAfter + secondShare; + + _executeDisburseAndAssertWithdrawal( + ChainConfig({ + l1ForkId: _mainnetForkId, + l2ForkId: _l2ForkId, + l1Withdrawer: _l1Withdrawer, + l1WithdrawalRecipient: _expectedL1WithdrawalRecipient, + expectedWithdrawalAmount: expectedWithdrawalAmount, + portal: _portal, + l1Messenger: _l1Messenger, + withdrawalGasLimit: _expectedWithdrawalGasLimit + }), + OPConfig({ + opL2ForkId: _opMainnetForkId, + opL1Messenger: _opL1Messenger, + opPortal: _opPortal, + feesDepositorTarget: _feesDepositorTarget + }) + ); + } +} diff --git a/test/integration/RevShareSetupIntegration.t.sol b/test/integration/RevShareSetupIntegration.t.sol index 6b3f759e8..084a7f230 100644 --- a/test/integration/RevShareSetupIntegration.t.sol +++ b/test/integration/RevShareSetupIntegration.t.sol @@ -34,13 +34,20 @@ contract RevShareSetupIntegrationTest is IntegrationBase { _mainnetForkId = vm.createFork("http://127.0.0.1:8545"); _opMainnetForkId = vm.createFork("http://127.0.0.1:9545"); - // Deploy contracts on L1 - vm.selectFork(_mainnetForkId); - - // Deploy RevShareContractsUpgrader and etch at predetermined address - revShareUpgrader = new RevShareContractsUpgrader(); - vm.etch(REV_SHARE_UPGRADER_ADDRESS, address(revShareUpgrader).code); - revShareUpgrader = RevShareContractsUpgrader(REV_SHARE_UPGRADER_ADDRESS); + // Configure OP Mainnet with production config values + // Values from test/tasks/example/eth/016-revshare-setup/config.toml + l2Chains.push( + L2ChainConfig({ + forkId: _opMainnetForkId, + portal: OP_MAINNET_PORTAL, + l1Messenger: OP_MAINNET_L1_MESSENGER, + minWithdrawalAmount: 2 ether, + l1WithdrawalRecipient: 0xed9B99a703BaD32AC96FDdc313c0652e379251Fd, + withdrawalGasLimit: 800000, + chainFeesRecipient: 0x16A27462B4D61BDD72CbBabd3E43e11791F7A28c, + name: "OP Mainnet" + }) + ); // Deploy RevShareSetup task revShareTask = new RevShareSetup(); @@ -163,42 +170,73 @@ contract RevShareSetupIntegrationTest is IntegrationBase { // Step 2: Execute task simulation revShareTask.simulate("test/tasks/example/eth/016-revshare-setup/config.toml"); - // Step 3: Relay deposit transactions from L1 to OP Mainnet - uint256[] memory forkIds = new uint256[](1); - forkIds[0] = _opMainnetForkId; + // Step 3: Relay deposit transactions from L1 to all L2s + uint256[] memory forkIds = new uint256[](l2Chains.length); + address[] memory portals = new address[](l2Chains.length); - address[] memory portals = new address[](1); - portals[0] = OP_MAINNET_PORTAL; + for (uint256 i = 0; i < l2Chains.length; i++) { + forkIds[i] = l2Chains[i].forkId; + portals[i] = l2Chains[i].portal; + } _relayAllMessages(forkIds, IS_SIMULATE, portals); - // Step 4: Assert the state of the OP Mainnet contracts - vm.selectFork(_opMainnetForkId); - address opL1Withdrawer = - _computeL1WithdrawerAddress(OP_MIN_WITHDRAWAL_AMOUNT, OP_L1_WITHDRAWAL_RECIPIENT, OP_WITHDRAWAL_GAS_LIMIT); - address opRevShareCalculator = _computeRevShareCalculatorAddress(opL1Withdrawer, OP_CHAIN_FEES_RECIPIENT); - _assertL2State( - opL1Withdrawer, - opRevShareCalculator, - OP_MIN_WITHDRAWAL_AMOUNT, - OP_L1_WITHDRAWAL_RECIPIENT, - OP_WITHDRAWAL_GAS_LIMIT, - OP_CHAIN_FEES_RECIPIENT - ); - - // Step 5: Do a withdrawal flow + // Step 4: Assert L2 state for all chains + for (uint256 i = 0; i < l2Chains.length; i++) { + L2ChainConfig memory chain = l2Chains[i]; + + vm.selectFork(chain.forkId); + + address l1Withdrawer = _computeL1WithdrawerAddress( + chain.minWithdrawalAmount, chain.l1WithdrawalRecipient, chain.withdrawalGasLimit + ); + address revShareCalculator = _computeRevShareCalculatorAddress(l1Withdrawer, chain.chainFeesRecipient); + + _assertL2State( + l1Withdrawer, + revShareCalculator, + chain.minWithdrawalAmount, + chain.l1WithdrawalRecipient, + chain.withdrawalGasLimit, + chain.chainFeesRecipient + ); + } - // Fund vaults with amount > minWithdrawalAmount - // It disburses 5 ether to each of the 4 vaults, so total sent is 20 ether - _fundVaults(5 ether, _opMainnetForkId); + // Step 5: Fund vaults for all chains + for (uint256 i = 0; i < l2Chains.length; i++) { + _fundVaults(5 ether, l2Chains[i].forkId); + } - // Disburse fees and expect the L1Withdrawer to trigger the withdrawal + // Step 6: Disburse fees in all chains and assert withdrawals // Expected L1Withdrawer share = 15 ether * 15% = 2.25 ether // It is 15 ether instead of 20 because net revenue doesn't count L1FeeVault's balance // For details on the rev share calculation, check the SuperchainRevSharesCalculator contract. // https://github.com/ethereum-optimism/optimism/blob/f392d4b7e8bc5d1c8d38fcf19c8848764f8bee3b/packages/contracts-bedrock/src/L2/SuperchainRevSharesCalculator.sol#L67-L101 uint256 expectedWithdrawalAmount = 2.25 ether; - _executeDisburseAndAssertWithdrawal(_opMainnetForkId, OP_L1_WITHDRAWAL_RECIPIENT, expectedWithdrawalAmount); + for (uint256 i = 0; i < l2Chains.length; i++) { + L2ChainConfig memory chain = l2Chains[i]; + address l1Withdrawer = _computeL1WithdrawerAddress( + chain.minWithdrawalAmount, chain.l1WithdrawalRecipient, chain.withdrawalGasLimit + ); + _executeDisburseAndAssertWithdrawal( + ChainConfig({ + l1ForkId: _mainnetForkId, + l2ForkId: chain.forkId, + l1Withdrawer: l1Withdrawer, + l1WithdrawalRecipient: chain.l1WithdrawalRecipient, + expectedWithdrawalAmount: expectedWithdrawalAmount, + portal: chain.portal, + l1Messenger: chain.l1Messenger, + withdrawalGasLimit: chain.withdrawalGasLimit + }), + OPConfig({ + opL2ForkId: _opMainnetForkId, + opL1Messenger: OP_MAINNET_L1_MESSENGER, + opPortal: OP_MAINNET_PORTAL, + feesDepositorTarget: OP_MAINNET_FEES_DEPOSITOR_TARGET + }) + ); + } } }