diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7d18ff44bb..5cbb02060d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ * @ethereum-optimism/evm-safety -/runbooks @ethereum-optimism/devrel @ethereum-optimism/evm-safety - +/runbooks @ethereum-optimism/solutions @ethereum-optimism/evm-safety /src @ethereum-optimism/contract-reviewers @ethereum-optimism/evm-safety +/src/tasks/sep @ethereum-optimism/solutions diff --git a/src/RevShareContractsUpgrader.sol b/src/RevShareContractsUpgrader.sol index debfbbb94e..8d8dbe6bd3 100644 --- a/src/RevShareContractsUpgrader.sol +++ b/src/RevShareContractsUpgrader.sol @@ -100,7 +100,7 @@ contract RevShareContractsUpgrader { RevShareCommon.depositCall( config.portal, RevShareCommon.FEE_SPLITTER, - FeeVaultUpgrader.SETTERS_GAS_LIMIT, + RevShareCommon.SETTERS_GAS_LIMIT, abi.encodeCall(IFeeSplitter.setSharesCalculator, (calculator)) ); diff --git a/src/libraries/FeeVaultUpgrader.sol b/src/libraries/FeeVaultUpgrader.sol index b6a3bb93df..fa919b377d 100644 --- a/src/libraries/FeeVaultUpgrader.sol +++ b/src/libraries/FeeVaultUpgrader.sol @@ -25,9 +25,6 @@ library FeeVaultUpgrader { /// @notice The gas limit for the Fee Vaults deployment. uint64 internal constant FEE_VAULTS_DEPLOYMENT_GAS_LIMIT = 1_200_000; - /// @notice The gas limit for the Fee Vaults setters. - uint64 internal constant SETTERS_GAS_LIMIT = 50_000; - /// @notice Address of the Operator Fee Vault Predeploy on L2. address internal constant OPERATOR_FEE_VAULT = 0x420000000000000000000000000000000000001b; @@ -103,16 +100,19 @@ library FeeVaultUpgrader { RevShareCommon.depositCall( _portal, vaults[i], - SETTERS_GAS_LIMIT, + RevShareCommon.SETTERS_GAS_LIMIT, abi.encodeCall(IFeeVault.setRecipient, (RevShareCommon.FEE_SPLITTER)) ); RevShareCommon.depositCall( - _portal, vaults[i], SETTERS_GAS_LIMIT, abi.encodeCall(IFeeVault.setMinWithdrawalAmount, (0)) + _portal, + vaults[i], + RevShareCommon.SETTERS_GAS_LIMIT, + abi.encodeCall(IFeeVault.setMinWithdrawalAmount, (0)) ); RevShareCommon.depositCall( _portal, vaults[i], - SETTERS_GAS_LIMIT, + RevShareCommon.SETTERS_GAS_LIMIT, abi.encodeCall(IFeeVault.setWithdrawalNetwork, (IFeeVault.WithdrawalNetwork.L2)) ); } diff --git a/src/libraries/RevShareCommon.sol b/src/libraries/RevShareCommon.sol index bab19b1e22..10a473f6da 100644 --- a/src/libraries/RevShareCommon.sol +++ b/src/libraries/RevShareCommon.sol @@ -19,6 +19,9 @@ library RevShareCommon { /// @notice The gas limit for the upgrade calls on L2. uint64 internal constant UPGRADE_GAS_LIMIT = 150_000; + /// @notice The gas limit for setter calls on L2. + uint64 internal constant SETTERS_GAS_LIMIT = 75_000; + /// @notice The salt prefix for the RevShare system. string internal constant SALT_SEED = "RevShare"; diff --git a/src/script/check-task-statuses.sh b/src/script/check-task-statuses.sh index b027209abb..5656357cae 100755 --- a/src/script/check-task-statuses.sh +++ b/src/script/check-task-statuses.sh @@ -1,7 +1,7 @@ #!/bin/bash set -euo pipefail -VALID_STATUSES=("DRAFT, NOT READY TO SIGN" "CONTINGENCY TASK, SIGN AS NEEDED" "READY TO SIGN" "SIGNED" "EXECUTED" "CANCELLED") +VALID_STATUSES=("DRAFT, NOT READY TO SIGN" "CONTINGENCY TASK, SIGN AS NEEDED" "READY TO SIGN" "SIGNED" "EXECUTED" "CANCELLED" "APPROVED") errors=() # We collect all errors then print them at the end. # Function to check status and hyperlinks for a single file. diff --git a/src/script/fetch-tasks.sh b/src/script/fetch-tasks.sh index 435c45ae11..fa1adb27d1 100755 --- a/src/script/fetch-tasks.sh +++ b/src/script/fetch-tasks.sh @@ -16,8 +16,8 @@ check_status() { # Extract the status line status_line=$(awk '/^Status: /{print; exit}' "$file_path") - # If status is not EXECUTED or CANCELLED, add to tasks to run - if [[ "$status_line" != *"EXECUTED"* ]] && [[ "$status_line" != *"CANCELLED"* ]]; then + # If status is not EXECUTED, CANCELLED, or APPROVED, add to tasks to run + if [[ "$status_line" != *"EXECUTED"* ]] && [[ "$status_line" != *"CANCELLED"* ]] && [[ "$status_line" != *"APPROVED"* ]]; then # Get the task directory path task_dir=$(dirname "$file_path") # Add to array if config.toml exists diff --git a/src/tasks/StackedSimulator.sol b/src/tasks/StackedSimulator.sol index 8416f54fe7..27c2f3fdb1 100644 --- a/src/tasks/StackedSimulator.sol +++ b/src/tasks/StackedSimulator.sol @@ -46,8 +46,8 @@ contract StackedSimulator is Script { _simulateStack(network, task, new address[](0)); } - /// @notice Simulates the execution of all non-terminal tasks for a given network. No gas metering is used. - function simulateStack(string memory network) public noGasMetering { + /// @notice Simulates the execution of all non-terminal tasks for a given network. + function simulateStack(string memory network) public { TaskInfo[] memory tasks = getNonTerminalTasks(network); if (tasks.length == 0) { console.log("No non-terminal tasks found for network: %s", network); diff --git a/src/tasks/eth/036-U17-main-base/README.md b/src/tasks/eth/036-U17-main-base/README.md index a9d62d0d66..c887a85ba1 100644 --- a/src/tasks/eth/036-U17-main-base/README.md +++ b/src/tasks/eth/036-U17-main-base/README.md @@ -1,6 +1,6 @@ # 036-U17-main-base: Upgrades Base Mainnet to `op-contracts/v5.0.0` (i.e. U17) -Status: [READY TO SIGN] +Status: [EXECUTED](https://etherscan.io/tx/0x9b9aa2d8e857e1a28e55b124e931eac706b3ae04c1b33ba949f0366359860993) ## Objective diff --git a/src/tasks/sep/046-worldchain-l2pao-key-handback-over/.env b/src/tasks/sep/046-worldchain-l2pao-key-handback-over/.env new file mode 100644 index 0000000000..fe89703585 --- /dev/null +++ b/src/tasks/sep/046-worldchain-l2pao-key-handback-over/.env @@ -0,0 +1 @@ +TENDERLY_GAS=16700000 diff --git a/src/tasks/sep/046-worldchain-l2pao-key-handback-over/README.md b/src/tasks/sep/046-worldchain-l2pao-key-handback-over/README.md new file mode 100644 index 0000000000..3cd31df6d4 --- /dev/null +++ b/src/tasks/sep/046-worldchain-l2pao-key-handback-over/README.md @@ -0,0 +1,21 @@ +# 046-worldchain-l2pao-key-handback-over + +Status: [EXECUTED](https://sepolia.etherscan.io/tx/0x28f74a6314e34f4d94e35ca9194656044d259855b014da943db3ab4cc6a64dcc) + +## Objective + +Transfer the L2 ProxyAdmin Owner for Worldchain Sepolia to Alchemy-controlled EOA. + +## Simulation & Signing + +Simulation commands for each safe: +```bash +cd src/tasks/sep/046-worldchain-l2pao-key-handback-over +SIMULATE_WITHOUT_LEDGER=1 SKIP_DECODE_AND_PRINT=1 just --dotenv-path $(pwd)/.env simulate +``` + +Signing commands for each safe: +```bash +cd src/tasks/sep/046-worldchain-l2pao-key-handback-over +SKIP_DECODE_AND_PRINT=1 just --dotenv-path $(pwd)/.env sign +``` diff --git a/src/tasks/sep/046-worldchain-l2pao-key-handback-over/VALIDATION.md b/src/tasks/sep/046-worldchain-l2pao-key-handback-over/VALIDATION.md new file mode 100644 index 0000000000..a7ebc9d085 --- /dev/null +++ b/src/tasks/sep/046-worldchain-l2pao-key-handback-over/VALIDATION.md @@ -0,0 +1,87 @@ +# Validation + +This document can be used to validate the inputs and result of the execution of the upgrade transaction which you are signing. + +The steps are: +1. [Validate the Domain and Message Hashes](#expected-domain-and-message-hashes) +2. [Verifying the state changes via the normalized state diff hash](#normalized-state-diff-hash-attestation) +3. [Verifying the transaction input](#understanding-task-calldata) +4. [Verifying the state changes](#task-state-changes) + +## Expected Domain and Message Hashes + +First, we need to validate the domain and message hashes. These values should match both the values on your ledger and the values printed to the terminal when you run the task. + +> [!CAUTION] +> +> Before signing, ensure the below hashes match what is on your ledger. +> +> ### Standard L2 Proxy Admin Owner (Unaliased) + ### Worldchain has their L2PAO transferred to the standard address but retained control of their L1PAO +(`0x1Eb2fFc903729a0F03966B917003800b145F56E2`) + +>### Security Council Safe (`0xf64bc17485f0B4Ea5F06A96514182FC4cB561977`) +> +> - Domain Hash: `0xbe081970e9fc104bd1ea27e375cd21ec7bb1eec56bfe43347c3e36c5d27b8533` +> - Message Hash: `0x90174dd31d5ba63b20aa1e5df91fe03c6166a323a17dcea11d6e5e92b744033e` +> +> ### Foundation Safe (`0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B`) +> +> - Domain Hash: `0x37e1f5dd3b92a004a23589b741196c8a214629d4ea3a690ec8e41ae45c689cbb` +> - Message Hash: `0x3156bd84f93074952b43af3368d798328835200f04aa6c6c092cc9fcf0af4d79` + + +## Understanding Task Calldata + +The transaction initiates a deposit transaction via the OptimismPortal on L1 Sepolia, which will be executed on L2 (Worldchain Sepolia) to transfer the L2 ProxyAdmin ownership to an EOA. + +### Decoding the depositTransaction call: +```bash +# The outer multicall to OptimismPortal +cast calldata-decode "depositTransaction(address,uint256,uint64,bool,bytes)" \ + 0xe9e05c42000000000000000000000000420000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024f2fde38b000000000000000000000000e78a0a96c5d6ae6c606418ed4a9ced378cb030a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +``` + +Returns: +- `_to`: `0x4200000000000000000000000000000000000018` (L2 ProxyAdmin predeploy) +- `_value`: `0` (no ETH sent) +- `_gasLimit`: `200000` (gas for L2 execution) +- `_isCreation`: `false` (not a contract creation) +- `_data`: `0xf2fde38b000000000000000000000000e78a0a96c5d6ae6c606418ed4a9ced378cb030a0` + +### Decoding the inner transferOwnership call: +```bash +cast calldata-decode "transferOwnership(address)" \ + 0xf2fde38b000000000000000000000000e78a0a96c5d6ae6c606418ed4a9ced378cb030a0 +``` + +Returns: +- `newOwnerEOA`: `0xe78a0A96C5D6aE6C606418ED4A9Ced378cb030A0` (the target EOA) + +# State Validations + +For a complete walkthrough of validating the state changes on L1 and L2 follow [the steps in this doc](https://github.com/ethereum-optimism/superchain-ops/blob/main/src/doc/simulate-l2-ownership-transfer.md) + +## Manual L2 Verification Steps + +After the L1 transaction is executed, you must verify that the L2 deposit transaction successfully transfers ownership: + +1. **Find the L2 deposit transaction**: Look for a transaction on Worldchain Sepolia from the L1 caller to the L2 ProxyAdmin at `0x4200000000000000000000000000000000000018`. + +2. **Verify the OwnershipTransferred event**: Confirm that the event shows: + - `previousOwner`: `0x2FC3ffc903729a0f03966b917003800B145F67F3` (aliased 2/2 safe) + - `newOwnerEOA`: `0xe78a0A96C5D6aE6C606418ED4A9Ced378cb030A0` (target EOA) + +3. **Verify final state**: Call `owner()` on the L2 ProxyAdmin to confirm it returns `0xe78a0A96C5D6aE6C606418ED4A9Ced378cb030A0`. + +```bash +# After L2 execution, verify the new owner +cast call 0x4200000000000000000000000000000000000018 "owner()(address)" --rpc-url worldchain-sepolia +# Should return: 0xe78a0A96C5D6aE6C606418ED4A9Ced378cb030A0 +``` + +## Task Calldata + +``` +0x174dea71000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000ff6eba109271fe6d4237eeed4bab1dd9a77dd1a40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000104e9e05c42000000000000000000000000420000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024f2fde38b000000000000000000000000e78a0a96c5d6ae6c606418ed4a9ced378cb030a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +``` \ No newline at end of file diff --git a/src/tasks/sep/046-worldchain-l2pao-key-handback-over/config.toml b/src/tasks/sep/046-worldchain-l2pao-key-handback-over/config.toml new file mode 100644 index 0000000000..bb7337fb93 --- /dev/null +++ b/src/tasks/sep/046-worldchain-l2pao-key-handback-over/config.toml @@ -0,0 +1,24 @@ +l2chains = [{name = "Worldchain Sepolia", chainId = 4801}] +templateName = "TransferL2PAOFromL1ToEOA" + +# The new owner address (EOA). See here https://www.notion.so/oplabs/Worldchain-key-handback-over-address-validation-272f153ee1628002bfa2e00a718c57d5?source=copy_link +newOwnerEOA = "0xe78a0A96C5D6aE6C606418ED4A9Ced378cb030A0" + +[addresses] +ProxyAdminOwner = "0x1Eb2fFc903729a0F03966B917003800b145F56E2" + +[stateOverrides] +# ProxyAdminOwner +0x1Eb2fFc903729a0F03966B917003800b145F56E2 = [ + {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 44} +] + +# Foundation Upgrades Safe +0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B = [ + {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 62} +] + +# Security Council +0xf64bc17485f0B4Ea5F06A96514182FC4cB561977 = [ + {key = "0x0000000000000000000000000000000000000000000000000000000000000005", value = 57} +] diff --git a/src/template/MigrateToLiveness2.sol b/src/template/MigrateToLiveness2.sol new file mode 100644 index 0000000000..fbb1468c17 --- /dev/null +++ b/src/template/MigrateToLiveness2.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {SimpleTaskBase} from "src/tasks/types/SimpleTaskBase.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {stdToml} from "forge-std/StdToml.sol"; +import {LibString} from "@solady/utils/LibString.sol"; +import {Action, TemplateConfig, TaskType, TaskPayload, SafeData} from "src/libraries/MultisigTypes.sol"; + +interface ISaferSafes { + struct ModuleConfig { + uint256 livenessResponsePeriod; + address fallbackOwner; + } + + function enableModule(address _module) external; + function setGuard(address _guard) external; + function configureTimelockGuard(uint256 _timelockDelay) external; + function configureLivenessModule(ModuleConfig memory _moduleConfig) external; + function version() external view returns (string memory); + function getModulesPaginated(address start, uint256 pageSize) + external + view + returns (address[] memory array, address next); + function disableModule(address prevModule, address module) external; + function isModuleEnabled(address module) external view returns (bool); + function getGuard() external view returns (address); + function livenessSafeConfiguration(address safe) external view returns (ModuleConfig memory); +} + +interface IMultisig { + function version() external view returns (string memory); +} + +contract MigrateToLiveness2 is SimpleTaskBase { + using stdToml for string; + using LibString for string; + + address public saferSafes; + address public multisig; + address public currentLivenessModule; + + uint256 public timelockDelay; + uint256 public livenessResponsePeriod; + address public fallbackOwner; + + function _taskStorageWrites() internal pure override returns (string[] memory) { + string[] memory writes = new string[](2); + writes[0] = "targetSafe"; // Safe being modified (enableModule, etc.) + writes[1] = "saferSafes"; // SaferSafes contract (configureLivenessModule) + return writes; + } + + function _getCodeExceptions() internal view override returns (address[] memory) {} + + function safeAddressString() public pure override returns (string memory) { + return "targetSafe"; // References the custom safe from config.toml + } + + /// @notice Find the previous module in the linked list + /// @param moduleToFind The module to find the previous module for + /// @return The address of the previous module in the linked list + function _findPrevModule(address moduleToFind) internal view returns (address) { + address SENTINEL_MODULES = address(0x1); + + (address[] memory modules,) = ISaferSafes(multisig).getModulesPaginated(SENTINEL_MODULES, 100); + + // If the module is the first in the list, previous is sentinel + if (modules.length > 0 && modules[0] == moduleToFind) { + return SENTINEL_MODULES; + } + + // Otherwise, find the module and return the previous one + for (uint256 i = 1; i < modules.length; i++) { + if (modules[i] == moduleToFind) { + return modules[i - 1]; + } + } + + revert("Module not found in list"); + } + + function _templateSetup(string memory taskConfigFilePath, address rootSafe) internal override { + super._templateSetup(taskConfigFilePath, rootSafe); + string memory tomlContent = vm.readFile(taskConfigFilePath); + + saferSafes = tomlContent.readAddress(".addresses.saferSafes"); + multisig = tomlContent.readAddress(".addresses.targetSafe"); + currentLivenessModule = tomlContent.readAddress(".addresses.currentLivenessModule"); + + livenessResponsePeriod = tomlContent.readUint(".livenessModule.livenessResponsePeriod"); + fallbackOwner = tomlContent.readAddress(".livenessModule.fallbackOwner"); + + require(address(saferSafes).code.length > 0, "SaferSafes does not have code"); + require(address(currentLivenessModule).code.length > 0, "Current LivenessModule does not have code"); + require(livenessResponsePeriod > 0, "Liveness response period must be greater than 0"); + } + + function _build(address) internal override { + // Remove the guard first so it doesn't interfere with subsequent operations + ISaferSafes(multisig).setGuard(address(0)); + + // Enable SaferSafes as a module on the safe + ISaferSafes(multisig).enableModule(saferSafes); + + // Configure the liveness module on SaferSafes + ISaferSafes.ModuleConfig memory moduleConfig = + ISaferSafes.ModuleConfig({livenessResponsePeriod: livenessResponsePeriod, fallbackOwner: fallbackOwner}); + ISaferSafes(saferSafes).configureLivenessModule(moduleConfig); + + // Remove the old liveness module + address prevModule = _findPrevModule(currentLivenessModule); + ISaferSafes(multisig).disableModule(prevModule, currentLivenessModule); + } + + function _validate(VmSafe.AccountAccess[] memory, Action[] memory, address) internal view override { + require( + ISaferSafes(multisig).isModuleEnabled(saferSafes), "Validation failed: SaferSafes module is not enabled" + ); + + require( + !ISaferSafes(multisig).isModuleEnabled(currentLivenessModule), + "Validation failed: Old liveness module is still enabled" + ); + + bytes32 guardSlot = 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8; + address guardAddress; + bytes32 value = vm.load(multisig, guardSlot); + assembly { + guardAddress := value + } + require(guardAddress == address(0), "Validation failed: Guard was not removed"); + + ISaferSafes.ModuleConfig memory config = ISaferSafes(saferSafes).livenessSafeConfiguration(multisig); + + require( + config.livenessResponsePeriod == livenessResponsePeriod, + "Validation failed: Liveness response period mismatch" + ); + + require(config.fallbackOwner == fallbackOwner, "Validation failed: Fallback owner mismatch"); + } +} diff --git a/src/template/RevShareSetup.sol b/src/template/RevShareSetup.sol new file mode 100644 index 0000000000..b44b71f2d5 --- /dev/null +++ b/src/template/RevShareSetup.sol @@ -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" + ); + + // 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)) + ); + } + + // 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); + } +} diff --git a/test/integration/IntegrationBase.t.sol b/test/integration/IntegrationBase.t.sol index aa50fb08d3..9e6757659e 100644 --- a/test/integration/IntegrationBase.t.sol +++ b/test/integration/IntegrationBase.t.sol @@ -8,10 +8,61 @@ import {AddressAliasHelper} from "@eth-optimism-bedrock/src/vendor/AddressAliasH 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 {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; + 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; + + // 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; + + // 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 = 800000; + address internal constant INK_CHAIN_FEES_RECIPIENT = 0x0000000000000000000000000000000000000002; + + // Test configuration - Soneium Mainnet + uint256 internal constant SONEIUM_MIN_WITHDRAWAL_AMOUNT = 500000; + address internal constant SONEIUM_L1_WITHDRAWAL_RECIPIENT = 0x0000000000000000000000000000000000000003; + uint32 internal constant SONEIUM_WITHDRAWAL_GAS_LIMIT = 800000; + address internal constant SONEIUM_CHAIN_FEES_RECIPIENT = 0x0000000000000000000000000000000000000003; + + 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. @@ -20,6 +71,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"); @@ -152,4 +204,128 @@ 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 + + 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); + } } diff --git a/test/integration/RevShareContractsUpgraderIntegration.t.sol b/test/integration/RevShareContractsUpgraderIntegration.t.sol index d6626b6c93..904ee507f3 100644 --- a/test/integration/RevShareContractsUpgraderIntegration.t.sol +++ b/test/integration/RevShareContractsUpgraderIntegration.t.sol @@ -4,65 +4,10 @@ pragma solidity 0.8.15; import {RevShareContractsUpgrader} from "src/RevShareContractsUpgrader.sol"; import {RevShareUpgradeAndSetup} from "src/template/RevShareUpgradeAndSetup.sol"; import {IntegrationBase} from "./IntegrationBase.t.sol"; -import {Predeploys} from "@eth-optimism-bedrock/src/libraries/Predeploys.sol"; -import {Test} from "forge-std/Test.sol"; - -// Interfaces -import {IOptimismPortal2} from "@eth-optimism-bedrock/interfaces/L1/IOptimismPortal2.sol"; -import {IProxyAdmin} from "@eth-optimism-bedrock/interfaces/universal/IProxyAdmin.sol"; -import {ICreate2Deployer} from "src/interfaces/ICreate2Deployer.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"; contract RevShareContractsUpgraderIntegrationTest is IntegrationBase { - RevShareContractsUpgrader public revShareUpgrader; RevShareUpgradeAndSetup public revShareTask; - // Events for testing - event WithdrawalInitiated(address indexed recipient, uint256 amount); - - // Fork IDs - uint256 internal _mainnetForkId; - uint256 internal _opMainnetForkId; - uint256 internal _inkMainnetForkId; - uint256 internal _soneiumMainnetForkId; - - // 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; - - // 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; - - // 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 = 800000; - address internal constant INK_CHAIN_FEES_RECIPIENT = 0x0000000000000000000000000000000000000002; - - // Test configuration - Soneium Mainnet - uint256 internal constant SONEIUM_MIN_WITHDRAWAL_AMOUNT = 500000; - address internal constant SONEIUM_L1_WITHDRAWAL_RECIPIENT = 0x0000000000000000000000000000000000000003; - uint32 internal constant SONEIUM_WITHDRAWAL_GAS_LIMIT = 800000; - address internal constant SONEIUM_CHAIN_FEES_RECIPIENT = 0x0000000000000000000000000000000000000003; - - bool internal constant IS_SIMULATE = true; - function setUp() public { // Create forks for L1 (mainnet) and L2s _mainnetForkId = vm.createFork("http://127.0.0.1:8545"); @@ -84,7 +29,7 @@ contract RevShareContractsUpgraderIntegrationTest is IntegrationBase { /// @notice Test the integration of upgradeAndSetupRevShare function test_upgradeAndSetupRevShare_integration() public { - // Step 1: Record logs for L1→L2 message replay + // Step 1: Record logs for L1→L2 message relay vm.recordLogs(); // Step 2: Execute task simulation @@ -168,123 +113,4 @@ contract RevShareContractsUpgraderIntegrationTest is IntegrationBase { _soneiumMainnetForkId, SONEIUM_L1_WITHDRAWAL_RECIPIENT, expectedWithdrawalAmount ); } - - 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 _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); - } } diff --git a/test/integration/RevShareSetupIntegration.t.sol b/test/integration/RevShareSetupIntegration.t.sol new file mode 100644 index 0000000000..13f10477a5 --- /dev/null +++ b/test/integration/RevShareSetupIntegration.t.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {RevShareContractsUpgrader} from "src/RevShareContractsUpgrader.sol"; +import {RevShareSetup} from "src/template/RevShareSetup.sol"; +import {IntegrationBase} from "./IntegrationBase.t.sol"; +import {FeeVaultUpgrader} from "src/libraries/FeeVaultUpgrader.sol"; +import {FeeSplitterSetup} from "src/libraries/FeeSplitterSetup.sol"; +import {RevShareCommon} from "src/libraries/RevShareCommon.sol"; +import {Proxy} from "@eth-optimism-bedrock/src/universal/Proxy.sol"; + +contract RevShareSetupIntegrationTest is IntegrationBase { + RevShareSetup public revShareTask; + + // EIP-1967 storage slots for proxy (specific to RevShareSetup test) + bytes32 internal constant PROXY_IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + bytes32 internal constant PROXY_OWNER_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + // Creation codes from libraries (specific to RevShareSetup test) + bytes internal OPERATOR_FEE_VAULT_CREATION_CODE = FeeVaultUpgrader.operatorFeeVaultCreationCode; + bytes internal SEQUENCER_FEE_VAULT_CREATION_CODE = FeeVaultUpgrader.sequencerFeeVaultCreationCode; + bytes internal DEFAULT_FEE_VAULT_CREATION_CODE = FeeVaultUpgrader.defaultFeeVaultCreationCode; + bytes internal FEE_SPLITTER_CREATION_CODE = FeeSplitterSetup.feeSplitterCreationCode; + + // Implementation addresses (deployed and etched in setUp) + address internal _operatorFeeVaultImpl; + address internal _sequencerFeeVaultImpl; + address internal _defaultFeeVaultImpl; + address internal _feeSplitterImpl; + + function setUp() public { + // Create forks for L1 (mainnet) and L2s (OP Mainnet, Ink, Soneium) + _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); + + // 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); + + // Deploy RevShareSetup task + revShareTask = new RevShareSetup(); + + // Deploy implementations once to get their addresses and bytecode + _operatorFeeVaultImpl = _deployFromCreationCode(OPERATOR_FEE_VAULT_CREATION_CODE); + _sequencerFeeVaultImpl = _deployFromCreationCode(SEQUENCER_FEE_VAULT_CREATION_CODE); + _defaultFeeVaultImpl = _deployFromCreationCode(DEFAULT_FEE_VAULT_CREATION_CODE); + _feeSplitterImpl = _deployFromCreationCode(FEE_SPLITTER_CREATION_CODE); + + // Get implementation bytecodes + bytes memory operatorFeeVaultImplCode = _operatorFeeVaultImpl.code; + bytes memory sequencerFeeVaultImplCode = _sequencerFeeVaultImpl.code; + bytes memory defaultFeeVaultImplCode = _defaultFeeVaultImpl.code; + bytes memory feeSplitterImplCode = _feeSplitterImpl.code; + + // Deploy a proxy to get its bytecode + Proxy proxyTemplate = new Proxy(address(this)); + bytes memory proxyCode = address(proxyTemplate).code; + + // Etch predeploys on OP Mainnet fork + vm.selectFork(_opMainnetForkId); + _etchImplementations( + _operatorFeeVaultImpl, + _sequencerFeeVaultImpl, + _defaultFeeVaultImpl, + _feeSplitterImpl, + operatorFeeVaultImplCode, + sequencerFeeVaultImplCode, + defaultFeeVaultImplCode, + feeSplitterImplCode + ); + _setupProxyPredeploys( + proxyCode, _operatorFeeVaultImpl, _sequencerFeeVaultImpl, _defaultFeeVaultImpl, _feeSplitterImpl + ); + + // Etch predeploys on Ink Mainnet fork + vm.selectFork(_inkMainnetForkId); + _etchImplementations( + _operatorFeeVaultImpl, + _sequencerFeeVaultImpl, + _defaultFeeVaultImpl, + _feeSplitterImpl, + operatorFeeVaultImplCode, + sequencerFeeVaultImplCode, + defaultFeeVaultImplCode, + feeSplitterImplCode + ); + _setupProxyPredeploys( + proxyCode, _operatorFeeVaultImpl, _sequencerFeeVaultImpl, _defaultFeeVaultImpl, _feeSplitterImpl + ); + + // Etch predeploys on Soneium Mainnet fork + vm.selectFork(_soneiumMainnetForkId); + _etchImplementations( + _operatorFeeVaultImpl, + _sequencerFeeVaultImpl, + _defaultFeeVaultImpl, + _feeSplitterImpl, + operatorFeeVaultImplCode, + sequencerFeeVaultImplCode, + defaultFeeVaultImplCode, + feeSplitterImplCode + ); + _setupProxyPredeploys( + proxyCode, _operatorFeeVaultImpl, _sequencerFeeVaultImpl, _defaultFeeVaultImpl, _feeSplitterImpl + ); + + // Switch back to mainnet fork after setup + vm.selectFork(_mainnetForkId); + } + + /// @notice Deploy a contract from creation code + /// @param _creationCode The creation code of the contract to deploy + /// @return deployed The address of the deployed contract + function _deployFromCreationCode(bytes memory _creationCode) internal returns (address deployed) { + assembly { + deployed := create(0, add(_creationCode, 0x20), mload(_creationCode)) + } + require(deployed != address(0), "Deployment failed"); + } + + /// @notice Etch implementation bytecode at addresses on the current fork + /// @param operatorFeeVaultImplAddr OperatorFeeVault implementation address + /// @param sequencerFeeVaultImplAddr SequencerFeeVault implementation address + /// @param defaultFeeVaultImplAddr Default FeeVault implementation address + /// @param feeSplitterImplAddr FeeSplitter implementation address + /// @param operatorFeeVaultImplCode OperatorFeeVault implementation bytecode + /// @param sequencerFeeVaultImplCode SequencerFeeVault implementation bytecode + /// @param defaultFeeVaultImplCode Default FeeVault implementation bytecode + /// @param feeSplitterImplCode FeeSplitter implementation bytecode + function _etchImplementations( + address operatorFeeVaultImplAddr, + address sequencerFeeVaultImplAddr, + address defaultFeeVaultImplAddr, + address feeSplitterImplAddr, + bytes memory operatorFeeVaultImplCode, + bytes memory sequencerFeeVaultImplCode, + bytes memory defaultFeeVaultImplCode, + bytes memory feeSplitterImplCode + ) internal { + vm.etch(operatorFeeVaultImplAddr, operatorFeeVaultImplCode); + vm.etch(sequencerFeeVaultImplAddr, sequencerFeeVaultImplCode); + vm.etch(defaultFeeVaultImplAddr, defaultFeeVaultImplCode); + vm.etch(feeSplitterImplAddr, feeSplitterImplCode); + } + + /// @notice Setup proxy predeploys pointing to implementations + /// @param proxyCode Proxy runtime bytecode + /// @param operatorFeeVaultImplAddr OperatorFeeVault implementation address + /// @param sequencerFeeVaultImplAddr SequencerFeeVault implementation address + /// @param defaultFeeVaultImplAddr Default FeeVault implementation address (for Base and L1) + /// @param feeSplitterImplAddr FeeSplitter implementation address + function _setupProxyPredeploys( + bytes memory proxyCode, + address operatorFeeVaultImplAddr, + address sequencerFeeVaultImplAddr, + address defaultFeeVaultImplAddr, + address feeSplitterImplAddr + ) internal { + // Setup OperatorFeeVault proxy + vm.etch(OPERATOR_FEE_VAULT, proxyCode); + vm.store(OPERATOR_FEE_VAULT, PROXY_IMPLEMENTATION_SLOT, bytes32(uint256(uint160(operatorFeeVaultImplAddr)))); + vm.store(OPERATOR_FEE_VAULT, PROXY_OWNER_SLOT, bytes32(uint256(uint160(RevShareCommon.PROXY_ADMIN)))); + + // Setup SequencerFeeVault proxy + vm.etch(SEQUENCER_FEE_VAULT, proxyCode); + vm.store(SEQUENCER_FEE_VAULT, PROXY_IMPLEMENTATION_SLOT, bytes32(uint256(uint160(sequencerFeeVaultImplAddr)))); + vm.store(SEQUENCER_FEE_VAULT, PROXY_OWNER_SLOT, bytes32(uint256(uint160(RevShareCommon.PROXY_ADMIN)))); + + // Setup BaseFeeVault proxy + vm.etch(BASE_FEE_VAULT, proxyCode); + vm.store(BASE_FEE_VAULT, PROXY_IMPLEMENTATION_SLOT, bytes32(uint256(uint160(defaultFeeVaultImplAddr)))); + vm.store(BASE_FEE_VAULT, PROXY_OWNER_SLOT, bytes32(uint256(uint160(RevShareCommon.PROXY_ADMIN)))); + + // Setup L1FeeVault proxy + vm.etch(L1_FEE_VAULT, proxyCode); + vm.store(L1_FEE_VAULT, PROXY_IMPLEMENTATION_SLOT, bytes32(uint256(uint160(defaultFeeVaultImplAddr)))); + vm.store(L1_FEE_VAULT, PROXY_OWNER_SLOT, bytes32(uint256(uint160(RevShareCommon.PROXY_ADMIN)))); + + // Setup FeeSplitter proxy + vm.etch(FEE_SPLITTER, proxyCode); + vm.store(FEE_SPLITTER, PROXY_IMPLEMENTATION_SLOT, bytes32(uint256(uint160(feeSplitterImplAddr)))); + vm.store(FEE_SPLITTER, PROXY_OWNER_SLOT, bytes32(uint256(uint160(RevShareCommon.PROXY_ADMIN)))); + } + + /// @notice Test the integration of setupRevShare + function test_setupRevShare_integration() public { + // Step 1: Record logs for L1→L2 message replay + vm.recordLogs(); + + // Step 2: Execute task simulation + revShareTask.simulate("test/tasks/example/eth/017-revshare-setup/config.toml"); + + // Step 3: Relay deposit transactions from L1 to all L2s + uint256[] memory forkIds = new uint256[](3); + forkIds[0] = _opMainnetForkId; + forkIds[1] = _inkMainnetForkId; + forkIds[2] = _soneiumMainnetForkId; + + address[] memory portals = new address[](3); + portals[0] = OP_MAINNET_PORTAL; + portals[1] = INK_MAINNET_PORTAL; + portals[2] = SONEIUM_MAINNET_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: 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 6: 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 7: Do a withdrawal flow + + // Fund vaults with amount > minWithdrawalAmount + _fundVaults(1 ether, _opMainnetForkId); + _fundVaults(1 ether, _inkMainnetForkId); + _fundVaults(1 ether, _soneiumMainnetForkId); + + // Disburse fees in all chains and expect the L1Withdrawer to trigger the 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; + + _executeDisburseAndAssertWithdrawal(_opMainnetForkId, OP_L1_WITHDRAWAL_RECIPIENT, expectedWithdrawalAmount); + _executeDisburseAndAssertWithdrawal(_inkMainnetForkId, INK_L1_WITHDRAWAL_RECIPIENT, expectedWithdrawalAmount); + _executeDisburseAndAssertWithdrawal( + _soneiumMainnetForkId, SONEIUM_L1_WITHDRAWAL_RECIPIENT, expectedWithdrawalAmount + ); + } +} diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index 840b264456..ce08bbb965 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -41,7 +41,9 @@ import {OPCMUpgradeV410} from "src/template/OPCMUpgradeV410.sol"; import {OPCMUpgradeSuperchainConfigV410} from "src/template/OPCMUpgradeSuperchainConfigV410.sol"; import {L1PortalExecuteL2Call} from "src/template/L1PortalExecuteL2Call.sol"; import {AddGameTypeTemplate} from "src/template/AddGameTypeTemplate.sol"; +import {MigrateToLiveness2} from "src/template/MigrateToLiveness2.sol"; import {RevShareUpgradeAndSetup} from "src/template/RevShareUpgradeAndSetup.sol"; +import {RevShareSetup} from "src/template/RevShareSetup.sol"; /// @notice Ensures that simulating the task consistently produces the same call data and data to sign. /// This guarantees determinism if a bug is introduced in the task logic, the call data or data to sign @@ -982,6 +984,63 @@ contract RegressionTest is Test { _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); } + /// @notice Expected call data and data to sign generated by manually running the RevShareSetup template at block 23820274 on mainnet. + /// Simulate from task directory (test/tasks/example/eth/017-revshare-setup) with: + /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path "$(pwd)"/.env --justfile ../../../../../src/justfile simulate + function testRegressionCallDataMatches_RevShareSetup() public { + string memory taskConfigFilePath = "test/tasks/example/eth/017-revshare-setup/config.toml"; + string memory expectedCallData = + "0x82ad56cb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006fee65a372d63295e8eb12574652f0bfeb913149000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000224cdab407800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000beb5fc579115071764c7423a4f12edde41f106ed0000000000000000000000000000000000000000000000000000000000055730000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000c350000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005d66c1782664115999c47c9fa5cd031f495d3e4f000000000000000000000000000000000000000000000000000000000007a120000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000c3500000000000000000000000000000000000000000000000000000000000000000200000000000000000000000088e529a6ccd302c948689cd5156c83d4614fae92000000000000000000000000000000000000000000000000000000000007a120000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000c3500000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000"; + MultisigTask multisigTask = new RevShareSetup(); + address rootSafe = address(0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A); + address foundationChildMultisig = 0x847B5c174615B1B7fDF770882256e2D3E95b9D92; + address[] memory allSafes = MultisigTaskTestHelper.getAllSafes(rootSafe, foundationChildMultisig); + + (Action[] memory actions, uint256[] memory allOriginalNonces) = + _setupAndSimulate(taskConfigFilePath, 23820274, "mainnet", multisigTask, allSafes); + + _assertCallDataMatches(multisigTask, actions, allSafes, allOriginalNonces, expectedCallData); + + string[] memory expectedDataToSign = new string[](2); + + // Foundation + expectedDataToSign[0] = + "0x1901a4a9c312badf3fcaa05eafe5dc9bee8bd9316c78ee8b0bebe3115bb21b7326725ee24803aca0a6af16bc1c33df420238c2447291f5ab417a1a6fe8375a17d5c0"; + // Security council + expectedDataToSign[1] = + "0x1901df53d510b56e539b90b369ef08fce3631020fbf921e3136ea5f8747c20bce9678d870bb68459395de3d3ba0b1a21834e6c6802a206a8797c4442d74f8a96cecb"; + + _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); + } + + /// @notice Expected call data and data to sign generated by manually running the RevShareUpgradeAndSetup template at block 9628956 on Sepolia. + /// Simulate from task directory (test/tasks/example/sep/031-revshare-setup) with: + /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env --justfile ../../../../../src/improvements/single.just simulate + function testRegressionCallDataMatches_RevShareSetupSepolia() public { + string memory taskConfigFilePath = "test/tasks/example/sep/031-revshare-setup/config.toml"; + string memory expectedCallData = + "0x82ad56cb000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000890c61c7f3f40b851ebcaacfa879c6075426419d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e4cdab40780000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000016fc5058f25648194471939df75cf27a2fdc48bc0000000000000000000000000000000000000000000000000000000000055730000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000c3500000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000"; + MultisigTask multisigTask = new RevShareSetup(); + address rootSafe = address(0x1Eb2fFc903729a0F03966B917003800b145F56E2); + address foundationChildMultisig = address(0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B); + address[] memory allSafes = MultisigTaskTestHelper.getAllSafes(rootSafe, foundationChildMultisig); + + (Action[] memory actions, uint256[] memory allOriginalNonces) = + _setupAndSimulate(taskConfigFilePath, 9628956, "sepolia", multisigTask, allSafes); + + _assertCallDataMatches(multisigTask, actions, allSafes, allOriginalNonces, expectedCallData); + + string[] memory expectedDataToSign = new string[](2); + // Foundation + expectedDataToSign[0] = + "0x190137e1f5dd3b92a004a23589b741196c8a214629d4ea3a690ec8e41ae45c689cbb60593f7cc9aeca00d93e8d3bc4ecfc1d1d3f77fd8e9d914002b75cf47d794d41"; + // Security Council + expectedDataToSign[1] = + "0x1901be081970e9fc104bd1ea27e375cd21ec7bb1eec56bfe43347c3e36c5d27b8533a12d8bcecba052629a3d85176a5924c80e5faa201eed78daf784b6d0cbbdf5e2"; + + _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); + } + /// @notice Expected call data and data to sign generated by manually running the RevShareUpgradeAndSetup template at block 23820274 on mainnet. /// Simulate from task directory (test/tasks/example/eth/016-revshare-upgrade-and-setup) with: /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path "$(pwd)"/.env --justfile ../../../../../src/justfile simulate @@ -1012,10 +1071,10 @@ contract RegressionTest is Test { } /// @notice Expected call data and data to sign generated by manually running the RevShareUpgradeAndSetup template at block 9628956 on Sepolia. - /// Simulate from task directory (test/tasks/example/sep/030-revshare-upgrade-and-setup) with: + /// Simulate from task directory (test/tasks/example/sep/032-revshare-upgrade-and-setup) with: /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env --justfile ../../../../../src/improvements/single.just simulate function testRegressionCallDataMatches_RevShareUpgradeAndSetupSepolia() public { - string memory taskConfigFilePath = "test/tasks/example/sep/030-revshare-upgrade-and-setup/config.toml"; + string memory taskConfigFilePath = "test/tasks/example/sep/032-revshare-upgrade-and-setup/config.toml"; string memory expectedCallData = "0x82ad56cb000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000890c61c7f3f40b851ebcaacfa879c6075426419d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e41a80dc380000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000016fc5058f25648194471939df75cf27a2fdc48bc0000000000000000000000000000000000000000000000000000000000055730000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000c3500000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000"; MultisigTask multisigTask = new RevShareUpgradeAndSetup(); @@ -1118,4 +1177,37 @@ contract RegressionTest is Test { assertEq(keccak256(bytes(dataToSign)), keccak256(bytes(expectedDataToSign[i]))); } } + + /// @notice Expected call data and data to sign generated by manually running the MigrateToLiveness2 at block 9691830 on sepolia. + /// Simulate from task directory (test/tasks/example/sep/030-migrate-to-liveness2/) with: + /// SIMULATE_WITHOUT_LEDGER=true just --justfile ../../../../../src/justfile simulate + function testRegressionCallDataMatches_MigrateToLiveness2() public { + address rootSafe = address(0xB2DEfc35a51E4f2126667A9FC8D941202077aC0E); + string memory taskConfigFilePath = "test/tasks/example/sep/030-migrate-to-liveness2/config.toml"; + // Calldata generated by manually running the MigrateToLiveness2 template at block 9691830 on sepolia. + string memory expectedCallData = + "0x174dea71000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000340000000000000000000000000b2defc35a51e4f2126667a9fc8d941202077ac0e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000024e19a9dd9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2defc35a51e4f2126667a9fc8d941202077ac0e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000024610b5925000000000000000000000000dfffe9dc096938fb156a54e67d09f73f5514eb1600000000000000000000000000000000000000000000000000000000000000000000000000000000dfffe9dc096938fb156a54e67d09f73f5514eb16000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004405ccf6060000000000000000000000000000000000000000000000000000000000000001000000000000000000000000e934dc97e347c6acef74364b50125bb8689c40ff00000000000000000000000000000000000000000000000000000000000000000000000000000000b2defc35a51e4f2126667a9fc8d941202077ac0e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000044e009cfde000000000000000000000000dfffe9dc096938fb156a54e67d09f73f5514eb16000000000000000000000000b78ea2a288fca20d7f499f8a9ee48ff50660985500000000000000000000000000000000000000000000000000000000"; + + MultisigTask multisigTask = new MigrateToLiveness2(); + + address[] memory allSafes = MultisigTaskTestHelper.getAllSafes(rootSafe); + (Action[] memory actions, uint256[] memory allOriginalNonces) = + _setupAndSimulate(taskConfigFilePath, 9691830, "sepolia", multisigTask, allSafes); + + bytes memory rootSafeCalldata = + _assertCallDataMatches(multisigTask, actions, allSafes, allOriginalNonces, expectedCallData); + uint256 rootSafeNonce = allOriginalNonces[allOriginalNonces.length - 1]; + + // Data to sign generated by manually running the MigrateToLiveness2 template at block 9691830 on sepolia. + string memory expectedDataToSign = + "0x1901bce54de085c4d47267dbd131a558f3e07f35d496718f397e1158eb30a6b28ae02569065845998f134d339270fcc5b81ac10c31ff00a9e4aba0a3a1f98461bbe1"; + string memory dataToSign = vm.toString( + GnosisSafeHashes.getEncodedTransactionData(rootSafe, rootSafeCalldata, 0, rootSafeNonce, MULTICALL3_ADDRESS) + ); + // assert that the data to sign generated in simulate is the same as the expected data to sign + assertEq(keccak256(bytes(dataToSign)), keccak256(bytes(expectedDataToSign))); + _assertDataToSignSingleMultisig( + rootSafe, rootSafeCalldata, expectedDataToSign, rootSafeNonce, MULTICALL3_ADDRESS + ); + } } diff --git a/test/tasks/example/eth/017-revshare-setup/.env b/test/tasks/example/eth/017-revshare-setup/.env new file mode 100644 index 0000000000..8610954676 --- /dev/null +++ b/test/tasks/example/eth/017-revshare-setup/.env @@ -0,0 +1,3 @@ +TENDERLY_GAS=16000000 +NESTED_SAFE_NAME_DEPTH_1=foundation +FORK_BLOCK_NUMBER=23820274 \ No newline at end of file diff --git a/test/tasks/example/eth/017-revshare-setup/config.toml b/test/tasks/example/eth/017-revshare-setup/config.toml new file mode 100644 index 0000000000..61daeb3573 --- /dev/null +++ b/test/tasks/example/eth/017-revshare-setup/config.toml @@ -0,0 +1,38 @@ +# RevShare Setup Configuration + +templateName = "RevShareSetup" + +# L2 chains to target +l2chains = [{name = "OP Mainnet", chainId = 10}, {name = "Ink", chainId = 57073}, {name = "Soneium", chainId = 1868}] + +safeAddress = "ProxyAdminOwner" + +# RevShareContractsUpgrader address +# TODO: Update to the corresponding address after audit +revShareUpgrader = "0x6fee65A372d63295E8EB12574652F0BfEb913149" + +# RevShare configurations (one per chain) - flattened arrays +portals = [ + "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed", # OP Mainnet Portal + "0x5d66C1782664115999C47c9fA5cd031f495D3e4F", # Ink Mainnet Portal + "0x88e529A6ccd302c948689Cd5156C83D4614FAE92" # Soneium Mainnet Portal +] +chainFeesRecipients = [ + "0x0000000000000000000000000000000000000001", # TODO: Update to the corresponding address once defined + "0x0000000000000000000000000000000000000002", # TODO: Update to the corresponding address once defined + "0x0000000000000000000000000000000000000003" # TODO: Update to the corresponding address once defined +] +l1WithdrawerMinWithdrawalAmounts = [350000, 500000, 500000] + +l1WithdrawerRecipients = [ + # TODO: Update to the corresponding (e.g. FeesDepositor) address once defined + "0x0000000000000000000000000000000000000001", + # TODO: Update to the corresponding (e.g. FeesDepositor) address once defined + "0x0000000000000000000000000000000000000002", + # TODO: Update to the corresponding (e.g. FeesDepositor) address once defined + "0x0000000000000000000000000000000000000003" +] +l1WithdrawerGasLimits = [800000, 800000, 800000] + +[addresses] +ProxyAdminOwner = "0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A" diff --git a/test/tasks/example/sep/030-migrate-to-liveness2/.env b/test/tasks/example/sep/030-migrate-to-liveness2/.env new file mode 100644 index 0000000000..de9ae20c76 --- /dev/null +++ b/test/tasks/example/sep/030-migrate-to-liveness2/.env @@ -0,0 +1,3 @@ +TENDERLY_GAS=16700000 +FORK_BLOCK_NUMBER=9691830 +NESTED_SAFE_NAME_DEPTH_1=foundation diff --git a/test/tasks/example/sep/030-migrate-to-liveness2/README.md b/test/tasks/example/sep/030-migrate-to-liveness2/README.md new file mode 100644 index 0000000000..d6f8200111 --- /dev/null +++ b/test/tasks/example/sep/030-migrate-to-liveness2/README.md @@ -0,0 +1,48 @@ +# MigrateToLiveness2 Task + +## Overview + +This task migrates a Safe from the v1 liveness module (which combined guard and module functionality) to the v2 SaferSafes-based liveness system. + +## What This Task Does + +1. Removes the guard (sets it to `address(0)`) +2. Enables the SaferSafes module +3. Configures the liveness module settings +4. Disables the old liveness module + +## State Override Explanation + +### Why do we need `stateOverrides` in the config? + +The config includes a state override that temporarily removes the guard during simulation: + +```toml +[stateOverrides] +0xB2DEfc35a51E4f2126667A9FC8D941202077aC0E = [ + {key = "0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8", value = 0} +] +``` + +**This is only needed for simulation, NOT for production.** + +### The Problem + +The old liveness guard has a `checkTransaction()` hook that runs **before** every Safe transaction. This guard checks that `msg.sender` (the caller of `execTransaction`) is a Safe owner. + +**In simulation:** +- The MultisigTask framework contract calls `execTransaction()` +- The guard sees `msg.sender = MultisigTask contract` (not an owner) +- The guard blocks the transaction with error `TimelockGuard_NotOwner()` + +**In production (using `just sign` and `just execute`):** +- Owners sign the transaction using `just sign` +- An owner executes the transaction using `just execute` +- The guard sees `msg.sender = owner` +- The guard allows the transaction to proceed + +### The Solution + +The state override temporarily sets the guard slot to `address(0)` during simulation, bypassing the guard check. This allows the simulation framework to test the transaction logic without needing to be an owner. + +**Important:** This is purely a simulation workaround. In production, an owner must run `just execute` so the guard check passes. The transaction will succeed without any override. diff --git a/test/tasks/example/sep/030-migrate-to-liveness2/config.toml b/test/tasks/example/sep/030-migrate-to-liveness2/config.toml new file mode 100644 index 0000000000..a31ebb214c --- /dev/null +++ b/test/tasks/example/sep/030-migrate-to-liveness2/config.toml @@ -0,0 +1,16 @@ +templateName = "MigrateToLiveness2" + +[addresses] +saferSafes="0xdffFe9Dc096938fb156A54e67D09f73f5514Eb16" +targetSafe="0xB2DEfc35a51E4f2126667A9FC8D941202077aC0E" +currentLivenessModule="0xB78eA2a288fca20D7f499F8a9eE48ff506609855" + + +[livenessModule] +livenessResponsePeriod="1" +fallbackOwner="0xe934Dc97E347C6aCef74364B50125bb8689c40ff" + +[stateOverrides] +0xB2DEfc35a51E4f2126667A9FC8D941202077aC0E = [ # targetSafe - temporarily remove guard for simulation + {key = "0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8", value = 0} # guard slot +] \ No newline at end of file diff --git a/test/tasks/example/sep/030-revshare-upgrade-and-setup/.env b/test/tasks/example/sep/031-revshare-setup/.env similarity index 100% rename from test/tasks/example/sep/030-revshare-upgrade-and-setup/.env rename to test/tasks/example/sep/031-revshare-setup/.env diff --git a/test/tasks/example/sep/031-revshare-setup/config.toml b/test/tasks/example/sep/031-revshare-setup/config.toml new file mode 100644 index 0000000000..e39def724b --- /dev/null +++ b/test/tasks/example/sep/031-revshare-setup/config.toml @@ -0,0 +1,22 @@ +# RevShare Setup Configuration + +templateName = "RevShareSetup" + +# L2 chains to target +l2chains = [{name = "OP Sepolia Testnet", chainId = 11155420}] + +safeAddress = "ProxyAdminOwner" + +# RevShareContractsUpgrader address +# TODO: Update to the corresponding address after audit +revShareUpgrader = "0x890C61C7F3f40B851EbCAacFA879C6075426419D" + +# RevShare configurations (one per chain) - flattened arrays +portals = ["0x16Fc5058F25648194471939df75CF27A2fdC48BC"] # OP Sepolia Testnet Portal +chainFeesRecipients = ["0x0000000000000000000000000000000000000001"] # OP Sepolia Testnet chain fees recipient +l1WithdrawerMinWithdrawalAmounts = [350000] +l1WithdrawerRecipients = ["0x0000000000000000000000000000000000000001"] +l1WithdrawerGasLimits = [800000] + +[addresses] +ProxyAdminOwner = "0x1Eb2fFc903729a0F03966B917003800b145F56E2" diff --git a/test/tasks/example/sep/032-revshare-upgrade-and-setup/.env b/test/tasks/example/sep/032-revshare-upgrade-and-setup/.env new file mode 100644 index 0000000000..2fd241eabf --- /dev/null +++ b/test/tasks/example/sep/032-revshare-upgrade-and-setup/.env @@ -0,0 +1,3 @@ +TENDERLY_GAS=16000000 +FORK_BLOCK_NUMBER=9628956 +NESTED_SAFE_NAME_DEPTH_1=foundation diff --git a/test/tasks/example/sep/030-revshare-upgrade-and-setup/config.toml b/test/tasks/example/sep/032-revshare-upgrade-and-setup/config.toml similarity index 100% rename from test/tasks/example/sep/030-revshare-upgrade-and-setup/config.toml rename to test/tasks/example/sep/032-revshare-upgrade-and-setup/config.toml diff --git a/test/template/revenue-share-upgrade-path/RevShareContractsUpgrader.t.sol b/test/template/revenue-share-upgrade-path/RevShareContractsUpgrader.t.sol index a0e74becaa..1982e3a5db 100644 --- a/test/template/revenue-share-upgrade-path/RevShareContractsUpgrader.t.sol +++ b/test/template/revenue-share-upgrade-path/RevShareContractsUpgrader.t.sol @@ -189,7 +189,7 @@ contract RevShareContractsUpgrader_TestInit is Test { _portal, abi.encodeCall( IOptimismPortal2.depositTransaction, - (RevShareCommon.FEE_SPLITTER, 0, FeeVaultUpgrader.SETTERS_GAS_LIMIT, false, setCalculatorCall) + (RevShareCommon.FEE_SPLITTER, 0, RevShareCommon.SETTERS_GAS_LIMIT, false, setCalculatorCall) ), abi.encode() ); @@ -250,7 +250,7 @@ contract RevShareContractsUpgrader_TestInit is Test { ( _vault, 0, - FeeVaultUpgrader.SETTERS_GAS_LIMIT, + RevShareCommon.SETTERS_GAS_LIMIT, false, abi.encodeCall(IFeeVault.setRecipient, (RevShareCommon.FEE_SPLITTER)) ) @@ -266,7 +266,7 @@ contract RevShareContractsUpgrader_TestInit is Test { ( _vault, 0, - FeeVaultUpgrader.SETTERS_GAS_LIMIT, + RevShareCommon.SETTERS_GAS_LIMIT, false, abi.encodeCall(IFeeVault.setMinWithdrawalAmount, (0)) ) @@ -282,7 +282,7 @@ contract RevShareContractsUpgrader_TestInit is Test { ( _vault, 0, - FeeVaultUpgrader.SETTERS_GAS_LIMIT, + RevShareCommon.SETTERS_GAS_LIMIT, false, abi.encodeCall(IFeeVault.setWithdrawalNetwork, (IFeeVault.WithdrawalNetwork.L2)) )