diff --git a/src/template/TransferSystemConfigOwnership.sol b/src/template/TransferSystemConfigOwnership.sol new file mode 100644 index 000000000..735ac88e7 --- /dev/null +++ b/src/template/TransferSystemConfigOwnership.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {VmSafe} from "forge-std/Vm.sol"; +import {stdToml} from "lib/forge-std/src/StdToml.sol"; + +import {L2TaskBase} from "src/tasks/types/L2TaskBase.sol"; +import {SuperchainAddressRegistry} from "src/SuperchainAddressRegistry.sol"; +import {Action} from "src/libraries/MultisigTypes.sol"; + +interface ISystemConfig { + function owner() external view returns (address); + function transferOwnership(address newOwner) external; +} + +/// @notice Template for transferring ownership of a chain's SystemConfig proxy. +/// The root Safe (current SystemConfig owner) MUST be configured via the top-level +/// `safeAddressString` key in the task's config.toml. +/// ATTENTION: Transferring ownership is high-risk — restricted to one chain per task. +contract TransferSystemConfigOwnership is L2TaskBase { + using stdToml for string; + + /// @notice The new owner of the SystemConfig proxy. + address public newOwner; + + /// @notice The single chain targeted by this task. + SuperchainAddressRegistry.ChainInfo internal activeChain; + + /// @notice Must be set via the top-level `safeAddressString` key in the config file. + function safeAddressString() public pure override returns (string memory) { + revert("safeAddressString must be set in the config file"); + } + + /// @notice Returns the storage write permissions required for this task. + function _taskStorageWrites() internal pure virtual override returns (string[] memory) { + string[] memory storageWrites = new string[](1); + storageWrites[0] = "SystemConfigProxy"; + return storageWrites; + } + + /// @notice Sets up the template with the new owner from a TOML file. + function _templateSetup(string memory _taskConfigFilePath, address _rootSafe) internal override { + super._templateSetup(_taskConfigFilePath, _rootSafe); + + string memory toml = vm.readFile(_taskConfigFilePath); + newOwner = toml.readAddress(".newOwner"); + require(newOwner != address(0), "TransferSystemConfigOwnership: newOwner is zero address"); + + SuperchainAddressRegistry.ChainInfo[] memory chains = superchainAddrRegistry.getChains(); + require(chains.length == 1, "TransferSystemConfigOwnership: exactly one chain required"); + activeChain = chains[0]; + + address systemConfigProxy = superchainAddrRegistry.getAddress("SystemConfigProxy", activeChain.chainId); + address currentOwner = ISystemConfig(systemConfigProxy).owner(); + require(currentOwner != newOwner, "TransferSystemConfigOwnership: newOwner equals current owner"); + require( + currentOwner == _rootSafe, "TransferSystemConfigOwnership: rootSafe is not current SystemConfig owner" + ); + } + + /// @notice Builds the action that transfers SystemConfig ownership. + function _build(address) internal override { + address systemConfigProxy = superchainAddrRegistry.getAddress("SystemConfigProxy", activeChain.chainId); + ISystemConfig(systemConfigProxy).transferOwnership(newOwner); + } + + /// @notice Validates that ownership was transferred to the new owner. + function _validate(VmSafe.AccountAccess[] memory, Action[] memory, address) internal view override { + address systemConfigProxy = superchainAddrRegistry.getAddress("SystemConfigProxy", activeChain.chainId); + require( + ISystemConfig(systemConfigProxy).owner() == newOwner, + "TransferSystemConfigOwnership: owner not updated" + ); + } + + /// @notice newOwner may be an EOA or a Safe with no runtime code at the pinned block. + function _getCodeExceptions() internal view virtual override returns (address[] memory) { + address[] memory exceptions = new address[](1); + exceptions[0] = newOwner; + return exceptions; + } +} diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index d6e27218e..e968971f2 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -47,6 +47,7 @@ import {MigrateToLiveness2} from "src/template/MigrateToLiveness2.sol"; import {RevShareUpgradeAndSetup} from "src/template/RevShareUpgradeAndSetup.sol"; import {RevShareSetup} from "src/template/RevShareSetup.sol"; import {SetBatcherAndOrSigner} from "src/template/SetBatcherAndOrSigner.sol"; +import {TransferSystemConfigOwnership} from "src/template/TransferSystemConfigOwnership.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 @@ -1301,4 +1302,30 @@ contract RegressionTest is Test { rootSafe, rootSafeCalldata, expectedDataToSign, rootSafeNonce, MULTICALL3_ADDRESS ); } + + /// @notice Expected call data and data to sign generated by manually running the TransferSystemConfigOwnership template at block 10624099 on sepolia. + /// Simulate from task directory (test/tasks/example/sep/036-transfer-systemconfig-ownership) with: + /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path $(pwd)/.env --justfile ../../../../../src/justfile simulate + function testRegressionCallDataMatches_TransferSystemConfigOwnership() public { + string memory taskConfigFilePath = "test/tasks/example/sep/036-transfer-systemconfig-ownership/config.toml"; + // TODO: paste calldata captured from a manual `just simulate` run at block 10624099 on sepolia. + string memory expectedCallData = "0x"; + MultisigTask multisigTask = new TransferSystemConfigOwnership(); + address rootSafe = address(0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B); // FoundationUpgradeSafe on Sepolia + address[] memory allSafes = MultisigTaskTestHelper.getAllSafes(rootSafe); + + (Action[] memory actions, uint256[] memory allOriginalNonces) = + _setupAndSimulate(taskConfigFilePath, 10624099, "sepolia", multisigTask, allSafes); + + bytes memory rootSafeCalldata = + _assertCallDataMatches(multisigTask, actions, allSafes, allOriginalNonces, expectedCallData); + uint256 rootSafeNonce = allOriginalNonces[allOriginalNonces.length - 1]; + + // TODO: paste dataToSign captured from a manual `just simulate` run at block 10624099 on sepolia. + string memory expectedDataToSign = "0x"; + + _assertDataToSignSingleMultisig( + rootSafe, rootSafeCalldata, expectedDataToSign, rootSafeNonce, MULTICALL3_ADDRESS + ); + } } diff --git a/test/tasks/example/sep/036-transfer-systemconfig-ownership/.env b/test/tasks/example/sep/036-transfer-systemconfig-ownership/.env new file mode 100644 index 000000000..a6f83d864 --- /dev/null +++ b/test/tasks/example/sep/036-transfer-systemconfig-ownership/.env @@ -0,0 +1 @@ +FORK_BLOCK_NUMBER=10624099 diff --git a/test/tasks/example/sep/036-transfer-systemconfig-ownership/config.toml b/test/tasks/example/sep/036-transfer-systemconfig-ownership/config.toml new file mode 100644 index 000000000..003ea749c --- /dev/null +++ b/test/tasks/example/sep/036-transfer-systemconfig-ownership/config.toml @@ -0,0 +1,13 @@ +templateName = "TransferSystemConfigOwnership" +safeAddressString = "FoundationUpgradeSafe" + +l2chains = [{name = "OP Sepolia Testnet", chainId = 11155420}] + +newOwner = "0x1234567890AbcdEF1234567890aBcdef12345678" + +[stateOverrides] +# Override SystemConfig owner (slot 0x33) to FoundationUpgradeSafe on Sepolia so +# the rootSafe is the current owner at the pinned block. +0x034edD2A225f7f429A63E0f1D2084B9E0A93b538 = [ + { key = "0x0000000000000000000000000000000000000000000000000000000000000033", value = "0x000000000000000000000000DEe57160aAfCF04c34C887B5962D0a69676d3C8B" } +]