diff --git a/security-council-rehearsals/README.md b/security-council-rehearsals/README.md index 2e32e075f0..1088a06386 100644 --- a/security-council-rehearsals/README.md +++ b/security-council-rehearsals/README.md @@ -5,8 +5,10 @@ This directory contains templates and ceremonies related to onboarding Security ## Creating and signing a new rehearsal ceremony To create a new rehearsal ceremony, follow the instructions in the _Facilitator_ section of the README files for each of the following rehearsals: + 1. [rehearsal-1-welcome](./rehearsal-1-welcome/README.md) 2. [rehearsal-2-remove-signer](./rehearsal-2-remove-signer/README.md) 3. [rehearsal-3-nested-upgrade](./rehearsal-3-jointly-upgrade/README.md) +4. [rehearsal-4-noop-upgrade](./rehearsal-4-noop-upgrade/README.md) If you're a signer, you can find instructions for signing the rehearsals in the top sections of the same README files. diff --git a/security-council-rehearsals/rehearsal-4-noop-upgrade/README.md b/security-council-rehearsals/rehearsal-4-noop-upgrade/README.md new file mode 100644 index 0000000000..0faa4bd21e --- /dev/null +++ b/security-council-rehearsals/rehearsal-4-noop-upgrade/README.md @@ -0,0 +1,265 @@ +# Rehearsal 4 - No-op Governor Upgrade + +## Objective + +In this rehearsal, we will be performing an upgrade call to the Governor Proxy on OP Mainnet. + +Once completed: + +- A deposit transaction event will be emitted from the OptimismPortal contract on L1. +- The Governor implementation on OP Mainnet will remain unchanged + +The call executed by the Safe contract is defined in the `build` function of the [`L1PortalExecuteL2Call`](../../src/improvements/template/L1PortalExecuteL2Call.sol) template. + +Note: No onchain actions will occur during this rehearsal. You are not submitting a transaction, and your wallet does not need to be funded. You will simply sign an offchain message with your wallet. These signatures will be collected by a Facilitator, who will submit them for execution. +Once the required number of signatures is collected, anyone can finalize the execution. For convenience, a Facilitator will handle this step. + +## Approving the transaction + +### 1. Update repo and move to the appropriate folder for this rehearsal task: + +``` +cd superchain-ops +git pull +# Make sure you've installed the dependencies for the repository. +cd src/improvements/tasks//rehearsals/ # This path should be shared with you by the Facilitator. +``` + +See the [README](../../src/improvements/README.md) for more information on how to install the dependencies for the repository. + +### 2. Setup Ledger + +Your Ledger needs to be connected and unlocked. The Ethereum +application needs to be opened on Ledger with the message “Application +is ready”. + +### 3. Simulate and validate the transaction + +Make sure your ledger is still unlocked and run the following. + +```shell +cd src/improvements/tasks//rehearsals/ +just --dotenv-path $(pwd)/.env simulate +# For a different derivation path, use: HD_PATH=1 just --dotenv-path $(pwd)/.env simulate +``` + +You will see a "Simulation link" URL in the output. + +Copy this URL from the output and and open it with your browser. A prompt may ask you to choose a +project, any project will do. You can create one if necessary. + +Click "Simulate Transaction". + +We will be performing 3 validations and ensure the domain hash and +message hash are the same between the Tenderly simulation and your +Ledger: + +1. Validate integrity of the simulation. +2. Validate correctness of the state diff. +3. Validate and extract domain hash and message hash to approve. + +#### 3.1. Validate integrity of the simulation. + +To validate integrity of the simulation, we need to check the following: + +1. "Network": Check the network is Ethereum Mainnet. +2. "Timestamp": Check the simulation is performed on a block with a + recent timestamp (i.e. close to when you run the script). +3. "Sender": Check the address shown is your signer account. If not, + you will need to determine which “number” it is in the list of + addresses on your ledger. By default the script will assume the + derivation path is `m/44'/60'/0'/0/0`. + +Here is an example screenshot, note that the Timestamp and Sender +might be different in your simulation: + +![](./images/1-simulation-success.png) + +#### 3.2. Validate correctness of the state diff. + +Now click on the "State" tab. Verify that: + +1. You'll see 3 nonce changes for the signer address, the Safe and the Proxy Admin's safe. +2. You will see state overrides (not a state change) for both Council's Safe and Proxy Admin's. This is expected and its purpose is to generate a successful Safe execution simulation without collecting any signatures. +3. You will also see state changes on the Proxy Admin's `approvedHashes` mapping. +4. You will see state changes for the OptimismPortal's `prevBoughtGas` and `prevBlockNum` storage variables, on the first slot. +5. You may see some LivenessGuard state changes which are safe to ignore, as per the screenshot below. These are a result of using the production Security Council safe for the simulation, which has LivenessGuard enabled. + +Here is an example screenshot. Note that the addresses may be +different: + +![](./images/2-state-diff.png) + +Now click on the "Events" tab. Verify that: + +1. `TransactionDeposited` event has been emitted by the Optimism Portal. This should include: + - The correct `to` address, in this case, the address used in the `config.toml` for `l2Target`. + - The correct `from` address, in this case, the aliased address of the `ProxyAdmin`. + - The correct calldata sent to the Governor in the `opaqueData` field: this is the concatenation of the value sent to the contract, the gas limit, a flag indicating if we are creating a contract (`false` in this rehearsal) and the actual call made to the L2 contract. + +Here is an example screenshot. Note the specific data might be different: + +![](./images/3-event-emission.png) + +In this rehearsal the values correspond to: + +``` +0000000000000000000000000000000000000000000000000000000000000000 -> msg.value +0000000000000000000000000000000000000000000000000000000000000000 -> value sent to L2 contract +000000000007a120 -> Gas Limit, in the photo the value corresponds to 500,000 +00 -> isCreation, set to false +3659cfe60000000000000000000000000000000000000000000000000000000000001234 -> The abi-encoded call to `upgradeTo(address)` with address `0x0000000000000000000000000000000000001234` +``` + +#### 3.3. Extract the domain hash and the message hash to approve. + +Now that we have verified the transaction performs the right +operation, we need to extract the domain hash and the message hash to +approve. + +Go back to the "Overview" tab, and find the first +`GnosisSafe.domainSeparator` call. This call's return value will be +the domain hash that will show up in your Ledger. + +Here is an example screenshot. Note that the hash value may be +different: + +![](./images/4-tenderly-hashes-1.png) + +Right before the `GnosisSafe.domainSeparator` call, you will see a +call to `GnosisSafe.encodeTransactionData`. Its return value will be a +concatenation of `0x1901`, the domain hash, and the message hash: +`0x1901[domain hash][message hash]`. + +Here is an example screenshot. Note that the hash value may be +different: + +![](./images/5-tenderly-hashes-2.png) + +Note down both the domain hash and the message hash. You will need to +compare them with the ones displayed in your terminal AND on the Ledger screen at signing. + +### 4. Approve the signature on your ledger + +Once the validations are done, it's time to actually sign the +transaction. Make sure your ledger is still unlocked and run the +following: + +```shell +cd src/improvements/tasks//rehearsals/ +just --dotenv-path $(pwd)/.env sign +# For a different derivation path, use: HD_PATH=1 just --dotenv-path $(pwd)/.env sign +``` + +> [!IMPORTANT] This is the most security critical part of the +> playbook: make sure the domain hash and message hash in the +> following three places match: + +1. In your terminal output. +2. On your Ledger screen. +3. In the Tenderly simulation. You should use the same Tenderly + simulation as the one you used to verify the state diffs, instead + of opening the new one printed in the console. + +After verification, sign the transaction. You will see the `Data`, +`Signer` and `Signature` printed in the console. Format should be +something like this: + +``` +Data: +Signer:
+Signature: +``` + +Double check the signer address is the right one. + +### 5. Send the output to Facilitator(s) + +Nothing has occurred onchain - these are offchain signatures which +will be collected by Facilitators for execution. Execution can occur +by anyone once a threshold of signatures are collected, so a +Facilitator will do the final execution for convenience. + +Format should be something like this: + +``` +Data: +Signer:
+Signature: +``` + +Share the `Data`, `Signer` and `Signature` with the Facilitator, and +congrats, you are done! + +## [For Facilitator ONLY] How to prepare and execute the rehearsal + +### [Before the rehearsal] Prepare the rehearsal + +#### 1. Create a new task in the `eth` directory: + +```bash +cd superchain-ops/src/improvements +just new task # Follow the prompts to create a new rehearsals task. +# (a) choose 'eth' +# (b) choose 'L1PortalExecuteL2Call' +# (c) press enter to answer 'no' to 'Is this a test task?' +# (d) press 'y' for 'Is this a security council rehearsal task?' +# (e) enter a name of the task in the format of '-' + +# This creates a new directory in the `src/improvements/tasks/eth/rehearsals` directory. +``` + +Next, make sure your `config.toml` is correct. You should use the TOML below as a starting point. + +```toml +templateName = "L1PortalExecuteL2Call" + +portal = "0x0" # L1 OptimismPortal +l2Target = "0x0" # replace with the target contract on L2 +l2Data = "0x0" # calldata to pass to L2 contract +gasLimit = 500000 +value = 0 +isCreation = false + +[addresses] +ProxyAdminOwner = "0x0" +OptimismPortal = "0x0" +``` + +#### 2. Test the rehearsal and commit the files to Github + +1. Test the newly created rehearsal by following the security council + steps in the `Approving the transaction` section above. +2. Commit the newly created files to Github. + +### [After the rehearsal] Execute the output + +1. Collect outputs from all participating signers. +2. Concatenate all signatures and export it as the `SIGNATURES` + environment variable, i.e. `export +SIGNATURES="0x[SIGNATURE1][SIGNATURE2]..."`. +3. Execute the transaction onchain. + +For example, if the quorum is 2 and you get the following outputs: + +```shell +Data: 0xDEADBEEF +Signer: 0xC0FFEE01 +Signature: AAAA +``` + +```shell +Data: 0xDEADBEEF +Signer: 0xC0FFEE02 +Signature: BBBB +``` + +Then you should run + +```shell +export SIGNATURES="0xAAAABBBB" +cd src/improvements/tasks//rehearsals/ +just --dotenv-path $(pwd)/.env execute +``` + +For posterity, you should make a `README.md` file in the tasks directory that contains a link to the executed transaction e.g. see [here](../../src/improvements/tasks/eth/rehearsals/2025-11-28-R2-remove-signer/README.md). diff --git a/security-council-rehearsals/rehearsal-4-noop-upgrade/images/1-simulation-success.png b/security-council-rehearsals/rehearsal-4-noop-upgrade/images/1-simulation-success.png new file mode 100644 index 0000000000..c6fb87e440 Binary files /dev/null and b/security-council-rehearsals/rehearsal-4-noop-upgrade/images/1-simulation-success.png differ diff --git a/security-council-rehearsals/rehearsal-4-noop-upgrade/images/2-state-diff.png b/security-council-rehearsals/rehearsal-4-noop-upgrade/images/2-state-diff.png new file mode 100644 index 0000000000..88725ddd71 Binary files /dev/null and b/security-council-rehearsals/rehearsal-4-noop-upgrade/images/2-state-diff.png differ diff --git a/security-council-rehearsals/rehearsal-4-noop-upgrade/images/3-event-emission.png b/security-council-rehearsals/rehearsal-4-noop-upgrade/images/3-event-emission.png new file mode 100644 index 0000000000..1fb09c4023 Binary files /dev/null and b/security-council-rehearsals/rehearsal-4-noop-upgrade/images/3-event-emission.png differ diff --git a/security-council-rehearsals/rehearsal-4-noop-upgrade/images/4-tenderly-hashes-1.png b/security-council-rehearsals/rehearsal-4-noop-upgrade/images/4-tenderly-hashes-1.png new file mode 100644 index 0000000000..cb0233241a Binary files /dev/null and b/security-council-rehearsals/rehearsal-4-noop-upgrade/images/4-tenderly-hashes-1.png differ diff --git a/security-council-rehearsals/rehearsal-4-noop-upgrade/images/5-tenderly-hashes-2.png b/security-council-rehearsals/rehearsal-4-noop-upgrade/images/5-tenderly-hashes-2.png new file mode 100644 index 0000000000..b0d9e16748 Binary files /dev/null and b/security-council-rehearsals/rehearsal-4-noop-upgrade/images/5-tenderly-hashes-2.png differ diff --git a/src/improvements/tasks/eth/rehearsals/2025-08-11-l1-to-l2-noop/README.md b/src/improvements/tasks/eth/rehearsals/2025-08-11-l1-to-l2-noop/README.md deleted file mode 100644 index 043ca1bb12..0000000000 --- a/src/improvements/tasks/eth/rehearsals/2025-08-11-l1-to-l2-noop/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Rehearsal - L1→L2 No‑op via OptimismPortal - -## Objective -Dry-run the nested SC ceremony to send a no‑op L2 call through the L1 OptimismPortal. - -## Steps (nested) -1) Update addresses in `config.toml`. -2) Simulate from the signer’s Safe: -```sh -just --dotenv-path $(pwd)/.env simulate council -# or foundation / chain-governor -``` -3) Validate -- Tenderly state diff has no unintended changes -- op‑txverify shows: Safe → OptimismPortal.depositTransaction → decoded inner L2 call -- Extract domain/message hashes and match Ledger -4) Sign -```sh -just --dotenv-path $(pwd)/.env sign council -``` -5) Facilitator approve/execute per `NESTED.md`. - -## Notes -- `gasLimit` must be explicit. `value` defaults to 0. -- Use a semantic no‑op inner calldata (e.g. upgradeTo current impl) or read‑only target for rehearsal. - - diff --git a/src/improvements/tasks/eth/rehearsals/2025-08-11-l1-to-l2-noop/config.toml b/src/improvements/tasks/eth/rehearsals/2025-08-11-l1-to-l2-noop/config.toml deleted file mode 100644 index d84d37d8bd..0000000000 --- a/src/improvements/tasks/eth/rehearsals/2025-08-11-l1-to-l2-noop/config.toml +++ /dev/null @@ -1,20 +0,0 @@ -templateName = "L1PortalExecuteL2Call" - -# L2 chain context for address labels (fake chain for rehearsal or Sepolia) -l2chains = [{ name = "Rehearsal Chain", chainId = 10101010101010 }] - -# Portal + L2 call params -portal = "0x0000000000000000000000000000000000000001" # replace with real L1 OptimismPortal -l2Target = "0x0000000000000000000000000000000000000002" # replace with L2 target -l2Data = "0x" # replace with encoded no-op data (e.g., upgradeTo(currentImpl) or a read-only call) -gasLimit = 500000 -value = 0 -isCreation = false - -[addresses] -TestRehearsalCouncil = "0x0000000000000000000000000000000000000003" -TestRehearsalFoundation = "0x0000000000000000000000000000000000000004" -ProxyAdminOwner = "0x0000000000000000000000000000000000000005" # 2-of-2 between council and foundation -OptimismPortal = "0x0000000000000000000000000000000000000001" - - diff --git a/src/improvements/tasks/eth/rehearsals/2025-08-22-R4-governor-upgrade/.env b/src/improvements/tasks/eth/rehearsals/2025-08-22-R4-governor-upgrade/.env new file mode 100644 index 0000000000..663b00c132 --- /dev/null +++ b/src/improvements/tasks/eth/rehearsals/2025-08-22-R4-governor-upgrade/.env @@ -0,0 +1 @@ +TENDERLY_GAS=10000000 diff --git a/src/improvements/tasks/eth/rehearsals/2025-08-22-R4-governor-upgrade/README.md b/src/improvements/tasks/eth/rehearsals/2025-08-22-R4-governor-upgrade/README.md new file mode 100644 index 0000000000..eea55f0ca0 --- /dev/null +++ b/src/improvements/tasks/eth/rehearsals/2025-08-22-R4-governor-upgrade/README.md @@ -0,0 +1 @@ +Status: [DRAFT, NOT READY TO SIGN] diff --git a/src/improvements/tasks/eth/rehearsals/2025-08-22-R4-governor-upgrade/config.toml b/src/improvements/tasks/eth/rehearsals/2025-08-22-R4-governor-upgrade/config.toml new file mode 100644 index 0000000000..d7a5b934c5 --- /dev/null +++ b/src/improvements/tasks/eth/rehearsals/2025-08-22-R4-governor-upgrade/config.toml @@ -0,0 +1,13 @@ +templateName = "L1PortalExecuteL2Call" + +# Portal + L2 call params +portal = "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed" # L1 OptimismPortal +l2Target = "0x1234567890123456789012345678901234567890" # replace with real OptimismGovernor Proxy +l2Data = "0x3659cfe60000000000000000000000000000000000000000000000000000000000001234" # upgradeTo(0x00..1234) +gasLimit = 500000 +value = 0 +isCreation = false + +[addresses] +ProxyAdminOwner = "0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A" +OptimismPortal = "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed" \ No newline at end of file diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index 10357849d6..620ef50347 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -30,6 +30,7 @@ import {WelcomeToSuperchainOps} from "src/improvements/template/WelcomeToSuperch import {GnosisSafeRemoveOwner} from "src/improvements/template/GnosisSafeRemoveOwner.sol"; import {SetEIP1967Implementation} from "src/improvements/template/SetEIP1967Implementation.sol"; import {UnpauseSuperchainConfigV400} from "src/improvements/template/UnpauseSuperchainConfigV400.sol"; +import {L1PortalExecuteL2Call} from "src/improvements/template/L1PortalExecuteL2Call.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 @@ -657,6 +658,34 @@ contract RegressionTest is Test { _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); } + /// @notice Expected call data and data to sign generated by manually running the L1PortalExecuteL2CallUpgradeGovernor template at block 23197819 on mainnet. + /// Simulate from task directory (test/tasks/example/eth/014-noop-call-optimismportal/config.toml) with: + /// just --dotenv-path $(pwd)/.env --justfile ../../../../../src/improvements/justfile simulate (foundation|council) + function testRegressionCallDataMatches_L1PortalExecuteL2CallUpgradeGovernor() public { + string memory taskConfigFilePath = "test/tasks/example/eth/014-noop-call-optimismportal/config.toml"; + string memory expectedCallData = + "0x174dea71000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000beb5fc579115071764c7423a4f12edde41f106ed0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000104e9e05c42000000000000000000000000cdf27f107725988f2261ce2256bdfcde8b382b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007a120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000243659cfe6000000000000000000000000ecbf4ed9f47302f00f0f039a691e7db83bdd26240000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + MultisigTask multisigTask = new L1PortalExecuteL2Call(); + address rootSafe = address(0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A); // L1PAO + address securityCouncilChildMultisig = address(0xc2819DC788505Aac350142A7A707BF9D03E3Bd03); + address[] memory allSafes = MultisigTaskTestHelper.getAllSafes(rootSafe, securityCouncilChildMultisig); + + (Action[] memory actions, uint256[] memory allOriginalNonces) = + _setupAndSimulate(taskConfigFilePath, 23197819, "mainnet", multisigTask, allSafes); + + _assertCallDataMatches(multisigTask, actions, allSafes, allOriginalNonces, expectedCallData); + + string[] memory expectedDataToSign = new string[](2); + // Foundation + expectedDataToSign[0] = + "0x1901a4a9c312badf3fcaa05eafe5dc9bee8bd9316c78ee8b0bebe3115bb21b73267229ea72d29d343d55ff76a6ce84cc8514d45683b4339b10bef5e956955bfe65c9"; + // Security Council + expectedDataToSign[1] = + "0x1901df53d510b56e539b90b369ef08fce3631020fbf921e3136ea5f8747c20bce9672b811a78d33f39e928848432a404247a2ab7c4a596b8586797a2e86b284b3b8b"; + + _assertDataToSignNestedMultisig(multisigTask, actions, expectedDataToSign, MULTICALL3_ADDRESS, rootSafe); + } + /// @notice Internal function to set up the fork and run the simulate method function _setupAndSimulate( string memory taskConfigFilePath, diff --git a/test/tasks/example/eth/014-noop-call-optimismportal/.env b/test/tasks/example/eth/014-noop-call-optimismportal/.env new file mode 100644 index 0000000000..42c65a99c5 --- /dev/null +++ b/test/tasks/example/eth/014-noop-call-optimismportal/.env @@ -0,0 +1,3 @@ +TENDERLY_GAS=10000000 +SIMULATE_WITHOUT_LEDGER=true +FORK_BLOCK_NUMBER=23197819 \ No newline at end of file diff --git a/test/tasks/example/eth/014-noop-call-optimismportal/config.toml b/test/tasks/example/eth/014-noop-call-optimismportal/config.toml new file mode 100644 index 0000000000..3c3c71ca2f --- /dev/null +++ b/test/tasks/example/eth/014-noop-call-optimismportal/config.toml @@ -0,0 +1,13 @@ +templateName = "L1PortalExecuteL2Call" + +# Portal + L2 call params +portal = "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed" # L1 OptimismPortal +l2Target = "0xcDF27F107725988f2261Ce2256bDfCdE8B382B10" # OptimismGovernor Proxy +l2Data = "0x3659cfe6000000000000000000000000ecbf4ed9f47302f00f0f039a691e7db83bdd2624" # upgradeTo(currentImpl) -> 0xecbf4ed9f47302f00f0f039a691e7db83bdd2624 +gasLimit = 500000 +value = 0 +isCreation = false + +[addresses] +ProxyAdminOwner = "0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A" # 2-of-2 between council and foundation +OptimismPortal = "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed" \ No newline at end of file