Skip to content

Commit

Permalink
Add verification framework & FP dispute game module (#429)
Browse files Browse the repository at this point in the history
Developed in #421 and now split into its own PR.
  • Loading branch information
sebastianst authored Dec 19, 2024
1 parent c04360d commit 3f63f70
Show file tree
Hide file tree
Showing 7 changed files with 423 additions and 2 deletions.
23 changes: 23 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,28 @@ parameters:
type: integer
default: 5

commands:
simulate_nested:
description: "Runs simulations of a nested task"
parameters:
task:
type: string
steps:
- checkout
- run:
name: "simulate nested << parameters.task >>"
command: |
just install
cd tasks/<< parameters.task >>
SIMULATE_WITHOUT_LEDGER=true just \
--dotenv-path $(pwd)/.env \
--justfile ../../../nested.just \
simulate foundation
SIMULATE_WITHOUT_LEDGER=true just \
--dotenv-path $(pwd)/.env \
--justfile ../../../nested.just \
simulate council
jobs:
check_sepolia_rpc_endpoints:
circleci_ip_ranges: true
Expand Down Expand Up @@ -186,6 +208,7 @@ jobs:
just install
forge --version
forge build --deny-warnings
forge_fmt:
docker:
- image: <<pipeline.parameters.ci_builder_image>>
Expand Down
102 changes: 102 additions & 0 deletions NESTED-VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Validation - Nested Safe

This document describes the generic validation steps for running a Mainnet or Sepolia tasks for the
nested 2/2 Security Council/Foundation Safe.

## State Overrides

The following state overrides related to the nested Safe execution must be seen:

### `GnosisSafeProxy` - the 2/2 `ProxyAdminOwner` Safe

The `ProxyAdminOwner` has the following address:
- Mainnet: [`0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A`](https://etherscan.io/address/0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A)
- Sepolia: [`0x1Eb2fFc903729a0F03966B917003800b145F56E2`](https://sepolia.etherscan.io/address/0x1Eb2fFc903729a0F03966B917003800b145F56E2)

These addresses are attested to in the [Optimism Docs](https://docs.optimism.io/chain/security/privileged-roles#addresses).

Enables the simulation by setting the threshold to 1:

- **Key:** `0x0000000000000000000000000000000000000000000000000000000000000004` <br/>
**Value:** `0x0000000000000000000000000000000000000000000000000000000000000001`
**Meaning:** The threshold is set to 1.

### Security Council Safe or Foundation Safe

Depending on which role (Security Council or Foundation) the task was simulated for,
you must see the following overrides for the following address:
- Mainnet
- Council Safe: [`0xc2819DC788505Aac350142A7A707BF9D03E3Bd03`](https://etherscan.io/address/0xc2819DC788505Aac350142A7A707BF9D03E3Bd03)
- Foundation Safe: [`0x847B5c174615B1B7fDF770882256e2D3E95b9D92`](https://etherscan.io/address/0x847B5c174615B1B7fDF770882256e2D3E95b9D92)
- Sepolia
- Council Safe: [`0xf64bc17485f0B4Ea5F06A96514182FC4cB561977`](https://sepolia.etherscan.io/address/0xf64bc17485f0B4Ea5F06A96514182FC4cB561977)
- Foundation Safe: [`0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B`](https://sepolia.etherscan.io/address/0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B)

The simulated role will also be called the **Safe Signer** in the remaining document.

These addresses can be verified as the owners of the 2/2 `ProxyAdminOwner` Safe described above.

The Safe Signer will have the following overrides which will set the [Multicall](https://sepolia.etherscan.io/address/0xca11bde05977b3631167028862be2a173976ca11#code) contract as the sole owner of the signing Safe. This allows simulating both the approve hash and the final tx in a single Tenderly tx.

- **Key:** 0x0000000000000000000000000000000000000000000000000000000000000003 <br/>
**Value:** 0x0000000000000000000000000000000000000000000000000000000000000001 <br/>
**Meaning:** The number of owners is set to 1.

- **Key:** 0x0000000000000000000000000000000000000000000000000000000000000004 <br/>
**Value:** 0x0000000000000000000000000000000000000000000000000000000000000001 <br/>
**Meaning:** The threshold is set to 1.

The following two overrides are modifications to the [`owners` mapping](https://github.com/safe-global/safe-contracts/blob/v1.4.0/contracts/libraries/SafeStorage.sol#L15). For the purpose of calculating the storage, note that this mapping is in slot `2`.
This mapping implements a linked list for iterating through the list of owners. Since we'll only have one owner, `Multicall3` (`0xca11bde05977b3631167028862be2a173976ca11` on [Mainnet](https://etherscan.io/address/0xca11bde05977b3631167028862be2a173976ca11) and [Sepolia](https://sepolia.etherscan.io/address/0xca11bde05977b3631167028862be2a173976ca11)), and the `0x01` address is used as the first and last entry in the linked list, we will see the following overrides:
- `owners[1] -> 0xca11bde05977b3631167028862be2a173976ca11`
- `owners[0xca11bde05977b3631167028862be2a173976ca11] -> 1`

And we do indeed see these entries:

- **Key:** 0x316a0aac0d94f5824f0b66f5bbe94a8c360a17699a1d3a233aafcf7146e9f11c <br/>
**Value:** 0x0000000000000000000000000000000000000000000000000000000000000001 <br/>
**Meaning:** This is `owners[0xca11bde05977b3631167028862be2a173976ca11] -> 1`, so the key can be
derived from `cast index address 0xca11bde05977b3631167028862be2a173976ca11 2`.

- **Key:** 0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0 <br/>
**Value:** 0x000000000000000000000000ca11bde05977b3631167028862be2a173976ca11 <br/>
**Meaning:** This is `owners[1] -> 0xca11bde05977b3631167028862be2a173976ca11`, so the key can be
derived from `cast index address 0x0000000000000000000000000000000000000001 2`.

## State Changes

The following state changes related to the nested Safe execution must be seen, either for the
Security Council, or the Foundation Safe, depending on which role the simulation was run for:

### `GnosisSafeProxy` - `approvedHashes` mapping update

- **Key:** _Needs to be computed._ <br/>
**Before:** `0x0000000000000000000000000000000000000000000000000000000000000000`<br/>
**After:** `0x0000000000000000000000000000000000000000000000000000000000000001` <br/>

#### Key Computation

The GnosisSafe `approvedHashes` mapping is updated to indicate approval of this transaction by the Safe Signer. The correctness of this slot can be verified as follows:
- Since this is a nested mapping, we need to use `cast index` twice to confirm that this is the correct slot. The inputs needed are:
- The location (`8`) of the `approvedHashes` mapping in the [GnosisSafe storage layout](https://github.com/safe-global/safe-contracts/blob/v1.4.0/contracts/libraries/SafeStorage.sol#L23)
- The address of the Safe Signer, stored at the env var `$SAFE_SIGNER` in the following cast script command.
- The safe hash to approve, stored at the env var `$SAFE_HASH` in the following cast script command.
It's the value after "Nested hash:" in the simulation output logs.
- Then using `cast index`, we can compute the key with
```shell
$ cast index bytes32 $SAFE_HASH $(cast index address $SAFE_SIGNER 8)
```
The output of this command must match the key of the state change.

### Liveness Guard

When the Security Council executes a transaction, the liveness timestamp are updated for each owner that signed the tasks.
This is updating at the moment of the transaction is submitted (`block.timestamp`) into the [`lastLive`](https://github.com/ethereum-optimism/optimism/blob/e84868c27776fd04dc77e95176d55c8f6b1cc9a3/packages/contracts-bedrock/src/safe/LivenessGuard.sol#L41) mapping located at the slot `0`.

### Nonce increments

The only other state changes related to the nested execution are _three_ nonce increments:

- One on the `ProxyAdminOwner` 2/2. If this is not decoded, it corresponds to key `0x05` on a `GnosisSafeProxy`.
- One on the Council or Foundation Safe. If this is not decoded, it corresponds to key `0x05` on a `GnosisSafeProxy`.
- One of the EOA that is the first entry in the owner set of the simulated role.
2 changes: 1 addition & 1 deletion lib/solady
Submodule solady updated 185 files
2 changes: 1 addition & 1 deletion lib/superchain-registry
42 changes: 42 additions & 0 deletions script/verification/CouncilFoundationNestedSign.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import {VerificationBase} from "script/verification/Verification.s.sol";
import {CommonBase} from "forge-std/Base.sol";
import {GnosisSafe} from "safe-contracts/GnosisSafe.sol";

contract CouncilFoundationNestedSign is VerificationBase, CommonBase {
GnosisSafe councilSafe = GnosisSafe(payable(vm.envAddress("COUNCIL_SAFE")));
GnosisSafe fndSafe = GnosisSafe(payable(vm.envAddress("FOUNDATION_SAFE")));
GnosisSafe ownerSafe = GnosisSafe(payable(vm.envAddress("OWNER_SAFE")));

// The slot used to store the livenessGuard address in GnosisSafe.
// See https://github.com/safe-global/safe-smart-account/blob/186a21a74b327f17fc41217a927dea7064f74604/contracts/base/GuardManager.sol#L30
bytes32 constant livenessGuardSlot = 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8;

constructor() {
_addCodeExceptions();
_addAllowedStorageAccesses();
}

function _addCodeExceptions() internal {
address[] memory securityCouncilSafeOwners = councilSafe.getOwners();
for (uint256 i = 0; i < securityCouncilSafeOwners.length; i++) {
address owner = securityCouncilSafeOwners[i];
if (securityCouncilSafeOwners[i].code.length == 0) {
addCodeException(owner);
}
}
}

function _addAllowedStorageAccesses() internal {
addAllowedStorageAccess(address(councilSafe));
addAllowedStorageAccess(address(fndSafe));
addAllowedStorageAccess(address(ownerSafe));
addAllowedStorageAccess(livenessGuard());
}

function livenessGuard() public view returns (address) {
return address(uint160(uint256(vm.load(address(councilSafe), livenessGuardSlot))));
}
}
89 changes: 89 additions & 0 deletions script/verification/DisputeGameUpgrade.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import {console2 as console} from "forge-std/console2.sol";
import {Vm} from "forge-std/Vm.sol";
import {LibString} from "solady/utils/LibString.sol";
import {VerificationBase, SuperchainRegistry} from "script/verification/Verification.s.sol";
import "@eth-optimism-bedrock/src/dispute/lib/Types.sol";
import {FaultDisputeGame} from "@eth-optimism-bedrock/src/dispute/FaultDisputeGame.sol";
import {PermissionedDisputeGame} from "@eth-optimism-bedrock/src/dispute/PermissionedDisputeGame.sol";
import {DisputeGameFactory} from "@eth-optimism-bedrock/src/dispute/DisputeGameFactory.sol";
import {MIPS} from "@eth-optimism-bedrock/src/cannon/MIPS.sol";
import {ISemver} from "@eth-optimism-bedrock/src/universal/ISemver.sol";
import {Simulation} from "@base-contracts/script/universal/Simulation.sol";
import {NestedMultisigBuilder} from "@base-contracts/script/universal/NestedMultisigBuilder.sol";

interface IASR {
function superchainConfig() external view returns (address superchainConfig_);
}

abstract contract DisputeGameUpgrade is VerificationBase, SuperchainRegistry {
using LibString for string;

bytes32 immutable expAbsolutePrestate;
address immutable expFaultDisputeGame;
address immutable expPermissionedDisputeGame;

constructor(bytes32 _absolutePrestate, address _faultDisputeGame, address _permissionedDisputeGame) {
expAbsolutePrestate = _absolutePrestate;
expFaultDisputeGame = _faultDisputeGame;
expPermissionedDisputeGame = _permissionedDisputeGame;

addAllowedStorageAccess(proxies.DisputeGameFactory);
}

/// @notice Public function that must be called by the verification script.
function checkDisputeGameUpgrade() public view {
console.log("check dispute game implementations");

DisputeGameFactory dgfProxy = DisputeGameFactory(proxies.DisputeGameFactory);
FaultDisputeGame faultDisputeGame = FaultDisputeGame(address(dgfProxy.gameImpls(GameTypes.CANNON)));
PermissionedDisputeGame permissionedDisputeGame =
PermissionedDisputeGame(address(dgfProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)));

require(expFaultDisputeGame == address(faultDisputeGame), "game-100");
require(expPermissionedDisputeGame == address(permissionedDisputeGame), "game-110");

require(faultDisputeGame.version().eq(standardVersions.FaultDisputeGame.version), "game-200");
require(permissionedDisputeGame.version().eq(standardVersions.PermissionedDisputeGame.version), "game-210");

require(faultDisputeGame.absolutePrestate().raw() == expAbsolutePrestate, "game-300");
require(permissionedDisputeGame.absolutePrestate().raw() == expAbsolutePrestate, "game-310");

require(faultDisputeGame.l2ChainId() == chainConfig.chainId, "game-400");
require(permissionedDisputeGame.l2ChainId() == chainConfig.chainId, "game-410");

console.log("check mips");

require(address(faultDisputeGame.vm()) == standardVersions.MIPS.Address, "mips-100");
require(address(permissionedDisputeGame.vm()) == standardVersions.MIPS.Address, "mips-110");

require(ISemver(standardVersions.MIPS.Address).version().eq(standardVersions.MIPS.version), "mips-200");
require(
address(MIPS(standardVersions.MIPS.Address).oracle()) == standardVersions.PreimageOracle.Address, "mips-300"
);

console.log("check anchor state registry");

require(address(faultDisputeGame.anchorStateRegistry()) == proxies.AnchorStateRegistry, "asr-100");
require(address(permissionedDisputeGame.anchorStateRegistry()) == proxies.AnchorStateRegistry, "asr-110");

require(
ISemver(proxies.AnchorStateRegistry).version().eq(standardVersions.AnchorStateRegistry.version), "asr-200"
);
require(IASR(proxies.AnchorStateRegistry).superchainConfig() == proxies.SuperchainConfig, "asr-300");

console.log("check delayed weth");

require(
ISemver(address(faultDisputeGame.weth())).version().eq(standardVersions.DelayedWETH.version), "weth-100"
);
require(
ISemver(address(permissionedDisputeGame.weth())).version().eq(standardVersions.DelayedWETH.version),
"weth-110"
);

require(address(faultDisputeGame.weth()) != address(permissionedDisputeGame.weth()), "weth-200");
}
}
Loading

0 comments on commit 3f63f70

Please sign in to comment.