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
142 changes: 127 additions & 15 deletions active/evm/tasks/2026-10-07-denim-upgrade/FACILITATOR.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Denim upgrade facilitator guide

Sepolia and mainnet use `ExecuteDenimUpgradeWithL2Predeploys` for the two Denim L1 calls and five
L2 upgrade deposits in one ProxyAdmin-owner transaction. The Security Council signs once.
Zeronet ran `ExecuteDenimUpgrade` for Denim only and upgraded the L2 predeploys separately.
The unset `UPGRADE_SCRIPT_NAME` preserves that Denim-only path. For the combined path, set
`UPGRADE_SCRIPT_NAME=ExecuteDenimUpgradeWithL2Predeploys` in `config/<network>/.env`.
Sepolia executed `ExecuteDenimUpgradeWithL2Predeploys`: two Denim L1 calls and five L2 deposits.
Mainnet selects `ExecuteDenimUpgradeWithAllL2Predeploys`: two Denim L1 calls and thirteen L2 deposits,
fifteen calls in ONE ProxyAdmin-owner transaction. The Security Council approves this transaction once,
with eight member signatures. Zeronet ran `ExecuteDenimUpgrade` for Denim only. Leave the executed
paths and their network configurations unchanged.

| Change | Contracts touched |
| --- | --- |
| Fork-gated proof intervals for 200ms blocks | `AggregateVerifier` (redeployed as `0.3.0`), `DisputeGameFactory` (`setImplementation`) |
| Denim activation and minimum node version | `ProtocolVersions` (`registerUpgrade`, upgrade id 13) |
| L2 predeploy upgrades through `OptimismPortal` deposits | `L2ToL1MessagePasser` `1.2.0`, `L1Block` `1.8.0`, `SequencerFeeVault`, `BaseFeeVault`, `L1FeeVault` `1.6.0` |
| First five L2 predeploy upgrades through `OptimismPortal` deposits | `L2ToL1MessagePasser` `1.2.0`, `L1Block` `1.8.0`, `SequencerFeeVault`, `BaseFeeVault`, `L1FeeVault` `1.6.0` |

`AggregateVerifier` `0.3.0` carries two proposal interval pairs: the slow pair for 2s blocks and the
fast pair for Denim's 200ms blocks. It reads the Denim activation from `ProtocolVersions` index 13 and
Expand Down Expand Up @@ -50,6 +50,31 @@ Open `config/<network>/.env` and confirm:

Every other `AggregateVerifier` constructor argument is copied from the live verifier at deploy time.

For mainnet, verify the PCR0 and both SP1 verification keys against the v1.5.2 build provenance
in the network config before deploying or generating validations. The Sepolia f3a459f build cannot
derive post-Denim mainnet blocks because it disables mainnet Denim. Do not reuse its hashes.
Mainnet validation, approval and execution refuse missing hashes.

Build enclave and zk images from exactly base/base releases/v1.5.2 at 6efa75662b57c578d897fea2449035636f6ad9ad.
Use the proof-build recipes at 228352e514233177a65cc2d9873f6bf4b08e67cd, Rust 1.94.0 alpine,
linuxkit v1.8.2 and aws-nitro-enclaves-image-format 483114f. A different source revision, recipe or
toolchain can change the accepted hashes.

The mainnet script checks L1 chain id 1, L2 chain id 8453, the owner Safe's 2-of-2 membership,
Base 3-of-6 and Security Council 8-of-11, the current verifier's 600/30 intervals, portal version
6.0.1, the portal resource and L1 gas budgets described below, schedule length 13, canceled entry 7, last entry 1790791200,
and MIN_NOTICE 3600. It also checks the live L2 versions, proxy admins, legacy storage guards
and counterpart/vault configuration. Re-run `simulate-current` if any precondition changes.

Mainnet preserves the live vault recipients and L2 withdrawal networks. `BaseTime` is excluded:
Denim installs it only while its implementation slot is empty. The mainnet script refuses an occupied
implementation slot or an admin other than the L2 ProxyAdmin in both setup and the L2 post-check.

EAS mainnet `1.0.1` uses EIP-712 domain version `1.0.1`, not Sepolia's `1.2.0`. The new domain is
`1.3.0`. Outstanding delegated attestation/revocation signatures become invalid; clients must sign
again using the new domain, type hashes and delegation ABI. Existing attestations, schemas and nonces
remain at their existing storage locations.

## 3. Deploy and verify

```bash
Expand All @@ -67,16 +92,42 @@ For the combined path, deploy the five L2 implementations after deploying the ve
make TASK_NETWORK=<network> deploy-l2
```

The facilitator runs both deployment steps with a Ledger. `deploy-l2` uses the L2 RPC, 999999 optimizer runs,
For mainnet, then deploy the other eight implementations and LegacyMigrator with the task-local
Foundry project:

