diff --git a/test/integration/IntegrationBase.t.sol b/test/integration/IntegrationBase.t.sol index 3124ce291f..08e2d7bb2a 100644 --- a/test/integration/IntegrationBase.t.sol +++ b/test/integration/IntegrationBase.t.sol @@ -39,14 +39,11 @@ abstract contract IntegrationBase is Test { address internal constant INK_MAINNET_L1_MESSENGER = 0x69d3Cf86B2Bf1a9e99875B7e2D9B6a84426c171f; address internal constant SONEIUM_MAINNET_L1_MESSENGER = 0x9CF951E3F74B644e621b36Ca9cea147a78D4c39f; - // FeesDepositor configuration (triggers deposit to OP Mainnet when balance >= threshold) + // FeesDepositor configuration (triggers deposit to OP when balance >= threshold) uint256 internal constant FEES_DEPOSITOR_THRESHOLD = 2 ether; - // OP Mainnet fees recipient (OPM multisig) - target for FeesDepositor deposits - address internal constant OP_MAINNET_FEES_RECIPIENT = 0x16A27462B4D61BDD72CbBabd3E43e11791F7A28c; - - // Aliased address for L1→L2 message relay - address internal immutable OP_ALIASED_L1_MESSENGER = AddressAliasHelper.applyL1ToL2Alias(OP_MAINNET_L1_MESSENGER); + // 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; @@ -65,10 +62,21 @@ abstract contract IntegrationBase is Test { // 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; - // L2 chain configuration struct + /// @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; @@ -80,6 +88,38 @@ abstract contract IntegrationBase is Test { string name; } + /// @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; @@ -326,97 +366,81 @@ abstract contract IntegrationBase is Test { } /// @notice Execute disburseFees and assert that it triggers a withdrawal with the expected amount - /// @param _l1ForkId The L1 fork ID - /// @param _forkId The fork ID of the L2 chain to test - /// @param _opL2ForkId The OP Mainnet L2 fork ID (for relaying L1→L2 deposits) - /// @param _l1Withdrawer The L1Withdrawer address that emits the WithdrawalInitiated event - /// @param _l1WithdrawalRecipient The expected recipient of the withdrawal - /// @param _expectedWithdrawalAmount The expected withdrawal amount - /// @param _portal The OptimismPortal address for this L2 chain - /// @param _l1Messenger The L1CrossDomainMessenger address for this L2 chain - /// @param _withdrawalGasLimit The gas limit used for L1 withdrawals - function _executeDisburseAndAssertWithdrawal( - uint256 _l1ForkId, - uint256 _forkId, - uint256 _opL2ForkId, - address _l1Withdrawer, - address _l1WithdrawalRecipient, - uint256 _expectedWithdrawalAmount, - address _portal, - address _l1Messenger, - uint32 _withdrawalGasLimit - ) 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 { + vm.selectFork(_chainConfig.l2ForkId); vm.warp(block.timestamp + IFeeSplitter(FEE_SPLITTER).feeDisbursementInterval() + 1); - vm.expectEmit(true, true, true, true, _l1Withdrawer); - emit WithdrawalInitiated(_l1WithdrawalRecipient, _expectedWithdrawalAmount); + vm.expectEmit(true, true, true, true, _chainConfig.l1Withdrawer); + emit WithdrawalInitiated(_chainConfig.l1WithdrawalRecipient, _chainConfig.expectedWithdrawalAmount); IFeeSplitter(FEE_SPLITTER).disburseFees(); // Relay the withdrawal message to L1 - vm.selectFork(_l1ForkId); + vm.selectFork(_chainConfig.l1ForkId); - if (_expectedWithdrawalAmount >= FEES_DEPOSITOR_THRESHOLD) { - // Expect TransactionDeposited event from OP Mainnet Portal + if (_chainConfig.expectedWithdrawalAmount >= FEES_DEPOSITOR_THRESHOLD) { + // 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, OP_MAINNET_PORTAL); + vm.expectEmit(true, true, true, false, _opConfig.opPortal); emit TransactionDeposited( - OP_ALIASED_L1_MESSENGER, // aliased L1CrossDomainMessenger (caller of depositTransaction) + AddressAliasHelper.applyL1ToL2Alias(_opConfig.opL1Messenger), // aliased L1CrossDomainMessenger L2_CROSS_DOMAIN_MESSENGER, // L2 CrossDomainMessenger 0, "" ); _relayL2ToL1Message( - _portal, - _l1Messenger, - _l1Withdrawer, // sender on L2 - _l1WithdrawalRecipient, // target on L1 - _expectedWithdrawalAmount, // value - _withdrawalGasLimit, // minGasLimit + _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) ); - // Now relay the deposit from L1 to OP Mainnet L2 - vm.selectFork(_opL2ForkId); + // Now relay the deposit from L1 to OP L2 + vm.selectFork(_opConfig.opL2ForkId); - uint256 recipientBalanceBefore = OP_MAINNET_FEES_RECIPIENT.balance; + uint256 recipientBalanceBefore = _opConfig.feesDepositorTarget.balance; - // Relay the L1→L2 message (simple ETH transfer to OPM multisig) + // Relay the L1→L2 message (simple ETH transfer to FeesDepositor target) + address aliasedOpL1Messenger = AddressAliasHelper.applyL1ToL2Alias(_opConfig.opL1Messenger); _relayL1ToL2Message( - OP_ALIASED_L1_MESSENGER, - _l1WithdrawalRecipient, // sender (FeesDepositor) - OP_MAINNET_FEES_RECIPIENT, // target (OPM multisig) - _expectedWithdrawalAmount, - 200_000, // gas limit for simple ETH transfer + 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 = OP_MAINNET_FEES_RECIPIENT.balance; + uint256 recipientBalanceAfter = _opConfig.feesDepositorTarget.balance; assertEq( recipientBalanceAfter - recipientBalanceBefore, - _expectedWithdrawalAmount, - "OP Mainnet fees recipient should receive the withdrawal amount" + _chainConfig.expectedWithdrawalAmount, + "FeesDepositor target should receive the withdrawal amount" ); } else { // FeesDepositor holds the ETH (below threshold) - uint256 recipientBalanceBefore = _l1WithdrawalRecipient.balance; + uint256 recipientBalanceBefore = _chainConfig.l1WithdrawalRecipient.balance; _relayL2ToL1Message( - _portal, - _l1Messenger, - _l1Withdrawer, // sender on L2 - _l1WithdrawalRecipient, // target on L1 - _expectedWithdrawalAmount, // value - _withdrawalGasLimit, // minGasLimit + _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 = _l1WithdrawalRecipient.balance; + uint256 recipientBalanceAfter = _chainConfig.l1WithdrawalRecipient.balance; assertEq( recipientBalanceAfter - recipientBalanceBefore, - _expectedWithdrawalAmount, + _chainConfig.expectedWithdrawalAmount, "L1 recipient should receive the withdrawal amount" ); } @@ -424,9 +448,11 @@ abstract contract IntegrationBase is Test { /// @notice Relay a message from L2 to L1 via the CrossDomainMessenger /// @dev This simulates the L2->L1 message relay by: - /// 1. Setting the portal's l2Sender to the L2CrossDomainMessenger - /// 2. Calling relayMessage on the L1CrossDomainMessenger from the portal - /// 3. Resetting the l2Sender back to the default value + /// 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 diff --git a/test/integration/RevShareContractsUpgraderIntegration.t.sol b/test/integration/RevShareContractsUpgraderIntegration.t.sol index 4fb06c8f8a..244bcb14c1 100644 --- a/test/integration/RevShareContractsUpgraderIntegration.t.sol +++ b/test/integration/RevShareContractsUpgraderIntegration.t.sol @@ -107,15 +107,22 @@ contract RevShareContractsUpgraderIntegrationTest is IntegrationBase { chain.minWithdrawalAmount, chain.l1WithdrawalRecipient, chain.withdrawalGasLimit ); _executeDisburseAndAssertWithdrawal( - _mainnetForkId, - chain.forkId, - _opMainnetForkId, - l1Withdrawer, - chain.l1WithdrawalRecipient, - expectedWithdrawalAmount, - chain.portal, - chain.l1Messenger, - chain.withdrawalGasLimit + 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 index c279efc2bd..9fcac89b23 100644 --- a/test/integration/RevSharePostTaskAssertions.t.sol +++ b/test/integration/RevSharePostTaskAssertions.t.sol @@ -2,33 +2,43 @@ 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_MAINNET_RPC_URL: OP Mainnet L2 RPC URL (for L1→L2 relay tests, defaults to RPC_URL) +/// - 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 -/// - MIN_WITHDRAWAL_AMOUNT: Min withdrawal amount for L1Withdrawer -/// - L1_WITHDRAWAL_RECIPIENT: L1 withdrawal recipient address -/// - WITHDRAWAL_GAS_LIMIT: Gas limit for withdrawals -/// - CHAIN_FEES_RECIPIENT: Chain fees recipient address +/// - 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://mainnet.optimism.io" \ -/// L1_RPC_URL="https://eth.llamarpc.com" \ -/// OP_MAINNET_RPC_URL="https://mainnet.optimism.io" \ -/// OPTIMISM_PORTAL="0xbEb5Fc579115071764c7423A4f12eDde41f106Ed" \ -/// L1_MESSENGER="0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1" \ -/// MIN_WITHDRAWAL_AMOUNT="2000000000000000000" \ -/// L1_WITHDRAWAL_RECIPIENT="0xed9B99a703BaD32AC96FDdc313c0652e379251Fd" \ -/// WITHDRAWAL_GAS_LIMIT="800000" \ -/// CHAIN_FEES_RECIPIENT="0x16A27462B4D61BDD72CbBabd3E43e11791F7A28c" \ -/// forge test --match-contract RevSharePostTaskAssertionsTest +/// 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="10000000000000000000" \ +/// L1_WITHDRAWAL_RECIPIENT="0x81c01427DFA9A2512b4EBf1462868856BA4aA91a" \ +/// WITHDRAWAL_GAS_LIMIT="1000000" \ +/// CHAIN_FEES_RECIPIENT="0x455A1115C97cb0E2b24B064C00a9E13872cC37ca" \ +/// forge test --match-contract RevSharePostTaskAssertionsTest -vvv /// ``` contract RevSharePostTaskAssertionsTest is IntegrationBase { // Fork ID @@ -37,10 +47,19 @@ contract RevSharePostTaskAssertionsTest is IntegrationBase { // Chain configuration from env vars address internal _portal; address internal _l1Messenger; - uint256 internal _minWithdrawalAmount; - address internal _l1WithdrawalRecipient; - uint32 internal _withdrawalGasLimit; - address internal _chainFeesRecipient; + 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; @@ -57,30 +76,38 @@ contract RevSharePostTaskAssertionsTest is IntegrationBase { // 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 opMainnetRpcUrl = vm.envOr("OP_MAINNET_RPC_URL", rpcUrl); // Defaults to RPC_URL + string memory opRpcUrl = vm.envOr("OP_RPC_URL", string("")); _portal = vm.envOr("OPTIMISM_PORTAL", address(0)); _l1Messenger = vm.envOr("L1_MESSENGER", address(0)); - _minWithdrawalAmount = vm.envOr("MIN_WITHDRAWAL_AMOUNT", uint256(0)); - _l1WithdrawalRecipient = vm.envOr("L1_WITHDRAWAL_RECIPIENT", address(0)); - _withdrawalGasLimit = uint32(vm.envOr("WITHDRAWAL_GAS_LIMIT", uint256(0))); - _chainFeesRecipient = vm.envOr("CHAIN_FEES_RECIPIENT", address(0)); - - // Check if all required env vars are set - bool hasRpcUrl = bytes(rpcUrl).length > 0; - bool hasL1RpcUrl = bytes(l1RpcUrl).length > 0; - bool hasPortal = _portal != address(0); - bool hasL1Messenger = _l1Messenger != address(0); - bool hasL1WithdrawalRecipient = _l1WithdrawalRecipient != address(0); - bool hasWithdrawalGasLimit = _withdrawalGasLimit != 0; - bool hasChainFeesRecipient = _chainFeesRecipient != address(0); - - _isEnabled = hasRpcUrl && hasL1RpcUrl && hasPortal && hasL1Messenger && hasL1WithdrawalRecipient - && hasWithdrawalGasLimit && hasChainFeesRecipient; + _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(opMainnetRpcUrl); + _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"); } } @@ -88,46 +115,82 @@ contract RevSharePostTaskAssertionsTest is IntegrationBase { function test_assertRevShareState() public onlyIfEnabled { vm.selectFork(_l2ForkId); - address l1Withdrawer = - _computeL1WithdrawerAddress(_minWithdrawalAmount, _l1WithdrawalRecipient, _withdrawalGasLimit); - address revShareCalculator = _computeRevShareCalculatorAddress(l1Withdrawer, _chainFeesRecipient); - _assertL2State( - l1Withdrawer, - revShareCalculator, - _minWithdrawalAmount, - _l1WithdrawalRecipient, - _withdrawalGasLimit, - _chainFeesRecipient + _l1Withdrawer, + _calculator, + _expectedMinWithdrawalAmount, + _expectedL1WithdrawalRecipient, + _expectedWithdrawalGasLimit, + _expectedChainFeesRecipient ); } - /// @notice Test the withdrawal flow on the L2 chain + /// @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 onlyIfEnabled { - // Fund vaults - _fundVaults(1 ether, _l2ForkId); + // ==================== 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); - // Compute L1Withdrawer address - address l1Withdrawer = - _computeL1WithdrawerAddress(_minWithdrawalAmount, _l1WithdrawalRecipient, _withdrawalGasLimit); + // Warp time again + vm.warp(block.timestamp + IFeeSplitter(FEE_SPLITTER).feeDisbursementInterval() + 1); - // Disburse fees and assert withdrawal - // Expected L1Withdrawer share = 3 ether * 15% = 0.45 ether - // It is 3 ether instead of 4 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 = 0.45 ether; + // 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( - _mainnetForkId, - _l2ForkId, - _opMainnetForkId, - l1Withdrawer, - _l1WithdrawalRecipient, - expectedWithdrawalAmount, - _portal, - _l1Messenger, - _withdrawalGasLimit + 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 80b9424df2..084a7f230c 100644 --- a/test/integration/RevShareSetupIntegration.t.sol +++ b/test/integration/RevShareSetupIntegration.t.sol @@ -220,15 +220,22 @@ contract RevShareSetupIntegrationTest is IntegrationBase { chain.minWithdrawalAmount, chain.l1WithdrawalRecipient, chain.withdrawalGasLimit ); _executeDisburseAndAssertWithdrawal( - _mainnetForkId, - chain.forkId, - _opMainnetForkId, - l1Withdrawer, - chain.l1WithdrawalRecipient, - expectedWithdrawalAmount, - chain.portal, - chain.l1Messenger, - chain.withdrawalGasLimit + 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 + }) ); } }