Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/template/TransferSystemConfigOwnership.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
27 changes: 27 additions & 0 deletions test/tasks/Regression.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FORK_BLOCK_NUMBER=10624099
Original file line number Diff line number Diff line change
@@ -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" }
]