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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ forge-deps:
##
# Task Signer Tool
##
SIGNER_TOOL_COMMIT=1fc76441a1cde3581c224023adfa79b8cfdb2fa4
SIGNER_TOOL_COMMIT=8e059b65ddf936f2fcfd3c820544f8576f74bd35
SIGNER_TOOL_PATH=signer-tool

.PHONY: checkout-signer-tool
Expand Down
7 changes: 3 additions & 4 deletions active/evm/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ evm_version = "prague"
via-ir = false
remappings = [
'@base-contracts/=lib/contracts/',
'@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/',
'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/',
'interfaces/=lib/contracts/interfaces/',
'src/=lib/contracts/src/',
]

[profile.nitro-deploy]
Expand Down
137 changes: 137 additions & 0 deletions active/evm/tasks/2026-09-14-cobalt-upgrade/FACILITATOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Cobalt Upgrade — Facilitator Guide

This task delivers the three Cobalt L1 changes in a single ProxyAdmin-owner transaction:

| Cobalt change | Contracts touched |
| --- | --- |
| Dynamic upgrades | `ProtocolVersions` (new proxy + implementation), `AggregateVerifier` (redeployed to bind it) |
| EthLockbox removal | `OptimismPortal2`, `SystemConfig` |
| CREATE2 for dispute games | `DisputeGameFactory` |

Replace `<network>` with the network you are rolling out, for example `zeronet`. Every command
requires `TASK_NETWORK` explicitly — there is no default. Run everything from this directory
(`active/evm/tasks/2026-09-14-cobalt-upgrade/`).

## 1. Install dependencies

```bash
make TASK_NETWORK=<network> deps
```

This pins `base/contracts` at `BASE_CONTRACTS_COMMIT`, installs the extra OpenZeppelin and solmate
dependencies that building those sources requires, and applies `patch/max-gas-limit.patch`.

The patch matters. Zeronet runs a `SystemConfig` that raises `MAX_GAS_LIMIT` to 2,000,000,000, and
Cobalt modifies `SystemConfig`. Deploying a stock build would quietly drop the chain back to
500,000,000. The patch re-applies the raise and tags the semver `3.14.0+max-gas-limit-2000M`, and
the deploy script refuses to proceed if that version string is missing. On a network that does not
run the patched build, drop `apply-patches` from the `deps` prerequisites and relax the version
assertions in `DeployCobaltCoreImpls` and `ExecuteCobaltUpgrade` to the stock `3.14.0`.

## 2. Review the network config

Open `config/<network>/.env` and confirm every value, in particular:

- `PROTOCOL_VERSIONS_INITIAL_SCHEDULE` — the activation timestamp imported for each upgrade id.
Ids should stay aligned with the Base mainnet registry so that a given index means the same fork
on every chain. Entries may be `0` for an unscheduled fork. Every entry except Cobalt's is a fork
that has already happened, so each one must match the network's chain config in
[base/base](https://github.com/base/base/blob/main/crates/common/chains/src/config.rs), which is
what the nodes actually fork on. Do not assume a re-genesised network has everything active at the
genesis timestamp: on Zeronet, Azul and Beryl activated a few minutes after genesis, and id 7
(PectraBlobSchedule) is unscheduled at `0`. Cobalt (id 12) is the activation this task schedules,
and the node config carries no Cobalt timestamp until its own rollout lands.
- `PROTOCOL_VERSIONS_MINIMUM_PROTOCOL_VERSION` — must be non-zero and fit in 128 bits. The comment
above it gives the `cast` command to re-derive the packed value from the human-readable version.
- `PROTOCOL_VERSIONS_INCIDENT_RESPONDER` — the address allowed to use the incident path.
- `OLD_*` — the currently deployed implementations. `ExecuteCobaltUpgrade` asserts these match the
live proxies before building any calls, so a stale value stops the task rather than upgrading
from an unexpected base.
- `AGGREGATE_VERIFIER_TEE_IMAGE_HASH`, `AGGREGATE_VERIFIER_ZK_RANGE_HASH` and
`AGGREGATE_VERIFIER_ZK_AGGREGATE_HASH` — the proof program hashes, built from the Base node
release these games prove against, which for Zeronet is
[`releases/v1.4.0`](https://github.com/base/base/tree/releases/v1.4.0). The TEE value is PCR0 of
the Nitro enclave image; the two ZK values are the SP1 range and aggregation verification keys
from `just succinct vkeys --build`. The `.env` comment records how each is produced, so they can
be regenerated from that ref rather than taken on trust. They ship blank and the deploy script
refuses to run until they are filled in. Every other `AggregateVerifier` constructor argument,
including the config hash and the block intervals, is read back from the live implementation at
deploy time, so the redeploy cannot change them.

## 3. Deploy

```bash
make TASK_NETWORK=<network> deploy
```

This runs two steps because `base/contracts` pins different optimizer settings per contract, and
reproducible bytecode requires honouring them:

1. `deploy-core` (5000 runs) — `OptimismPortal2`, `SystemConfig`, `DisputeGameFactory`
implementations and the `ProtocolVersions` proxy.
2. `deploy-proofs` (999999 runs) — the `ProtocolVersions` implementation and `AggregateVerifier`.

The proxy is deployed in the first step because `AggregateVerifier` takes its address as a
constructor immutable. It is left pointing at no implementation; the upgrade transaction sets and
initializes it atomically. Each step writes to `config/<network>/addresses.json` and asserts its own
postconditions, so a wrong constructor argument fails at deploy time rather than at signing time.

Then verify the source:

```bash
VERIFIER_API_KEY=<key> make TASK_NETWORK=<network> verify-core
VERIFIER_API_KEY=<key> make TASK_NETWORK=<network> verify-proofs
```

Commit `config/<network>/addresses.json` and the `records/` broadcast artifacts.

## 4. Generate validation files

```bash
make TASK_NETWORK=<network> gen-validation-cb
make TASK_NETWORK=<network> gen-validation-sc
```

These write `config/<network>/validations/base-signer.json` and `security-council-signer.json`.
Commit them, then set the `config/<network>/README.md` status to `READY TO SIGN` and share it with
signers.

## 5. Collect signatures and execute

The ProxyAdmin owner is a 2-of-2 of the Base multisig and the Security Council, so each approves
its own nested Safe before the outer transaction runs.

```bash
SIGNATURES=<concatenated base signatures> make TASK_NETWORK=<network> approve-cb
SIGNATURES=<concatenated security council signatures> make TASK_NETWORK=<network> approve-sc
make TASK_NETWORK=<network> execute
```

`execute` re-runs the same pre- and postconditions inside the broadcast, so it will revert rather
than land a partial upgrade.

## What the upgrade transaction does

Five calls, all from the ProxyAdmin owner Safe:

1. `ProxyAdmin.upgradeAndCall(protocolVersionsProxy, protocolVersionsImpl, initialize(...))` —
`initialize` is `reinitializer(1)` on a never-initialized proxy, so it must be bundled with the
implementation set rather than sent separately.
2. `ProxyAdmin.upgrade(optimismPortal, newImpl)`
3. `ProxyAdmin.upgrade(systemConfig, newImpl)`
4. `ProxyAdmin.upgrade(disputeGameFactory, newImpl)`
5. `DisputeGameFactory.setImplementation(621, newAggregateVerifier)`

Calls 2–4 are bare upgrades: none of those implementations adds state or bumps its init version, so
there is nothing to reinitialize.

## Worth re-checking before signing

- **Every changed contract bumps its version.** `OptimismPortal2` goes `5.2.0` -> `6.0.0`,
`DisputeGameFactory` `1.4.0` -> `1.5.0`, `AggregateVerifier` `0.1.0` -> `0.2.0`, and `SystemConfig`
`3.13.2+max-gas-limit-2000M` -> `3.14.0+max-gas-limit-2000M`, so `version()` read through each
proxy is a sound check that the upgrade landed.
- **The Cobalt activation is scheduled, so the transaction is time-sensitive.**
`ProtocolVersions.initialize` enforces one hour of notice on future timestamps, so it reverts if
the upgrade lands within the hour before the configured Cobalt activation. Execute well before
that window, or push the timestamp out.
205 changes: 205 additions & 0 deletions active/evm/tasks/2026-09-14-cobalt-upgrade/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
include ../../../../Makefile
include $(REPO_ROOT)/Multisig.mk

# Cobalt rolls out across several networks from this one task directory, so the
# network is never defaulted. Select it on every command line:
# make TASK_NETWORK=zeronet deploy
ifndef TASK_NETWORK
$(error TASK_NETWORK is required, e.g. make TASK_NETWORK=zeronet $(MAKECMDGOALS))
endif

PROJECT_DIR := $(abspath ../..)

include $(REPO_ROOT)/config/$(TASK_NETWORK).env
include config/$(TASK_NETWORK)/.env

SIGNER_TOOL_PATH := $(REPO_ROOT)/signer-tool
VALIDATIONS_DIR := $(CURDIR)/config/$(TASK_NETWORK)/validations
RPC_URL := $(L1_RPC_URL)
ADDRESSES_JSON := $(CURDIR)/config/$(TASK_NETWORK)/addresses.json

TASK_DIR := tasks/2026-09-14-cobalt-upgrade
DEPLOY_CORE_SCRIPT := $(TASK_DIR)/script/DeployCobaltCoreImpls.s.sol:DeployCobaltCoreImpls
DEPLOY_PROOFS_SCRIPT := $(TASK_DIR)/script/DeployCobaltProofImpls.s.sol:DeployCobaltProofImpls
UPGRADE_SCRIPT := $(TASK_DIR)/script/ExecuteCobaltUpgrade.s.sol:ExecuteCobaltUpgrade
SCRIPT_NAME := $(UPGRADE_SCRIPT)

CB_SENDER = $(shell $(MISE_EXEC) cast call $(CB_MULTISIG) "getOwners()(address[])" --rpc-url $(L1_RPC_URL) | tr -d '[]' | cut -d',' -f1)
SC_SENDER = $(shell $(MISE_EXEC) cast call $(BASE_SECURITY_COUNCIL) "getOwners()(address[])" --rpc-url $(L1_RPC_URL) | tr -d '[]' | cut -d',' -f1)

NEW_OPTIMISM_PORTAL_IMPL = $(shell jq -er '.optimismPortalImpl' $(ADDRESSES_JSON) 2>/dev/null)
NEW_SYSTEM_CONFIG_IMPL = $(shell jq -er '.systemConfigImpl' $(ADDRESSES_JSON) 2>/dev/null)
NEW_DISPUTE_GAME_FACTORY_IMPL = $(shell jq -er '.disputeGameFactoryImpl' $(ADDRESSES_JSON) 2>/dev/null)
NEW_AGGREGATE_VERIFIER = $(shell jq -er '.aggregateVerifier' $(ADDRESSES_JSON) 2>/dev/null)
PROTOCOL_VERSIONS_PROXY = $(shell jq -er '.protocolVersionsProxy' $(ADDRESSES_JSON) 2>/dev/null)
PROTOCOL_VERSIONS_IMPL = $(shell jq -er '.protocolVersionsImpl' $(ADDRESSES_JSON) 2>/dev/null)
OPTIMISM_PORTAL_IMPL_CTOR_ARGS = $(shell jq -er '.optimismPortalImplConstructorArgs' $(ADDRESSES_JSON) 2>/dev/null)
PROTOCOL_VERSIONS_PROXY_CTOR_ARGS = $(shell jq -er '.protocolVersionsProxyConstructorArgs' $(ADDRESSES_JSON) 2>/dev/null)
AGGREGATE_VERIFIER_CTOR_ARGS = $(shell jq -er '.aggregateVerifierConstructorArgs' $(ADDRESSES_JSON) 2>/dev/null)

DEPLOY_ENV = ADDRESSES_JSON=$(ADDRESSES_JSON) \
L1_PROXY_ADMIN=$(L1_PROXY_ADMIN) \
OPTIMISM_PORTAL=$(OPTIMISM_PORTAL) \
DISPUTE_GAME_FACTORY_PROXY=$(DISPUTE_GAME_FACTORY_PROXY) \
L2_GENESIS_BLOCK_NUMBER=$(L2_GENESIS_BLOCK_NUMBER) \
L2_GENESIS_TIMESTAMP=$(L2_GENESIS_TIMESTAMP) \
L2_BLOCK_TIME=$(L2_BLOCK_TIME) \
AGGREGATE_VERIFIER_TEE_IMAGE_HASH=$(AGGREGATE_VERIFIER_TEE_IMAGE_HASH) \
AGGREGATE_VERIFIER_ZK_RANGE_HASH=$(AGGREGATE_VERIFIER_ZK_RANGE_HASH) \
AGGREGATE_VERIFIER_ZK_AGGREGATE_HASH=$(AGGREGATE_VERIFIER_ZK_AGGREGATE_HASH)

UPGRADE_ENV = PROXY_ADMIN_OWNER=$(PROXY_ADMIN_OWNER) \
L1_PROXY_ADMIN=$(L1_PROXY_ADMIN) \
OPTIMISM_PORTAL=$(OPTIMISM_PORTAL) \
SYSTEM_CONFIG=$(SYSTEM_CONFIG) \
DISPUTE_GAME_FACTORY_PROXY=$(DISPUTE_GAME_FACTORY_PROXY) \
OLD_OPTIMISM_PORTAL_IMPL=$(OLD_OPTIMISM_PORTAL_IMPL) \
OLD_SYSTEM_CONFIG_IMPL=$(OLD_SYSTEM_CONFIG_IMPL) \
OLD_DISPUTE_GAME_FACTORY_IMPL=$(OLD_DISPUTE_GAME_FACTORY_IMPL) \
OLD_AGGREGATE_VERIFIER=$(OLD_AGGREGATE_VERIFIER) \
NEW_OPTIMISM_PORTAL_IMPL=$(NEW_OPTIMISM_PORTAL_IMPL) \
NEW_SYSTEM_CONFIG_IMPL=$(NEW_SYSTEM_CONFIG_IMPL) \
NEW_DISPUTE_GAME_FACTORY_IMPL=$(NEW_DISPUTE_GAME_FACTORY_IMPL) \
NEW_AGGREGATE_VERIFIER=$(NEW_AGGREGATE_VERIFIER) \
PROTOCOL_VERSIONS_PROXY=$(PROTOCOL_VERSIONS_PROXY) \
PROTOCOL_VERSIONS_IMPL=$(PROTOCOL_VERSIONS_IMPL) \
PROTOCOL_VERSIONS_INCIDENT_RESPONDER=$(PROTOCOL_VERSIONS_INCIDENT_RESPONDER) \
PROTOCOL_VERSIONS_MINIMUM_PROTOCOL_VERSION=$(PROTOCOL_VERSIONS_MINIMUM_PROTOCOL_VERSION) \
PROTOCOL_VERSIONS_INITIAL_SCHEDULE=$(PROTOCOL_VERSIONS_INITIAL_SCHEDULE)

##
# Dependencies
##
# Prerequisites merge with the root `deps` rule, so these run after `forge-deps`
# has installed base/contracts at BASE_CONTRACTS_COMMIT.
.PHONY: deps
deps: task-extra-deps apply-patches

# The shared project installs only forge-std, solady and lib-keccak. Building the
# Cobalt implementations from source additionally needs base/contracts' own pinned
# dependencies, matching the versions in its justfile.
.PHONY: task-extra-deps
task-extra-deps:
cd $(PROJECT_DIR) && $(MISE_EXEC) forge install --no-git \
github.com/OpenZeppelin/openzeppelin-contracts@ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d \
github.com/OpenZeppelin/openzeppelin-contracts-upgradeable@0a2cb9a445c365870ed7a8ab461b12acf3e27d63 \
github.com/transmissions11/solmate@8f9b23f8838670afda0fd8983f2c41e8037ae6bc

# Zeronet runs a SystemConfig patched to raise MAX_GAS_LIMIT to 2e9. Cobalt changes
# SystemConfig, so the patch is re-applied on top of the pinned commit rather than
# silently reverting the live chain to the stock 5e8 limit.
.PHONY: apply-patches
apply-patches:
cd $(PROJECT_DIR)/lib/contracts && patch -p1 --forward < $(CURDIR)/patch/max-gas-limit.patch

##
# Deployment
##
.PHONY: deploy
deploy: deploy-core deploy-proofs

# base/contracts pins OptimismPortal2, SystemConfig, DisputeGameFactory and Proxy to
# 5000 optimizer runs, and ProtocolVersions and AggregateVerifier to 999999, and builds
# with no bytecode hash. Reproducing those settings is what makes the deployed bytecode
# verifiable, hence the two steps and the compiler overrides, which are kept identical
# to the ones the verify targets below use. The evm version request is inert at solc
# 0.8.15, which caps out at london, but it is set on both sides so they cannot diverge.
COBALT_BUILD = FOUNDRY_OPTIMIZER=true FOUNDRY_BYTECODE_HASH=none FOUNDRY_EVM_VERSION=cancun

.PHONY: deploy-core
deploy-core:
@test -f "$(ADDRESSES_JSON)" || echo '{}' > "$(ADDRESSES_JSON)"
cd $(PROJECT_DIR) && $(DEPLOY_ENV) $(COBALT_BUILD) FOUNDRY_OPTIMIZER_RUNS=5000 \
$(MISE_EXEC) forge script --rpc-url $(L1_RPC_URL) $(DEPLOY_CORE_SCRIPT) \
--ledger --hd-paths $(LEDGER_HD_PATH) --broadcast -vvvv

.PHONY: deploy-proofs
deploy-proofs:
cd $(PROJECT_DIR) && $(DEPLOY_ENV) $(COBALT_BUILD) FOUNDRY_OPTIMIZER_RUNS=999999 \
$(MISE_EXEC) forge script --rpc-url $(L1_RPC_URL) $(DEPLOY_PROOFS_SCRIPT) \
--ledger --hd-paths $(LEDGER_HD_PATH) --broadcast -vvvv

##
# Source verification
##
# Verification runs with lib/contracts as the project root, because that is the only
# root from which the target paths resolve. base/contracts then imports its own
# dependencies through literal `lib/<dep>/...` paths, which resolve against that root
# too, but the build keeps them a level up in active/evm/lib. Link them in for the
# duration of the command and remove them afterwards: left in place, every such import
# matches in two locations and the build fails as ambiguous.
VERIFY_DEPS = openzeppelin-contracts openzeppelin-contracts-upgradeable solady solmate
CONTRACTS_LIB = $(PROJECT_DIR)/lib/contracts/lib
LINK_VERIFY_DEPS = mkdir -p $(CONTRACTS_LIB); \
trap 'rm -f $(foreach d,$(VERIFY_DEPS),$(CONTRACTS_LIB)/$(d)); rmdir $(CONTRACTS_LIB) 2>/dev/null || true' EXIT; \
for d in $(VERIFY_DEPS); do ln -sfn ../../$$d $(CONTRACTS_LIB)/$$d; done

VERIFY_ENV = RUST_LOG=off FOUNDRY_BYTECODE_HASH=none FOUNDRY_EVM_VERSION=cancun

VERIFY_COMMON = --root $(PROJECT_DIR)/lib/contracts --compilation-profile default \
--chain $(L1_CHAIN_ID) --compiler-version v0.8.15+commit.e14f2714 \
--verifier custom --verifier-url "https://api.etherscan.io/v2/api?chainid=$(L1_CHAIN_ID)" \
--verifier-api-key $(VERIFIER_API_KEY) --watch

.PHONY: verify-core
verify-core:
ifndef VERIFIER_API_KEY
$(error VERIFIER_API_KEY is not set)
endif
@set -e; $(LINK_VERIFY_DEPS); \
cd $(PROJECT_DIR); \
$(VERIFY_ENV) FOUNDRY_OPTIMIZER_RUNS=5000 \
$(MISE_EXEC) forge verify-contract $(NEW_OPTIMISM_PORTAL_IMPL) src/L1/OptimismPortal2.sol:OptimismPortal2 \
--constructor-args $(OPTIMISM_PORTAL_IMPL_CTOR_ARGS) \
--num-of-optimizations 5000 $(VERIFY_COMMON); \
$(VERIFY_ENV) FOUNDRY_OPTIMIZER_RUNS=5000 \
$(MISE_EXEC) forge verify-contract $(NEW_SYSTEM_CONFIG_IMPL) src/L1/SystemConfig.sol:SystemConfig \
--num-of-optimizations 5000 $(VERIFY_COMMON); \
$(VERIFY_ENV) FOUNDRY_OPTIMIZER_RUNS=5000 \
$(MISE_EXEC) forge verify-contract $(NEW_DISPUTE_GAME_FACTORY_IMPL) src/L1/proofs/DisputeGameFactory.sol:DisputeGameFactory \
--num-of-optimizations 5000 $(VERIFY_COMMON); \
$(VERIFY_ENV) FOUNDRY_OPTIMIZER_RUNS=5000 \
$(MISE_EXEC) forge verify-contract $(PROTOCOL_VERSIONS_PROXY) src/universal/Proxy.sol:Proxy \
--constructor-args $(PROTOCOL_VERSIONS_PROXY_CTOR_ARGS) \
--num-of-optimizations 5000 $(VERIFY_COMMON)

.PHONY: verify-proofs
verify-proofs:
ifndef VERIFIER_API_KEY
$(error VERIFIER_API_KEY is not set)
endif
@set -e; $(LINK_VERIFY_DEPS); \
cd $(PROJECT_DIR); \
$(VERIFY_ENV) FOUNDRY_OPTIMIZER_RUNS=999999 \
$(MISE_EXEC) forge verify-contract $(PROTOCOL_VERSIONS_IMPL) src/L1/ProtocolVersions.sol:ProtocolVersions \
--num-of-optimizations 999999 $(VERIFY_COMMON); \
$(VERIFY_ENV) FOUNDRY_OPTIMIZER_RUNS=999999 \
$(MISE_EXEC) forge verify-contract $(NEW_AGGREGATE_VERIFIER) src/L1/proofs/AggregateVerifier.sol:AggregateVerifier \
--constructor-args $(AGGREGATE_VERIFIER_CTOR_ARGS) \
--num-of-optimizations 999999 $(VERIFY_COMMON)

##
# Validation files
##
.PHONY: gen-validation-cb
gen-validation-cb: deps-signer-tool
$(call GEN_VALIDATION,$(UPGRADE_SCRIPT),$(CB_MULTISIG),$(CB_SENDER),base-signer.json,$(UPGRADE_ENV))

.PHONY: gen-validation-sc
gen-validation-sc: deps-signer-tool
$(call GEN_VALIDATION,$(UPGRADE_SCRIPT),$(BASE_SECURITY_COUNCIL),$(SC_SENDER),security-council-signer.json,$(UPGRADE_ENV))

##
# Approve and execute
##
.PHONY: approve-cb
approve-cb:
export $(UPGRADE_ENV); $(call MULTISIG_APPROVE,$(CB_MULTISIG),$(SIGNATURES))

.PHONY: approve-sc
approve-sc:
export $(UPGRADE_ENV); $(call MULTISIG_APPROVE,$(BASE_SECURITY_COUNCIL),$(SIGNATURES))

.PHONY: execute
execute:
export $(UPGRADE_ENV); $(call MULTISIG_EXECUTE,0x)
Loading
Loading