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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ checkout-base-contracts-commit:
##
# Task Signer Tool
##
SIGNER_TOOL_COMMIT=db50e4234a5475a3f109e61a83fd047924916b41
SIGNER_TOOL_COMMIT=43c55040fee45ec90a33400b10215f1756687ad7
SIGNER_TOOL_PATH=signer-tool

.PHONY: checkout-signer-tool
Expand Down
14 changes: 14 additions & 0 deletions mainnet/2026-05-13-incident-multisig-signers/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Required: Git commit hash for https://github.com/base/contracts
BASE_CONTRACTS_COMMIT=8b5baf316c8dd1011b8b0dcfa9f6eac5a0f216e5

# Network-specific addresses are automatically loaded from {network}/.env via include ../.env

# Required: Address of the Gnosis Safe whose signers will be updated
OWNER_SAFE=0x14536667Cd30e52C0b458BaACcB9faDA7046E056

# Required: Address of a signer on OWNER_SAFE (used for simulation)
# Must also match the sender defined in validations/base-signer.json
SENDER=0x1841CB3C2ce6870D0417844C817849da64E6e937

# Enable state diff recording for validation
RECORD_STATE_DIFF=true
60 changes: 60 additions & 0 deletions mainnet/2026-05-13-incident-multisig-signers/FACILITATOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Facilitator Guide

Guide for facilitators managing the mainnet Incident Multisig signer update.

## Task Origin Signing

After setting up the task, generate cryptographic attestations (sigstore bundles) to prove who created and facilitated the task. These signatures are stored in `mainnet/signatures/2026-05-13-incident-multisig-signers/`.

### Task creator (run after task setup):
```bash
make sign-as-task-creator
```

### Base facilitator:
```bash
make sign-as-base-facilitator
```

### Security Council facilitator:
```bash
make sign-as-sc-facilitator
```

## Generate Validation File

Run this after any change to [OwnerDiff.json](./OwnerDiff.json), [.env](./.env), or [script/UpdateSigners.s.sol](./script/UpdateSigners.s.sol).

```bash
cd contract-deployments
git pull
cd mainnet/2026-05-13-incident-multisig-signers
make deps
make gen-validation
```

This produces `validations/base-signer.json`. Check that the `cmd` field uses:

```text
--sender 0x1841CB3C2ce6870D0417844C817849da64E6e937
```

## Collect Signatures

Ask signers to follow [README.md](./README.md). They should run `make sign-task` from the repo root and select `mainnet/2026-05-13-incident-multisig-signers` in the signing UI.

## Execute

After collecting enough signatures:

```bash
cd contract-deployments
git pull
cd mainnet/2026-05-13-incident-multisig-signers
make deps
SIGNATURES=AAABBBCCC make execute
```

Replace `AAABBBCCC` with the concatenated signatures collected from signers.

After execution, update [README.md](./README.md) status to `EXECUTED` with the transaction link and check in any generated execution records.
22 changes: 22 additions & 0 deletions mainnet/2026-05-13-incident-multisig-signers/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
include ../../Makefile
include ../../Multisig.mk
include ../.env
include .env

RPC_URL = $(L1_RPC_URL)
SCRIPT_NAME = UpdateSigners

.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: deps-signer-tool
$(call GEN_VALIDATION,$(SCRIPT_NAME),,$(SENDER),base-signer.json,)

.PHONY: execute
execute:
$(call MULTISIG_EXECUTE,$(SIGNATURES))
14 changes: 14 additions & 0 deletions mainnet/2026-05-13-incident-multisig-signers/OwnerDiff.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"OwnersToAdd": [
"0x082Cc00d1031a57d53496aBf6dAD8A6247159452",
"0x0c1Ea3aCA9fc2cFa3640fec98a3214A849715b43",
"0x8faB0B6b31A0b50A2c3d1FFBE6C0e1125699aE9d",
"0xD56C6462DC3A943596c7a54d6B0Dba404490E206"
],
"OwnersToRemove": [
"0x4427683AA1f0ff25ccDC4a5Db83010c1DE9b5fF4",
"0xA31E1c38d5c37D8ECd0e94C80C0F7FD624d009A3",
"0x24c3AE1AeDB8142D32BB6d3B988f5910F272D53b",
"0x5468985B560D966dEDEa2DAF493f5756101137DC"
]
}
56 changes: 56 additions & 0 deletions mainnet/2026-05-13-incident-multisig-signers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Update Mainnet Incident Multisig Signers

Status: READY TO SIGN

## Description

We wish to update the owners of our [Incident Multisig](https://etherscan.io/address/0x14536667Cd30e52C0b458BaACcB9faDA7046E056) on Mainnet 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.

The signer changes are configured in [OwnerDiff.json](./OwnerDiff.json), and the simulation sender is configured in [.env](./.env).

## 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, you can move on. Otherwise, install 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.

Task name: `mainnet/2026-05-13-incident-multisig-signers`

### 4. Send signature to facilitator

You may now kill the Signer Tool process in your terminal window by running `Ctrl + C`.
18 changes: 18 additions & 0 deletions mainnet/2026-05-13-incident-multisig-signers/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[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 = [
'@base-contracts/=lib/contracts/',
]

[lint]
lint_on_build = false

# 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,131 @@
// 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/scripts/universal/Simulation.sol";

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

contract UpdateSigners is MultisigScript {
using stdJson for string;

address public constant SENTINEL_OWNERS = address(0x1);

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");

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 (Call[] memory) {
Call[] memory calls = new Call[](OWNERS_TO_ADD.length + OWNERS_TO_REMOVE.length);

for (uint256 i; i < OWNERS_TO_ADD.length; i++) {
calls[i] = Call({
operation: Enum.Operation.Call,
target: OWNER_SAFE,
data: 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] = Call({
operation: Enum.Operation.Call,
target: OWNER_SAFE,
data: 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;
}
}
Loading
Loading