Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/RevShareContractsUpgrader.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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))
);

Expand Down
12 changes: 6 additions & 6 deletions src/libraries/FeeVaultUpgrader.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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))
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/RevShareCommon.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
2 changes: 1 addition & 1 deletion src/script/check-task-statuses.sh
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/script/fetch-tasks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/tasks/StackedSimulator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/tasks/eth/036-U17-main-base/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 1 addition & 0 deletions src/tasks/sep/046-worldchain-l2pao-key-handback-over/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TENDERLY_GAS=16700000
21 changes: 21 additions & 0 deletions src/tasks/sep/046-worldchain-l2pao-key-handback-over/README.md
Original file line number Diff line number Diff line change
@@ -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 <council|foundation>
```

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 <council|foundation>
```
87 changes: 87 additions & 0 deletions src/tasks/sep/046-worldchain-l2pao-key-handback-over/VALIDATION.md
Original file line number Diff line number Diff line change
@@ -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
```
24 changes: 24 additions & 0 deletions src/tasks/sep/046-worldchain-l2pao-key-handback-over/config.toml
Original file line number Diff line number Diff line change
@@ -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}
]
143 changes: 143 additions & 0 deletions src/template/MigrateToLiveness2.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading