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
78 changes: 78 additions & 0 deletions archive/evm/2026-10-21-replace-balance-tracker/FACILITATOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Task facilitator guide

Replace the L1 `BalanceTracker`, point the L2 `FeeDisburser` at it, and move the `FeeDisburser` admin to the alias of `NEW_ADMIN`.

Run commands from this task directory and pass `TASK_NETWORK=<network>` on every Make command line. Inputs live in `config/<network>/.env`. Deployed addresses are written to `config/<network>/addresses.json`.

The work happens in three phases:

1. **Deploy on L1 from the facilitator's Ledger.** This deploys and initializes the new `BalanceTracker` proxy, then hands its admin to `NEW_ADMIN`.
2. **Deploy on L2 from the facilitator's Ledger.** This deploys the `FeeDisburser` implementation with `L1_WALLET` set to the new `BalanceTracker`.
3. **Multisig transaction from `OWNER_SAFE`.** This sends two `OptimismPortal` deposits to the `FeeDisburser` proxy: `upgradeTo(new implementation)`, then `changeAdmin(NEW_ADMIN_L2_ALIAS)`.

## Install dependencies

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

## Deploy

Use the Ledger account at `LEDGER_ACCOUNT`; it needs gas on both L1 and L2. Each script simulates and checks its output before broadcasting:

- `deploy-l1` checks the new proxy's admin, implementation, profit wallet, system addresses and targets. It also checks that `initialize` with the real arguments reverts with the initializer guard on both the proxy and the implementation, and that `processFees()` refills both system addresses and sends the rest to the profit wallet.
- `deploy-l2` runs `disburseFees()` through the live `FeeDisburser` proxy with the new implementation. Each fee vault is topped up to its minimum withdrawal amount first, so every vault withdrawal and the bridge to the new `BalanceTracker` always run.

```sh
make TASK_NETWORK=<network> deploy-l1
make TASK_NETWORK=<network> deploy-l2
```

Commit `config/<network>/addresses.json` and the task-scoped `records/`. Each target refuses to run again once its address is recorded.

### If a deploy fails partway

The scripts write `addresses.json` during simulation, before broadcasting. If the broadcast then fails, some transactions may be onchain and others not. For example, `deploy-l1` can stop before `changeAdmin`, leaving the new proxy admined by the facilitator's Ledger. Don't delete `addresses.json` or start a fresh deployment. Finish the recorded broadcast instead:

```sh
make TASK_NETWORK=<network> deploy-l1 RESUME=1
make TASK_NETWORK=<network> deploy-l2 RESUME=1
```

`--resume` only re-sends the unconfirmed transactions from `records/` and doesn't re-run the script's checks. It can't recover transactions that were mined but reverted. If a creation reverted, for example out of gas, check whether each address in `addresses.json` has code. If none does, nothing usable was deployed: delete `addresses.json` and run the target again without `RESUME`. Afterwards, confirm onchain that the `BalanceTracker` proxy's `admin()` is `NEW_ADMIN` and that every address in `addresses.json` has code. `gen-validation` also refuses to run until the `BalanceTracker` admin is `NEW_ADMIN`. Delete `addresses.json` only if `records/` shows that no transaction was sent.

## Generate validation files

```sh
make TASK_NETWORK=<network> gen-validation
```

This writes `config/<network>/validations/base-signer.json` for `OWNER_SAFE`. The post-check replays both deposits as the alias of `OWNER_SAFE` on an L2 fork. It then asserts the new implementation, the new admin, the new `L1_WALLET`, and an unchanged `lastDisbursementTime`.

Check the state diff and replace any summary placeholders. This task is not a mainnet ProxyAdmin-owner operation, so remove the generated `taskOriginConfig` and add this root field instead:

```json
"skipTaskOriginValidation": true
```

Commit the validation, set the network step to `ready_to_sign`, then `collecting`, and update the `base` signature count in `status.json` as signatures arrive.

## Execute

Confirm the live threshold and nonce of `OWNER_SAFE`, then order the signatures by owner address and concatenate them. Set the step to `ready_to_execute`, then run:

```sh
make TASK_NETWORK=<network> execute SIGNATURES=<concatenated-signatures>
```

After the deposits are relayed on L2, run:

```sh
make TASK_NETWORK=<network> verify-l2
```

Commit the final artifacts. Set `Status: EXECUTED` with the L1 transaction link in `config/<network>/README.md`, and update the step, `executed_tx`, and `last_updated` in `status.json` in the same change. Then update `BALANCE_TRACKER` in `config/<network>.env` at the repository root to the new proxy.

## Archive