```bash
make TASK_NETWORK=mainnet deploy-remaining-l2
VERIFIER_API_KEY=<key> make TASK_NETWORK=mainnet verify-l2
make TASK_NETWORK=<network> simulate
```

On mainnet, all thirteen L2 implementations and LegacyMigrator must pass `verify-l2` on Basescan.
Fill in their verified-source links in `config/mainnet/README.md` before generating validations
or signing. `verify-l2` waits for each result and stops on a missing address or failed verification.

The extra deployments use 0.8.15 for factories and bridges, 0.8.19 for attestations, and 0.8.25 for
OperatorFeeVault. Per-file factory optimizer profiles use 5000 runs; the other implementations use
999999 runs and no bytecode hash. All use `via-ir=false`. Foundry clamps the configured Cancun
EVM target to London for 0.8.15 and Paris for 0.8.19; 0.8.25 uses Cancun.
The multisig script also builds from `active/evm` using
its default compiler settings. `simulate` (an alias for `test`) runs deterministic historical fork
tests with test-only verifier hashes and local implementations. It is not the current-state execution
prerequisite. The gas tests use `forge test --isolate`, so each measured ProxyAdmin call and
the owner-Safe execution starts a separate transaction with cold storage. Other tests run without
isolation so cheatcode basefee overrides remain effective. Neither simulation command deploys,
generates validations, signs or broadcasts.

The facilitator runs all deployment steps with a Ledger. `deploy-l2` uses the L2 RPC, 999999 optimizer runs,
no bytecode hash and Cancun, matching base/contracts. It appends `l2ToL1MessagePasser`, `l1Block`,
`sequencerFeeVault`, `baseFeeVault` and `l1FeeVault` to `config/<network>/addresses.json`. There is no
L2 source-verification step. Deploy the verifier first because its script replaces the address file;
L2 source-verification target for Sepolia or Zeronet.
Deploy the verifier first because its script replaces the address file;
`deploy` refuses to overwrite recorded L2 addresses. The fee-vault deployment uses Solidity 0.8.25,
while the verifier and other L2 deployment use 0.8.15.

Commit `config/<network>/addresses.json` and the task-scoped `records/` broadcast artifacts, then
fill in the implementation addresses in `config/<network>/README.md`. The combined validation,
approval and execution targets refuse to proceed until all six implementation addresses are present.
approval and execution targets refuse to proceed until all six addresses on Sepolia, or all fifteen addresses on mainnet including the migrator, are present.

## 4. Generate validation files

Expand All @@ -93,15 +144,38 @@ and state-change descriptions. Expected state changes:
- `ProtocolVersions`: a 14th schedule entry (Denim) with
`PROTOCOL_VERSIONS_DENIM_ACTIVATION_TIMESTAMP`, the extended schedule id chain, and the new
minimum protocol version.
- Combined path only: five separate `OptimismPortal.depositTransaction` calls to the L2 ProxyAdmin,
- Sepolia combined path: five separate `OptimismPortal.depositTransaction` calls to the L2 ProxyAdmin,
with five `TransactionDeposited` events and portal resource-metering state changes such as resource
params. Each deposit has zero value and a 200000 L2 gas limit. Check that the sender is the owner Safe.
- L2 storage changes are not part of the L1 validation state diff. The script post-check replays the
deposits on an L2 fork and checks implementation addresses, versions and all three vaults' initialized
recipient, minimum withdrawal and withdrawal network. Each upgrade is its own deposit so the
- Mainnet: thirteen separate zero-value owner-Safe deposits to the L2 ProxyAdmin, each with a
300000 gas limit, totaling 3900000 L2 gas against the portal's 20000000 resource limit. The first
five calls are the Sepolia set above. The remaining eight are:

| Target | New version | Method |
| --- | --- | --- |
| OptimismMintableERC721Factory | 1.4.2 | upgrade |
| OperatorFeeVault | 1.1.0 | upgradeAndCall initialize |
| SchemaRegistry | 1.3.1-beta.2 | upgrade |
| EAS | 1.4.1-beta.3 | upgrade |
| L2CrossDomainMessenger | 2.2.0 | upgradeAndCall LegacyMigrator.migrate |
| L2StandardBridge | 1.13.0 | upgradeAndCall LegacyMigrator.migrate |
| OptimismMintableERC20Factory | 1.10.2 | upgradeAndCall LegacyMigrator.migrate |
| L2ERC721Bridge | 1.10.0 | upgradeAndCall LegacyMigrator.migrate |

The four migrations clear legacy slot 0, install the final implementation, and initialize its
counterpart atomically. On mainnet those slots contain only initializer flags or zero spacers,
unlike Sepolia's packed state. All three fee vaults and OperatorFeeVault initialize their storage
configuration without clearing totalProcessed in slot 0. The new ERC20 factory deployments mapping
starts empty; it does not backfill tokens created by the old factory.
- L2 storage changes are not part of the L1 validation state diff. On mainnet, the script post-check validates the
captured deposit arguments in order, including the target, zero value, gas limit, creation flag and
calldata. It replays that decoded calldata on an L2 fork and checks implementation addresses,
versions and all three vaults' initialized recipient, minimum withdrawal and withdrawal network.
Sepolia replays its five expected calls and checks the same vault settings. Each upgrade is its own deposit so the
L2 ProxyAdmin sees the aliased owner.
- Safe nonce and approval changes.

For mainnet, KEEP `taskOriginConfig`. Task-origin validation is required for this owner-Safe transaction.
For any non-mainnet rollout, remove each generated `taskOriginConfig` and add this root field:

```json
Expand All @@ -118,6 +192,12 @@ 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
```

On mainnet, first run `make TASK_NETWORK=mainnet simulate-current` after both approvals are onchain.
The current-state simulation requirements and gas budget are below.

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

Expand All @@ -126,9 +206,10 @@ margin. `ProtocolVersions` enforces this one-hour `MIN_NOTICE`. Once registered,
only be delayed by the incident responder and freezes one hour before it lands.

The deposits execute on L2 shortly after the L1 transaction, not at Denim activation. The predeploy
upgrades have no ordering dependency on Denim. Check that all five deposits succeeded on L2.
upgrades have no ordering dependency on Denim. Check that all five Sepolia deposits, or all thirteen mainnet deposits, succeeded on L2.

Immediately after execution, roll the network's proposer and provers to the prover build documented in `config/<network>/.env`.
On mainnet this MUST be the reproducible v1.5.2 build. Execute first, then roll immediately; do not roll before execution.
The new verifier accepts proofs only from that build, so proposals pause until the rollout lands; keep the gap short.
New enclave signers register themselves in `TEEProverRegistry` on startup. The nitro host's health check requires a signer valid
for the registry's current expected image hash, so the new build cannot be deployed before execution. Confirm the new signers
Expand All @@ -143,6 +224,37 @@ cast call "$TEE_PROVER_REGISTRY_PROXY" 'signerImageHash(address)(bytes32)' <sign
After execution the script checks that the factory points at the new verifier, that Denim is upgrade
id 13 with the expected timestamp, that earlier upgrades are unchanged, and that the new verifier
returns the slow intervals for the block before Denim and the fast intervals from the first Denim
block. On the combined path it also checks the five successful owner-Safe deposits, replays each
block. On the combined path it also checks the five Sepolia or thirteen mainnet successful owner-Safe deposits, replays each
on an L2 fork as the aliased owner, and checks implementations, versions, fee-vault config and the
unchanged L2 ProxyAdmin owner. Set the README status to the execution transaction link.

Immediately before mainnet execution, after both Safe approvals are onchain, run from this directory:

```bash
make TASK_NETWORK=mainnet simulate-current
```

This invokes `MultisigScript.simulate(bytes)` with `0x` signatures, the real implementations from
`config/mainnet/addresses.json`, and the current L1 and L2 RPC heads. It checks the prerequisites,
including the notice window, simulates the owner transaction using the actual approvals, and replays
all deposits on the current L2 fork. Missing deployment addresses or approvals stop the command.
Do not execute unless it succeeds; the historical `test` / `simulate` target is not a substitute.

Gas rules enforced by the script and tests:

- Every deposit has a 300000 L2 gas limit. The isolated cold gas tests (`testColdL2DepositGas`,
run by `test` with `--isolate`) require 1.5 times each deposit's execution, intrinsic and calldata
gas to fit within it. `simulate-current` replays the actual deposited calldata with the deposit
gas limit minus intrinsic and calldata gas, but does not measure cold transaction gas.
- In `setUp` (validation, approval and execution simulations), the script reads the live portal
`params()` and resource configuration. It bounds the portal fee by `prevBaseFee` plus any positive
first-deposit fee update from the previous block's bought gas, applies the configured minimum (and
maximum on a block update), and takes no credit for empty-block decay. The burn for all deposits is
bounded by `3900000 * fee / max(L1 basefee, 1 gwei)`.
- The script requires that burn plus a 5000000 base-cost allowance to be at most 14000000 gas, below
the EIP-7825 per-transaction cap of 16777216. `testColdL1OwnerTransactionGas` requires the cold
owner transaction to fit within the allowance with at least 10% margin.

Fees and resource use can change after simulation. Recheck immediately before submission and do
not override a failed guard or split this transaction. The L1 block limit and portal resource limit
do not protect against the per-transaction cap.
Loading
Loading