Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 10 additions & 0 deletions sepolia/2026-07-24-incident-multisig-signers/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
BASE_CONTRACTS_COMMIT=e225648a7ed538e7e28c041d44f3b7a606ba7743

# Sepolia Incident Multisig.
OWNER_SAFE=0x646132A1667ca7aD00d36616AFBA1A28116C770A

# Sepolia mock Security Council Safe.
SECURITY_COUNCIL_SAFE=0x6AF0674791925f767060Dd52f7fB20984E8639d8

# Required for the task signer tool.
RECORD_STATE_DIFF=true
43 changes: 43 additions & 0 deletions sepolia/2026-07-24-incident-multisig-signers/FACILITATOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Facilitator Guide

The same signer diff is applied to both Safes:

- Incident Multisig: `OWNER_SAFE` in [.env](./.env)
- Mock Security Council Safe: `SECURITY_COUNCIL_SAFE` in [.env](./.env)

## Generate validation files

```bash
cd contract-deployments/sepolia/2026-07-24-incident-multisig-signers
make deps
make gen-validation
```

This produces:

- `validations/base-signer.json`
- `validations/security-council-signer.json`

Do not generate validation files until `.env` and [OwnerDiff.json](./OwnerDiff.json) are final.

## Execute the transactions

### Incident Multisig

Collect enough Incident Multisig signatures, concatenate them, and run:

```bash
export SIGNATURES="[SIGNATURE1][SIGNATURE2]..."
SIGNATURES=$SIGNATURES make execute-incident
```

### Mock Security Council Safe

Collect enough mock Security Council Safe signatures, concatenate them, and run:

```bash
export SIGNATURES="[SIGNATURE1][SIGNATURE2]..."
SIGNATURES=$SIGNATURES make execute-security-council
```

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

SIGNER_TOOL_PATH = ../../signer-tool
RPC_URL = $(L1_RPC_URL)
SCRIPT_NAME = script/UpdateSigners.s.sol:UpdateSigners

INCIDENT_SENDER := $(shell $(MISE_EXEC) cast call $(OWNER_SAFE) "getOwners()(address[])" --rpc-url $(RPC_URL) | tr -d '[]' | cut -d',' -f1)
SECURITY_COUNCIL_SENDER := $(shell $(MISE_EXEC) cast call $(SECURITY_COUNCIL_SAFE) "getOwners()(address[])" --rpc-url $(RPC_URL) | tr -d '[]' | cut -d',' -f1)

.PHONY: validate-config
validate-config:
@test -n "$(BASE_CONTRACTS_COMMIT)" || (echo "BASE_CONTRACTS_COMMIT required" && exit 1)
@test -n "$(OWNER_SAFE)" || (echo "OWNER_SAFE required" && exit 1)
@test -n "$(SECURITY_COUNCIL_SAFE)" || (echo "SECURITY_COUNCIL_SAFE required" && exit 1)

.PHONY: deps
deps: task-extra-deps

.PHONY: task-extra-deps
task-extra-deps:
$(MISE_EXEC) forge install --no-git safe-global/safe-smart-account@186a21a74b327f17fc41217a927dea7064f74604

.PHONY: gen-validation
gen-validation: gen-validation-incident gen-validation-security-council

.PHONY: gen-validation-incident
gen-validation-incident: validate-config deps-signer-tool
mkdir -p validations
$(call GEN_VALIDATION,$(SCRIPT_NAME),,$(INCIDENT_SENDER),base-signer.json,)

.PHONY: gen-validation-security-council
gen-validation-security-council: validate-config deps-signer-tool
mkdir -p validations
$(call GEN_VALIDATION,$(SCRIPT_NAME),,$(SECURITY_COUNCIL_SENDER),security-council-signer.json,OWNER_SAFE=$(SECURITY_COUNCIL_SAFE))

.PHONY: execute-incident
execute-incident: validate-config
$(call MULTISIG_EXECUTE,$(SIGNATURES))

.PHONY: execute-security-council
execute-security-council: validate-config
OWNER_SAFE=$(SECURITY_COUNCIL_SAFE) $(call MULTISIG_EXECUTE,$(SIGNATURES))
8 changes: 8 additions & 0 deletions sepolia/2026-07-24-incident-multisig-signers/OwnerDiff.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"OwnersToAdd": [
"0x2c1475476B586d66a85bC65A5aB396BBbAa4f3aD"
],
"OwnersToRemove": [
"0x72069542Ca378553A3D479c82Adc31f21CA6dE4a"
]
}
18 changes: 18 additions & 0 deletions sepolia/2026-07-24-incident-multisig-signers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Update Sepolia Incident Multisig Signers