Keep the task active until every intended network is executed or canceled. From the repository root, run `make check-task-status`, then `make archive-task` and select this task.
111 changes: 111 additions & 0 deletions archive/evm/2026-10-21-replace-balance-tracker/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
include ../../../../Makefile
include $(REPO_ROOT)/Multisig.mk

ifneq ($(origin TASK_NETWORK),command line)
$(error TASK_NETWORK is required on the command line, e.g. make TASK_NETWORK=<network> $(MAKECMDGOALS))
endif
ifeq ($(strip $(TASK_NETWORK)),)
$(error TASK_NETWORK must not be empty)
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_ORIGIN_DIR := $(CURDIR)/config/$(TASK_NETWORK)
SIGNATURE_DIR := $(CURDIR)/signatures/$(TASK_NETWORK)
TASK_DIR := tasks/2026-10-21-replace-balance-tracker

DEPLOY_L1_SCRIPT := $(TASK_DIR)/script/DeployBalanceTracker.s.sol:DeployBalanceTracker
DEPLOY_L2_SCRIPT := $(TASK_DIR)/script/DeployFeeDisburser.s.sol:DeployFeeDisburser
SCRIPT_NAME := $(TASK_DIR)/script/UpgradeFeeDisburser.s.sol:UpgradeFeeDisburser

SENDER = $(shell $(MISE_EXEC) cast call $(OWNER_SAFE) "getOwners()(address[])" --rpc-url $(L1_RPC_URL) | tr -d '[]' | cut -d',' -f1)
deployed = $(shell jq -er '.$(1)' $(ADDRESSES_JSON) 2>/dev/null)
require_deployed = @jq -e '[$(1)] | all(.[]; type == "string" and test("^0x[0-9a-fA-F]{40}$$"))' "$(ADDRESSES_JSON)" >/dev/null 2>&1 || \
{ echo '$(2)'; exit 1; }

DEPLOY_L1_ENV = PROFIT_WALLET=$(PROFIT_WALLET) \
NEW_ADMIN=$(NEW_ADMIN) \
SYSTEM_ADDRESS_0=$(SYSTEM_ADDRESS_0) \
TARGET_BALANCE_0=$(TARGET_BALANCE_0) \
SYSTEM_ADDRESS_1=$(SYSTEM_ADDRESS_1) \
TARGET_BALANCE_1=$(TARGET_BALANCE_1)

UPGRADE_ENV = OWNER_SAFE=$(OWNER_SAFE) \
NEW_ADMIN=$(NEW_ADMIN) \
NEW_ADMIN_L2_ALIAS=$(NEW_ADMIN_L2_ALIAS) \
OPTIMISM_PORTAL=$(OPTIMISM_PORTAL) \
FEE_DISBURSER=$(FEE_DISBURSER) \
NEW_FEE_DISBURSER_IMPL=$(call deployed,feeDisburserImplementation) \
NEW_BALANCE_TRACKER=$(call deployed,balanceTracker) \
L2_RPC_URL=$(L2_RPC_URL)

.PHONY: deps
deps: task-extra-deps

# BalanceTracker imports OpenZeppelin upgradeable contracts through a literal lib/ path. Pin from base/contracts.
.PHONY: task-extra-deps
task-extra-deps:
cd $(PROJECT_DIR) && $(MISE_EXEC) forge install --no-git \
github.com/OpenZeppelin/openzeppelin-contracts-upgradeable@0a2cb9a445c365870ed7a8ab461b12acf3e27d63

##
# Deployment (facilitator Ledger, no multisig signatures)
##
# The scripts write addresses.json during simulation, before broadcasting, so a failed broadcast can leave only
# some transactions onchain. addresses.json is kept as the checkpoint; RESUME=1 re-sends the unconfirmed
# transactions from the task-scoped broadcast record instead of starting a fresh deployment.
PARTIAL_DEPLOY = Deploy failed; $(ADDRESSES_JSON) is kept. Finish the broadcast with RESUME=1, see FACILITATOR.md.

# Foundry 1.5.1 predates Sepolia's Oct 2026 fork and underprices contract creation by about 7x, so its
# simulated gas limits run out of gas onchain. --skip-simulation sends sequentially with eth_estimateGas limits.
.PHONY: deploy-l1
deploy-l1:
ifndef RESUME
@test ! -f "$(ADDRESSES_JSON)" || { echo '$(ADDRESSES_JSON) already exists; use RESUME=1 to finish a failed deploy-l1'; exit 1; }
@echo '{}' > "$(ADDRESSES_JSON)"
endif
cd $(PROJECT_DIR) && $(DEPLOY_L1_ENV) ADDRESSES_JSON=$(ADDRESSES_JSON) \
$(MISE_EXEC) forge script --rpc-url $(L1_RPC_URL) $(DEPLOY_L1_SCRIPT) \
--ledger --hd-paths $(LEDGER_HD_PATH) --broadcast --skip-simulation $(if $(RESUME),--resume) -vvvv || { echo "$(PARTIAL_DEPLOY)"; exit 1; }

