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
8 changes: 8 additions & 0 deletions sepolia/2025-11-03-incident-multisig-signers/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
OP_COMMIT=594bc933a38425f745b46399a3619bcdeb74965d
BASE_CONTRACTS_COMMIT=dcd8c98aa881e0ae4ebf872e0d91692a7bf94000

OWNER_SAFE=0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f

SENDER=0x0CF2F86C3338993ce10F74d6f4B095712c7efe26

RECORD_STATE_DIFF=true
33 changes: 33 additions & 0 deletions sepolia/2025-11-03-incident-multisig-signers/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
include ../../Makefile
include ../../Multisig.mk
include ../.env
include .env

RPC_URL = $(L1_RPC_URL)
SCRIPT = UpdateSigners

ifndef LEDGER_ACCOUNT
override LEDGER_ACCOUNT = 1
endif

.PHONY: deps
deps: new-forge-deps

.PHONY: new-forge-deps
new-forge-deps:
forge install --no-git safe-global/safe-smart-account@186a21a74b327f17fc41217a927dea7064f74604

.PHONY: gen-validation
gen-validation: checkout-signer-tool run-script

.PHONY: run-script
run-script:
cd $(SIGNER_TOOL_PATH); \
npm ci; \
bun run scripts/genValidationFile.ts --rpc-url $(L1_RPC_URL) \
--workdir .. --forge-cmd 'forge script --rpc-url $(L1_RPC_URL) \
$(SCRIPT) --sig "sign(address[])" [] --sender $(SENDER)' --out ../validations/base-signer.json;

.PHONY: execute
execute:
$(call MULTISIG_EXECUTE,$(SIGNATURES))
11 changes: 11 additions & 0 deletions sepolia/2025-11-03-incident-multisig-signers/OwnerDiff.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"OwnersToAdd": [
"0x5F1955729e3D6129FB73813E891742D473fc74F7",
"0x6e427c3212C0b63BE0C382F97715D49b011bFF33"
],
"OwnersToRemove": [
"0xaa2489DEbF1EF02ab83bA6Cde4419E662De9254E",
"0x9B43cC2EF6fa521127Ade09e20efD6ABBC5BF799",
"0xf2Fb17eab635f036Da7864B8e39ef8e9A03441df"
]
}
52 changes: 52 additions & 0 deletions sepolia/2025-11-03-incident-multisig-signers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Update Sepolia Incident Multisig Signers

Status: READY TO SIGN

## Description

We wish to update the owners of our Incident Multisig to be consistent with the current state of our Base Chain Eng team. This involves removing signers that are no longer closely involved with the team, and adding new team members as signers. The exact signer changes are outlined in the [OwnerDiff.json](./OwnerDiff.json) file.

## Install dependencies

### 1. Update foundry

```bash
foundryup
```

### 2. Install Node.js if needed

First, check if you have node installed

```bash
node --version
```

If you see a version output from the above command, at or above version `v18.18`, you can move on. Otherwise, install (or update) node.

```bash
brew install node
```

## Approving Signers Update

### 1. Update repo:

```bash
cd contract-deployments
git pull
```

### 2. Run the signing tool (NOTE: do not enter the task directory. Run this command from the project's root).

```bash
make sign-task
```

### 3. Open the UI at [http://localhost:3000](http://localhost:3000)

Be sure to select the correct task from the list of available tasks to sign.

### 4. Send signature to facilitator

You may now kill the Signer Tool process in your terminal window by running `Ctrl + C`.
20 changes: 20 additions & 0 deletions sepolia/2025-11-03-incident-multisig-signers/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
broadcast = 'records'
fs_permissions = [{ access = "read-write", path = "./" }]
optimizer = true
optimizer_runs = 999999
solc_version = "0.8.15"
via-ir = false
remappings = [
'@eth-optimism-bedrock/=lib/optimism/packages/contracts-bedrock/',
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts',
'@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts',
'@rari-capital/solmate/=lib/solmate/',
'@base-contracts/=lib/base-contracts',
'solady/=lib/solady/src/',
]

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import {Vm} from "forge-std/Vm.sol";
import {stdJson} from "forge-std/StdJson.sol";
import {Simulation} from "@base-contracts/script/universal/Simulation.sol";
import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol";