Status: READY TO SIGN

## Description

This task updates the owner sets for the Sepolia Incident Multisig and mock Security Council Safe.

It replaces `0x72069542Ca378553A3D479c82Adc31f21CA6dE4a` with `0x2c1475476B586d66a85bC65A5aB396BBbAa4f3aD` on both Safes. The signer changes are configured in [OwnerDiff.json](./OwnerDiff.json).

## Procedure

1. Run `make sign-task` from `contract-deployments`.
2. Open [http://localhost:3000](http://localhost:3000) and select `sepolia/2026-07-24-incident-multisig-signers`.
3. Select `base-signer.json` for the Incident Multisig or `security-council-signer.json` for the mock Security Council Safe, then sign the task.
4. Send the signature to the facilitator.

See `FACILITATOR.md` for execution instructions.
26 changes: 26 additions & 0 deletions sepolia/2026-07-24-incident-multisig-signers/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[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
evm_version = "shanghai"
remappings = [
'@base-contracts/=lib/contracts',
'@lib-keccak/=lib/lib-keccak/contracts/lib/',
'@solady/=lib/solady/src/',
'forge-std/=lib/forge-std/src/',
'interfaces/L1/=lib/contracts/interfaces/L1/',
'interfaces/legacy/=lib/contracts/interfaces/legacy/',
'interfaces/universal/=lib/contracts/interfaces/universal/',
'src/libraries/=lib/contracts/src/libraries/',
]

[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,149 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import {Vm} from "forge-std/Vm.sol";
import {stdJson} from "forge-std/StdJson.sol";

import {MultisigScript, Enum} from "@base-contracts/scripts/universal/MultisigScript.sol";
import {Simulation} from "@base-contracts/scripts/universal/Simulation.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 EXPECTED_OWNERS_TO_ADD = 1;
uint256 public constant EXPECTED_OWNERS_TO_REMOVE = 1;

address public immutable OWNER_SAFE = vm.envAddress("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() {
GnosisSafe ownerSafe = GnosisSafe(payable(OWNER_SAFE));
THRESHOLD = ownerSafe.getThreshold();
EXISTING_OWNERS = ownerSafe.getOwners();

string memory path = string.concat(vm.projectRoot(), "/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 == EXPECTED_OWNERS_TO_ADD, "Precheck 00");
require(OWNERS_TO_REMOVE.length == EXPECTED_OWNERS_TO_REMOVE, "Precheck 01");
require(EXISTING_OWNERS.length + OWNERS_TO_ADD.length >= OWNERS_TO_REMOVE.length, "Precheck 02");

uint256 expectedLength = EXISTING_OWNERS.length + OWNERS_TO_ADD.length - OWNERS_TO_REMOVE.length;
require(expectedLength >= THRESHOLD, "Precheck 03");

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

for (uint256 i = OWNERS_TO_ADD.length; i > 0; i--) {
uint256 index = i - 1;
address ownerToAdd = OWNERS_TO_ADD[index];

_validateOwnerAddress(ownerToAdd);
require(!ownerSafe.isOwner(ownerToAdd), "Precheck 04");
require(!expectedOwner[ownerToAdd], "Precheck 05");

ownerToPrevOwner[ownerToAdd] = prevOwner;
ownerToNextOwner[prevOwner] = ownerToAdd;
prevOwner = ownerToAdd;
expectedOwner[ownerToAdd] = true;
}

for (uint256 i; i < EXISTING_OWNERS.length; i++) {
_validateOwnerAddress(EXISTING_OWNERS[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++) {
address ownerToRemove = OWNERS_TO_REMOVE[i];

_validateOwnerAddress(ownerToRemove);
require(ownerSafe.isOwner(ownerToRemove), "Precheck 06");
require(expectedOwner[ownerToRemove], "Precheck 07");

expectedOwner[ownerToRemove] = false;

address nextOwner = ownerToNextOwner[ownerToRemove];
address prevPtr = ownerToPrevOwner[ownerToRemove];
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 < OWNERS_TO_ADD.length; i++) {
require(ownerSafe.isOwner(OWNERS_TO_ADD[i]), "Postcheck 02");
}

for (uint256 i; i < OWNERS_TO_REMOVE.length; i++) {
require(!ownerSafe.isOwner(OWNERS_TO_REMOVE[i]), "Postcheck 03");
}

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

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;
}

function _validateOwnerAddress(address owner) internal pure {
require(owner != address(0), "owner zero");
require(owner != SENTINEL_OWNERS, "owner sentinel");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"cmd": "mise exec -- forge script --rpc-url https://ethereum-full-sepolia-k8s-dev.cbhq.net script/UpdateSigners.s.sol:UpdateSigners --sig sign(address[]) [] --sender 0x72069542Ca378553A3D479c82Adc31f21CA6dE4a",
"ledgerId": 1,
"rpcUrl": "https://ethereum-full-sepolia-k8s-dev.cbhq.net",
"expectedDomainAndMessageHashes": {
"address": "0x646132A1667ca7aD00d36616AFBA1A28116C770A",
"domainHash": "0x1d3f2566fd7b1bf017258b03d4d4d435d326d9cb051d5b7993d7c65e7ec78d0e",
"messageHash": "0x675626096deeedd6a81e059ed08a267ea84419754dd808d0cd68299e0b970e89"
},
"stateOverrides": [
{
"name": "CB Coordinator Safe - Sepolia",
"address": "0x646132A1667ca7aD00d36616AFBA1A28116C770A",
"overrides": [
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000004",
"value": "0x0000000000000000000000000000000000000000000000000000000000000001",
"description": "Override the threshold to 1 so the transaction simulation can occur.",
"allowDifference": false
},
{
"key": "0x1d247253a281beaef726b3b2dc11b8e164aba71a921dca0f997f6c2087b41755",
"value": "0x0000000000000000000000000000000000000000000000000000000000000001",
"description": "Simulates an approval from msg.sender in order for the task simulation to succeed.",
"allowDifference": false
}
]
}
],
"stateChanges": [
{
"name": "CB Coordinator Safe - Sepolia",
"address": "0x646132A1667ca7aD00d36616AFBA1A28116C770A",
"changes": [
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000004",
"before": "0x0000000000000000000000000000000000000000000000000000000000000001",
"after": "0x0000000000000000000000000000000000000000000000000000000000000003",
"description": "Restores the execution threshold from the simulation override value 1 to the expected value 3.",
"allowDifference": false
},
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000005",
"before": "0x000000000000000000000000000000000000000000000000000000000000001d",
"after": "0x000000000000000000000000000000000000000000000000000000000000001e",
"description": "Increments the Safe nonce from 29 to 30.",
"allowDifference": false
},
{
"key": "0xceb0c5d13f4f619e5f8efdae7187fef9059df56bd34e0650a02c1c72dd716dfa",
"before": "0x000000000000000000000000821ff2a3fb66b008fa668b40eca9d3535b246575",
"after": "0x0000000000000000000000000000000000000000000000000000000000000000",
"description": "Removes `0x72069542Ca378553A3D479c82Adc31f21CA6dE4a` from the owners mapping by clearing its pointer to `0x821Ff2A3fB66B008fA668B40Eca9d3535B246575`.",
"allowDifference": false
},
{
"key": "0xd7f94929019af9a6efac262a2ba7cd160a70f89d6d1d5457a59e07618487bacb",
"before": "0x0000000000000000000000000000000000000000000000000000000000000000",
"after": "0x000000000000000000000000821ff2a3fb66b008fa668b40eca9d3535b246575",
"description": "Adds `0x2c1475476B586d66a85bC65A5aB396BBbAa4f3aD` to the owners mapping and points it to `0x821Ff2A3fB66B008fA668B40Eca9d3535B246575`, the next retained owner after removed head owner `0x72069542Ca378553A3D479c82Adc31f21CA6dE4a`.",
"allowDifference": false
},
{
"key": "0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0",
"before": "0x00000000000000000000000072069542ca378553a3d479c82adc31f21ca6de4a",
"after": "0x0000000000000000000000002c1475476b586d66a85bc65a5ab396bbbaa4f3ad",
"description": "Sets the head of the owners linked list to newly added owner `0x2c1475476B586d66a85bC65A5aB396BBbAa4f3aD`.",
"allowDifference": false
}
]
}
],
"balanceChanges": [],
"skipTaskOriginValidation": true
}
Loading
Loading