.PHONY: deploy-l2
deploy-l2:
$(call require_deployed,.balanceTracker,Run deploy-l1 before deploy-l2)
ifndef RESUME
@jq -e '.feeDisburserImplementation == null' "$(ADDRESSES_JSON)" >/dev/null || { echo 'FeeDisburser implementation already recorded; use RESUME=1 to finish a failed deploy-l2'; exit 1; }
endif
cd $(PROJECT_DIR) && FEE_DISBURSER=$(FEE_DISBURSER) FEE_DISBURSEMENT_INTERVAL=$(FEE_DISBURSEMENT_INTERVAL) \
ADDRESSES_JSON=$(ADDRESSES_JSON) \
$(MISE_EXEC) forge script --rpc-url $(L2_RPC_URL) $(DEPLOY_L2_SCRIPT) \
--ledger --hd-paths $(LEDGER_HD_PATH) --broadcast $(if $(RESUME),--resume) -vvvv || { echo "$(PARTIAL_DEPLOY)"; exit 1; }

.PHONY: check-deployments
check-deployments:
$(call require_deployed,.balanceTracker$(comma) .feeDisburserImplementation,Run deploy-l1 and deploy-l2 first)

##
# Validation and execution (OWNER_SAFE signatures)
##
.PHONY: gen-validation
gen-validation: check-deployments deps-signer-tool
mkdir -p $(VALIDATIONS_DIR)
$(call GEN_VALIDATION,$(SCRIPT_NAME),,$(SENDER),base-signer.json,$(UPGRADE_ENV))

.PHONY: execute
execute: check-deployments
export $(UPGRADE_ENV); $(call MULTISIG_EXECUTE,$(SIGNATURES))

# Run after the deposits are relayed on L2.
.PHONY: verify-l2
verify-l2: check-deployments
@test "$$($(MISE_EXEC) cast call $(FEE_DISBURSER) 'implementation()(address)' --from 0x0000000000000000000000000000000000000000 --rpc-url $(L2_RPC_URL))" = "$$($(MISE_EXEC) cast to-check-sum-address $(call deployed,feeDisburserImplementation))" \
&& echo 'FeeDisburser implementation upgraded' || { echo 'FeeDisburser implementation not upgraded'; exit 1; }
@test "$$($(MISE_EXEC) cast call $(FEE_DISBURSER) 'admin()(address)' --from 0x0000000000000000000000000000000000000000 --rpc-url $(L2_RPC_URL))" = "$$($(MISE_EXEC) cast to-check-sum-address $(NEW_ADMIN_L2_ALIAS))" \
&& echo 'FeeDisburser admin moved' || { echo 'FeeDisburser admin not moved'; exit 1; }
16 changes: 16 additions & 0 deletions archive/evm/2026-10-21-replace-balance-tracker/config/sepolia/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
BASE_CONTRACTS_COMMIT=8c21f6d08a36b4059ae2bb8938f1e7e758a0e962
Comment thread
mw2000 marked this conversation as resolved.
Resolved

# New L1 BalanceTracker. NEW_ADMIN becomes its proxy admin; PROFIT_WALLET receives the excess fees.
NEW_ADMIN=0x646132A1667ca7aD00d36616AFBA1A28116C770A
Comment thread
mw2000 marked this conversation as resolved.
Resolved
PROFIT_WALLET=0x646132A1667ca7aD00d36616AFBA1A28116C770A
# Proposer (config/sepolia.env PROPOSER), refilled to 200 ETH.
SYSTEM_ADDRESS_0=0xdb84125f2f4229c81c579f41bc129c71b174eb58
TARGET_BALANCE_0=200000000000000000000
# Batcher (SystemConfig.batcherHash), refilled to 1000 ETH.
SYSTEM_ADDRESS_1=0xfc56E7272EEBBBA5bC6c544e159483C4a38f8bA3
TARGET_BALANCE_1=1000000000000000000000

# L2 FeeDisburser. OWNER_SAFE's alias is its current proxy admin; the task moves it to NEW_ADMIN's alias.
OWNER_SAFE=0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f
NEW_ADMIN_L2_ALIAS=0x757232a1667ca7ad00D36616aFbA1A28116c881B
FEE_DISBURSEMENT_INTERVAL=604800
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 2026-10-21-replace-balance-tracker