import {MultisigScript} from "@base-contracts/script/universal/MultisigScript.sol";
import {GnosisSafe} from "safe-smart-account/GnosisSafe.sol";
import {OwnerManager} from "safe-smart-account/base/OwnerManager.sol";

contract UpdateSigners is MultisigScript {
using stdJson for string;

address public constant SENTINEL_OWNERS = address(0x1);

uint256 public constant EXISTING_OWNERS_LENGTH = 14;

address public immutable OWNER_SAFE;
uint256 public immutable THRESHOLD;
address[] public EXISTING_OWNERS;

address[] public OWNERS_TO_ADD;
address[] public OWNERS_TO_REMOVE;

mapping(address => address) public ownerToPrevOwner;
mapping(address => address) public ownerToNextOwner;
mapping(address => bool) public expectedOwner;

constructor() {
OWNER_SAFE = vm.envAddress("OWNER_SAFE");

GnosisSafe ownerSafe = GnosisSafe(payable(OWNER_SAFE));
THRESHOLD = ownerSafe.getThreshold();
EXISTING_OWNERS = ownerSafe.getOwners();

string memory rootPath = vm.projectRoot();
string memory path = string.concat(rootPath, "/OwnerDiff.json");
string memory jsonData = vm.readFile(path);

OWNERS_TO_ADD = abi.decode(jsonData.parseRaw(".OwnersToAdd"), (address[]));
OWNERS_TO_REMOVE = abi.decode(jsonData.parseRaw(".OwnersToRemove"), (address[]));
}

function setUp() external {
require(OWNERS_TO_ADD.length > 0, "Precheck 00");
require(OWNERS_TO_REMOVE.length > 0, "Precheck 01");
require(EXISTING_OWNERS.length == EXISTING_OWNERS_LENGTH, "Precheck 02");

GnosisSafe ownerSafe = GnosisSafe(payable(OWNER_SAFE));
address prevOwner = SENTINEL_OWNERS;

for (uint256 i = OWNERS_TO_ADD.length; i > 0; i--) {
uint256 index = i - 1;
// Make sure owners to add are not already owners
require(!ownerSafe.isOwner(OWNERS_TO_ADD[index]), "Precheck 03");
// Prevent duplicates
require(!expectedOwner[OWNERS_TO_ADD[index]], "Precheck 04");

ownerToPrevOwner[OWNERS_TO_ADD[index]] = prevOwner;
ownerToNextOwner[prevOwner] = OWNERS_TO_ADD[index];
prevOwner = OWNERS_TO_ADD[index];
expectedOwner[OWNERS_TO_ADD[index]] = true;
}

for (uint256 i; i < EXISTING_OWNERS.length; i++) {
ownerToPrevOwner[EXISTING_OWNERS[i]] = prevOwner;
ownerToNextOwner[prevOwner] = EXISTING_OWNERS[i];
prevOwner = EXISTING_OWNERS[i];
expectedOwner[EXISTING_OWNERS[i]] = true;
}

for (uint256 i; i < OWNERS_TO_REMOVE.length; i++) {
// Make sure owners to remove are owners
require(ownerSafe.isOwner(OWNERS_TO_REMOVE[i]), "Precheck 05");
// Prevent duplicates
require(expectedOwner[OWNERS_TO_REMOVE[i]], "Precheck 06");
expectedOwner[OWNERS_TO_REMOVE[i]] = false;

// Remove from linked list to keep ownerToPrevOwner up to date
// Note: This works as long as the order of OWNERS_TO_REMOVE does not change during `_buildCalls()`
address nextOwner = ownerToNextOwner[OWNERS_TO_REMOVE[i]];
address prevPtr = ownerToPrevOwner[OWNERS_TO_REMOVE[i]];
ownerToPrevOwner[nextOwner] = prevPtr;
ownerToNextOwner[prevPtr] = nextOwner;
}
}

function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal view override {
GnosisSafe ownerSafe = GnosisSafe(payable(OWNER_SAFE));
address[] memory postCheckOwners = ownerSafe.getOwners();
uint256 postCheckThreshold = ownerSafe.getThreshold();

uint256 expectedLength = EXISTING_OWNERS.length + OWNERS_TO_ADD.length - OWNERS_TO_REMOVE.length;

require(postCheckThreshold == THRESHOLD, "Postcheck 00");
require(postCheckOwners.length == expectedLength, "Postcheck 01");

for (uint256 i; i < postCheckOwners.length; i++) {
require(expectedOwner[postCheckOwners[i]], "Postcheck 02");
}
}

function _buildCalls() internal view override returns (IMulticall3.Call3Value[] memory) {
IMulticall3.Call3Value[] memory calls =
new IMulticall3.Call3Value[](OWNERS_TO_ADD.length + OWNERS_TO_REMOVE.length);

for (uint256 i; i < OWNERS_TO_ADD.length; i++) {
calls[i] = IMulticall3.Call3Value({
target: OWNER_SAFE,
allowFailure: false,
callData: abi.encodeCall(OwnerManager.addOwnerWithThreshold, (OWNERS_TO_ADD[i], THRESHOLD)),
value: 0
});
}

for (uint256 i; i < OWNERS_TO_REMOVE.length; i++) {
calls[OWNERS_TO_ADD.length + i] = IMulticall3.Call3Value({
target: OWNER_SAFE,
allowFailure: false,
callData: abi.encodeCall(
OwnerManager.removeOwner, (ownerToPrevOwner[OWNERS_TO_REMOVE[i]], OWNERS_TO_REMOVE[i], THRESHOLD)
),
value: 0
});
}

return calls;
}

function _ownerSafe() internal view override returns (address) {
return OWNER_SAFE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
{
"cmd": "forge script --rpc-url https://ethereum-full-sepolia-k8s-dev.cbhq.net UpdateSigners --sig sign(address[]) [] --sender 0x0CF2F86C3338993ce10F74d6f4B095712c7efe26",
"ledgerId": 0,
"rpcUrl": "https://ethereum-full-sepolia-k8s-dev.cbhq.net",
"expectedDomainAndMessageHashes": {
"address": "0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f",
"domainHash": "0x0127bbb910536860a0757a9c0ffcdf9e4452220f566ed83af1f27f9e833f0e23",
"messageHash": "0xd672c948194b6dcd59f2bd289d5cbe2830861f632e9697d3502b8f4df4a46e2e"
},
"stateOverrides": [
{
"name": "CB Signer Safe - Sepolia",
"address": "0x5dfeb066334b67355a15dc9b67317fd2a2e1f77f",
"overrides": [
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000004",
"value": "0x0000000000000000000000000000000000000000000000000000000000000001",
"description": "Override the threshold to 1 so the transaction simulation can occur."
},
{
"key": "0x5184e114a07bb2874fba528601c90cda5eae014b6c6f7d81c92277500bdb0848",
"value": "0x0000000000000000000000000000000000000000000000000000000000000001",
"description": "Simulates an approval from msg.sender in order for the task simulation to succeed."
}
]
}
],
"stateChanges": [
{
"name": "CB Signer Safe - Sepolia",
"address": "0x5dfeb066334b67355a15dc9b67317fd2a2e1f77f",
"changes": [
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000003",
"before": "0x000000000000000000000000000000000000000000000000000000000000000e",
"after": "0x000000000000000000000000000000000000000000000000000000000000000d",
"description": "Updates the owner count",
"allowDifference": false
},
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000004",
"before": "0x0000000000000000000000000000000000000000000000000000000000000001",
"after": "0x0000000000000000000000000000000000000000000000000000000000000003",
"description": "Updates the execution threshold",
"allowDifference": false
},
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000005",
"before": "0x000000000000000000000000000000000000000000000000000000000000000a",
"after": "0x000000000000000000000000000000000000000000000000000000000000000b",
"description": "Increments the nonce",
"allowDifference": false
},
{
"key": "0x1b89e6111d27169de2f97ec92e344b249dff7dd6170ee65af7a1d9dda6cf7255",
"before": "0x0000000000000000000000000000000000000000000000000000000000000000",
"after": "0x0000000000000000000000005f1955729e3d6129fb73813e891742d473fc74f7",
"description": "Updates the owners mapping",
"allowDifference": false
},
{
"key": "0xa17f6b5d5c5960e10cb741113b64d7f023e3513339475444585747297bf07208",
"before": "0x0000000000000000000000009b43cc2ef6fa521127ade09e20efd6abbc5bf799",
"after": "0x000000000000000000000000644d0f5c2c55a4679b4bfe057b87ba203af9ac0d",
"description": "Updates the owners mapping",
"allowDifference": false
},
{
"key": "0xb06fd2848181e1497bbaa80b8d007c335c7806a2e47211a7bba7a9af63e0ca64",
"before": "0x000000000000000000000000f2fb17eab635f036da7864b8e39ef8e9a03441df",
"after": "0x000000000000000000000000edecf2c444559210a865a22acfc6a2a25590ab1b",
"description": "Updates the owners mapping",
"allowDifference": false
},
{
"key": "0xc5e3baa34a074e15c9dc99b0b81b573ebbdd07b026df6162630478f6d12892f3",
"before": "0x0000000000000000000000000000000000000000000000000000000000000000",
"after": "0x0000000000000000000000000cf2f86c3338993ce10f74d6f4b095712c7efe26",
"description": "Updates the owners mapping",
"allowDifference": false
},
{
"key": "0xd5a3955556fe832b0d9dee1f7edb84880eee53ea4dc4e838bfa6bebc90480c2a",
"before": "0x000000000000000000000000644d0f5c2c55a4679b4bfe057b87ba203af9ac0d",
"after": "0x0000000000000000000000000000000000000000000000000000000000000000",
"description": "Updates the owners mapping",
"allowDifference": false
},
{
"key": "0xdbe1c9b8b04b880813d25a9c5e89056031fb59c22374b4ee4da23606ff9ee5f5",
"before": "0x000000000000000000000000aa2489debf1ef02ab83ba6cde4419e662de9254e",
"after": "0x0000000000000000000000000000000000000000000000000000000000000001",
"description": "Updates the owners mapping",
"allowDifference": false
},
{
"key": "0xdf293ebdd99010d44bfef5d86d47c7943a4e69db74a426354dc622e6d4d85761",
"before": "0x000000000000000000000000edecf2c444559210a865a22acfc6a2a25590ab1b",
"after": "0x0000000000000000000000000000000000000000000000000000000000000000",
"description": "Updates the owners mapping",
"allowDifference": false
},
{
"key": "0xe475b9e5509f43031ff937d793c47553513dacb8de5b10e556e7da05bd6d6e54",
"before": "0x0000000000000000000000000000000000000000000000000000000000000001",
"after": "0x0000000000000000000000000000000000000000000000000000000000000000",
"description": "Updates the owners mapping",
"allowDifference": false
},
{
"key": "0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0",
"before": "0x0000000000000000000000000cf2f86c3338993ce10f74d6f4b095712c7efe26",
"after": "0x0000000000000000000000006e427c3212c0b63be0c382f97715d49b011bff33",
"description": "Sets the head of the owners linked list",
"allowDifference": false
}
]
}
],
"balanceChanges": []
}
Loading