Status: EXECUTED
https://sepolia.etherscan.io/tx/0x1d9e7be871ea17c1fc215940e01c19dfabbb67ace76d83275ac72f0609096789

## Description

The Sepolia `BalanceTracker` is admined by an inaccessible EOA, so a new `BalanceTracker` has been deployed on L1 at `0x5D744AeA3e41B525948f000B5588Df291B47C420`, with `0x646132A1667ca7aD00d36616AFBA1A28116C770A` as its proxy admin and profit wallet. It refills the proposer to 200 ETH and the batcher to 1000 ETH.

This transaction from `0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f` sends two `OptimismPortal` deposits to the Base Sepolia `FeeDisburser` proxy `0x76355A67fCBCDE6F9a69409A8EAd5EaA9D8d875d`:

1. `upgradeTo` the new implementation `0xdC3e4dB1947F57fB18b093924Ca95B96ddc1ef2e`, whose `L1_WALLET` is the new `BalanceTracker`.
2. `changeAdmin` to `0x757232a1667ca7ad00D36616aFbA1A28116c881B`, the L2 alias of `0x646132A1667ca7aD00d36616AFBA1A28116C770A`.

## Sign

From the repository root:

```sh
make TASK_NETWORK=sepolia sign-task
```

Select `2026-10-21-replace-balance-tracker` for sepolia, sign, and send the signature to the facilitator.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"balanceTrackerImplementation": "0xA4B11D64d0aEE9D8a24a4EB629B20e4A830cFA5b",
"balanceTracker": "0x5D744AeA3e41B525948f000B5588Df291B47C420",
"feeDisburserImplementation": "0xdC3e4dB1947F57fB18b093924Ca95B96ddc1ef2e"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"cmd": "OWNER_SAFE=0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f NEW_ADMIN=0x646132A1667ca7aD00d36616AFBA1A28116C770A NEW_ADMIN_L2_ALIAS=0x757232a1667ca7ad00D36616aFbA1A28116c881B OPTIMISM_PORTAL=0x49f53e41452C74589E85cA1677426Ba426459e85 FEE_DISBURSER=0x76355A67fCBCDE6F9a69409A8EAd5EaA9D8d875d NEW_FEE_DISBURSER_IMPL=0xdC3e4dB1947F57fB18b093924Ca95B96ddc1ef2e NEW_BALANCE_TRACKER=0x5D744AeA3e41B525948f000B5588Df291B47C420 L2_RPC_URL=https://base-sepolia.cbhq.net mise exec -- forge script --rpc-url https://ethereum-full-sepolia-k8s-dev.cbhq.net tasks/2026-10-21-replace-balance-tracker/script/UpgradeFeeDisburser.s.sol:UpgradeFeeDisburser --sig sign(address[]) [] --sender 0x6e427c3212C0b63BE0C382F97715D49b011bFF33",
"ledgerId": 1,
"rpcUrl": "https://ethereum-full-sepolia-k8s-dev.cbhq.net",
"expectedDomainAndMessageHashes": {
"address": "0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f",
"domainHash": "0x0127bbb910536860a0757a9c0ffcdf9e4452220f566ed83af1f27f9e833f0e23",
"messageHash": "0x9ee89f7222e471bd59061b93dbc439c5ec8fcebc1388d0206c3a5b56c4d43a0d"
},
"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.",
"allowDifference": false
},
{
"key": "0x83be7c160f4d0cf29c7dda7e3ebdf5ae00cd56e27cf0988a178d057f2ce7cbdc",
"value": "0x0000000000000000000000000000000000000000000000000000000000000001",
"description": "Simulates an approval from msg.sender in order for the task simulation to succeed.",
"allowDifference": false
}
]
}
],
"stateChanges": [
{
"name": "OptimismPortal - Sepolia",
"address": "0x49f53e41452C74589E85cA1677426Ba426459e85",
"changes": [
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000001",
"before": "0x0000000000b6af7200000000000186a00000000000000000000000003b9aca00",
"after": "0x0000000000b6af7200000000000493e00000000000000000000000003b9aca00",
"description": "Updates the ResourceMetering parameters (prevBlockNum, prevBoughtGas, prevBaseFee).",
"allowDifference": true
}
]
},
{
"name": "CB Signer Safe - Sepolia",
"address": "0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f",
"changes": [
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000005",
"before": "0x0000000000000000000000000000000000000000000000000000000000000019",
"after": "0x000000000000000000000000000000000000000000000000000000000000001a",
"description": "Increments the nonce",
"allowDifference": false
}
]
}
],
"balanceChanges": [],
"skipTaskOriginValidation": true
}
Loading
Loading