Skip to content

feat(mainnet): add Denim upgrade with L2 predeploy upgrades - #827

Open
jackchuma wants to merge 16 commits into
mainfrom
toshi/mainnet-denim-l2-predeploys-task
Open

jackchuma wants to merge 16 commits into
mainfrom
toshi/mainnet-denim-l2-predeploys-task

Conversation

@jackchuma

@jackchuma jackchuma commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What changed? Why?

Adds the Base mainnet invocation of the Denim upgrade task (active/evm/tasks/2026-10-07-denim-upgrade/config/mainnet/). Mainnet Denim activates on 2026-11-05 at 18:00:00 UTC. All L1 Denim changes and every pending L2 predeploy upgrade are combined into one ProxyAdmin-owner transaction, so the Security Council signs once.

The transaction makes 15 calls:

  1. DisputeGameFactory.setImplementation(621, AggregateVerifier 0.3.0): fork-gated proposal intervals, 600/30 before Denim and 6000/300 after.
  2. ProtocolVersions.registerUpgrade: Denim (upgrade id 13) at 1793901600, with the minimum protocol version raised from v1.4.2 to v1.5.1.
  3. 13 OptimismPortal deposits to the L2 ProxyAdmin, each with a 300,000 L2 gas limit:
Predeploy Live on mainnet New (base/contracts v8.4.0) Method
L2ToL1MessagePasser 1.0.0 1.2.0 upgrade
L1Block 1.7.0 1.8.0 upgrade
Sequencer / Base / L1 FeeVault 1.3.0 1.6.0 upgradeAndCall initialize (keeps recipient, 2 ETH min, L2 network)
OptimismMintableERC721Factory 1.2.0 1.4.2 upgrade
OperatorFeeVault 1.0.0 1.1.0 upgradeAndCall initialize (keeps recipient, 0 min, L2 network)
SchemaRegistry 1.0.1 1.3.1-beta.2 upgrade
EAS 1.0.1 1.4.1-beta.3 upgrade
L2CrossDomainMessenger 1.4.0 2.2.0 LegacyMigrator
L2StandardBridge 1.1.0 1.13.0 LegacyMigrator
OptimismMintableERC20Factory 1.9.0 1.10.2 LegacyMigrator
L2ERC721Bridge 1.1.0 1.10.0 LegacyMigrator

The eight upgrades from the Sepolia task archive/evm/2026-10-20-upgrade-remaining-l2-predeploys were copied into this task, along with its migrator, deploy scripts and fork tests. Every guard was rewritten for mainnet, because mainnet runs older legacy versions with different storage. The legacy migrator is a delegatecall-only helper. Inside one upgradeAndCall, it clears the legacy initializer slot, installs the final implementation and calls its initialize, so a failed init reverts the whole L2 call.

BaseTime (0x4200…0030) is excluded. The Denim hardfork installs it only while its implementation slot is empty. On mainnet that slot is empty and the proxy admin is 0x4200…0018, and the script checks both before and after the L2 replay.

The Sepolia and Zeronet paths (ExecuteDenimUpgrade, ExecuteDenimUpgradeWithL2Predeploys) and their Makefile behavior are unchanged. Mainnet selects the new ExecuteDenimUpgradeWithAllL2Predeploys script through UPGRADE_SCRIPT_NAME.

Notes to reviewers

Proof program hashes. Mainnet provers run a base/base releases/v1.5.2 build (6efa7566). The Sepolia build (f3a459f) cannot be used: it sets mainnet Denim to ForkCondition::Never, so it cannot derive post-Denim mainnet blocks. The TEE image hash is keccak256 of the PCR0 of the Nitro enclave image reproducibly built from that commit. The ZK range and aggregate hashes are the SP1 vkeys from just succinct vkeys --build at the same commit. Provenance is in config/mainnet/.env. The same methods reproduce Sepolia's live hashes from f3a459f. The prover enclave and zk images must be built from exactly 6efa7566 with the same recipe commit and toolchain, or these hashes stop matching.

Storage analysis. Each live mainnet implementation was matched to its verified source and compiled with its original solc to get its storage layout, then compared against the v8.4.0 layouts and live slot reads:

  • L2ToL1MessagePasser 1.0.0: sentMessages stays at slot 0 and msgNonce at slot 1. L1Block 1.7.0: layout identical through slot 8.
  • Fee vaults 1.3.0 and OperatorFeeVault 1.0.0: totalProcessed stays in slot 0. The new min, recipient and network fields land in slots 1 and 2, which are zero live, as is the ERC-7201 initializable slot. Initialize is reachable and does not touch slot 0.
  • SchemaRegistry and EAS 1.0.1: _registry stays at slot 0. EAS _nonces stays at 0, _db, _timestamps and _revocationsOffchain stay at 50 to 52, and the struct layouts are unchanged. EAS's EIP-712 domain version changes, so outstanding delegated signatures must be signed again.
  • ERC721 factory 1.2.0: the isOptimismMintableERC721 mapping stays at slot 0. Bridge and remote chain id are constructor immutables (0x4200…0014, 1).
  • Messenger 1.4.0: slot 0 is exactly 1 << 160 (initialized=1 behind a zero spacer). Slots 1 to 206 are unchanged. The new otherMessenger uses gap slot 207, which is zero live. The nonce and the idle sender 0xdead are preserved.
  • StandardBridge 1.1.0 and ERC721 bridge 1.1.0: slot 0 is zero. The new messenger and counterpart fields use gap slots that are zero live. deposits stays at slot 2.
  • ERC20 factory 1.9.0: slot 0 holds only the initializer flag (1). The bridge stays at slot 1 (0x4200…0010) and is rewritten to the same value. The new deployments mapping uses gap slot 2, and tokens created before the upgrade are not backfilled into it.
  • No live data lands in a slot the new code reinterprets.

Gas. Each L2 call was measured as its own cold transaction on a mainnet fork (forge test --isolate), counting deposit intrinsic and calldata gas. The heaviest are the StandardBridge and ERC721 bridge migrations at about 127k gas in total and the fee vault initializers at about 121k, so the 300,000 limit is at least 2.3x the heaviest call. The 13 deposits buy 3.9M L2 gas against the portal's 20M per-block resource limit. The cold two-owner L1 execution is about 4.2M gas with the portal at its 1 gwei floor. Each extra gwei of portal prevBaseFee adds roughly 3.9M burn gas while L1 basefee is below 1 gwei. setUp reads the live portal prevBaseFee and L1 basefee and refuses when the projected L1 gas (a 5M baseline plus the metering burn) exceeds 14M, leaving about 2.8M below the cap. Immediately before execution, run make TASK_NETWORK=mainnet simulate-current, which simulates the real transaction with the deployed addresses against live L1 and L2 state.

Signers. Task origin validation is required on mainnet, so the generated taskOriginConfig must be kept. The post-check decodes every captured deposit (target, value, gas limit, creation flag, calldata, order) and replays exactly the deposited calldata on the L2 fork. check-upgrade-env fails if the script reads an env var that the validation command does not pass, since signers' sign-task runs only with those assignments. Base multisig 3-of-6, Security Council 8-of-11, ProxyAdmin owner 2-of-2.

What the facilitator still has to do (not in this PR):

  1. make TASK_NETWORK=mainnet deploy, then verify, then deploy-l2, then deploy-remaining-l2, each with a Ledger. This deploys AggregateVerifier, the 13 L2 implementations and the LegacyMigrator, and writes config/mainnet/addresses.json.
  2. Commit addresses.json and records/, and fill the implementation addresses into config/mainnet/README.md.
  3. Generate base-signer.json and security-council-signer.json (keeping task origin config), review them, commit, then collect signatures.
  4. Run simulate-current, then execute at least one hour before activation (ProtocolVersions MIN_NOTICE). Then immediately roll mainnet provers and the proposer to the v1.5.2 build, and check that each new TEE signer's image hash equals the configured TEE hash.

The validation, approval and execution targets refuse to run until all 15 addresses are present and the three hashes are set.

How has it been tested?

Mainnet fork tests (make TASK_NETWORK=mainnet test, Ethereum block 26250153, Base block 52953400), 15 passing:

  • testMainnetSignerSimulation: the full 15-call transaction through MultisigScript's signer simulation on an L1 fork, using locally deployed implementations. The script's post-check then replays all 13 L2 calls on an L2 fork as the aliased owner and checks every implementation, version, initializer flag and configuration value.
  • testLiveRecordsSurviveUpgrade: byte-compares real live records before and after the upgrade: a relayed messenger message, an EAS attestation and its schema, a factory-created ERC721, a message-passer withdrawal and nonce, vault accounting and balances, all L1Block slots and messenger slots 1 to 206. The StandardBridge deposits entry is synthetic, because sampled events showed no nonzero live entry. It is read through the public deposits(address,address) getter before and after the upgrade.
  • testRevertingInitializersRollBack: a failing initializer leaves the implementation and storage unchanged, for all four migrations and all four vault initializers.
  • testBridgeMintBurnAndETHWithdrawal: creates an ERC20 through the new factory, mints it via a messenger relay from the aliased L1 messenger, burns it via bridgeERC20To, and checks that an ETH withdrawal reaches L2ToL1MessagePasser.
  • testColdL2DepositGas, testColdL1OwnerTransactionGas (run with --isolate): each L2 call's cold gas plus intrinsic and calldata gas is at most two thirds of its deposit limit, and the cold L1 owner transaction fits inside the guard's baseline allowance.
  • testPortalFeeSensitivity: the L1 gas bound when the portal fee rises.
  • testRejectsCorruptedDeposits: a deposit with a corrupted target, value, gas limit, creation flag, calldata or order is rejected.
  • testRejectsInstalledBaseTime, testRejectsWrongBaseTimeAdmin: the script refuses when BaseTime's state would block the hardfork install.
  • testRejectsPortalFeesWithoutGasHeadroom, testRejectsPortalFeeIncreaseAfterFullBlock, testPortalGasBudgetUsesL1Basefee: the L1 gas budget guard.
  • testDirectMigratorCallReverts and testRejectsChangedSafeThreshold.

Also checked: the multisig script builds from active/evm with default settings, forge fmt --check passes, make check-task-status passes, and cast keccak of the PCR0 equals the configured TEE hash.

jackchuma and others added 3 commits October 22, 2026 16:09
@cb-heimdall

cb-heimdall commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/2
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 2
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 2
2
1 if commit is unverified 0
Sum 2

@jackchuma

Copy link
Copy Markdown
Contributor Author

Review round 1

Reviewers: anthropic/claude-opus-5-5, cbhq-openai/gpt-6.1-sol (parallel)

  • Fixed: [P2] Pre-execution simulation used historical fork state; added simulate-current against live state with real addresses, plus a setUp guard bounding projected L1 gas (portal metering burn at live prevBaseFee and basefee) at 14M, below the 16,777,216 per-transaction cap. Raised by gpt; gas-cap part promoted from opus residual risks. (f71a473)
  • Fixed: [P2] Synthetic StandardBridge escrow fixture now checked through the public deposits(address,address) getter before and after the upgrade (gpt) (f71a473)
  • Fixed: [P3] Signer README stale prover-hash wording and signing directory (both, listed as dismissed) (f71a473)
  • Fixed: [P3] Mainnet check-implementations message (both, listed as dismissed) (f71a473)
    Synced with main: fcaed94 (already current)
    Validation: 10 mainnet fork tests pass (devbox), forge fmt --check, make check-task-status

🤖 Reply generated with Toshi

@jackchuma

Copy link
Copy Markdown
Contributor Author

Review round 2

Reviewers: anthropic/claude-opus-5-5, cbhq-openai/gpt-6.1-sol (parallel): no findings. Spec check (anthropic/claude-opus-5-5) raised:

  • Fixed: [P1] Validation command omitted CB_MULTISIG and BASE_SECURITY_COUNCIL, so sign-task would revert; added to mainnet UPGRADE_ENV plus check-upgrade-env (9e565c2)
  • Fixed: [P1] L2 gas measurements were warm and ignored deposit intrinsic gas; now isolated cold transactions with intrinsic and calldata gas, heaviest 126,660 of 300,000; cold L1 owner tx 4,218,345 (9e565c2)
  • Fixed: [P2] Deposit contents were not checked; post-check decodes each deposit and replays the deposited calldata, with negative tests (9e565c2)
  • Fixed: [P2] BaseTime guard had no negative test (9e565c2)
    Synced with main: fcaed94 (already current)
    Validation: 15 mainnet fork tests pass (devbox), forge fmt --check, make check-task-status

🤖 Reply generated with Toshi

Co-authored-by: Toshi <toshi-noreply@coinbase.com>
@jackchuma

Copy link
Copy Markdown
Contributor Author

Review round 3

Reviewers: anthropic/claude-opus-5-5, cbhq-openai/gpt-6.1-sol (parallel)

  • Fixed: [P3] Point-in-time gas measurements copied into FACILITATOR.md and a script comment; the guide now states only the enforced rules and names the tests that pin them (opus) (be2f94e)
    Synced with main: fcaed94 (already current)
    Validation: 15 mainnet fork tests pass (devbox), forge fmt --check

🤖 Reply generated with Toshi

@jackchuma

Copy link
Copy Markdown
Contributor Author

Review round 4

Reviewer: cbhq-openai/gpt-6.1-sol: no findings on 2c88101.
Synced with main: c4663a9 (clean merge, d4aa1ea)
Validation: 15 mainnet fork tests pass (devbox), make check-task-status

🤖 Reply generated with Toshi

@jackchuma
jackchuma marked this pull request as ready for review October 26, 2026 18:09
@jackchuma
jackchuma marked this pull request as draft October 26, 2026 18:29
deploy gen-validation-cb gen-validation-sc approve-cb approve-sc execute: check-prover-hashes
deps-signer-tool: check-prover-hashes

deploy-remaining-l2:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mainnet flow deploys 13 L2 implementations and LegacyMigrator with no source-verification step; only AggregateVerifier gets forge verify-contract, and the post-check only asserts version(), selected getters and slots. Signers cannot confirm that the addresses in addresses.json hold the v8.4.0 bytecode, which matters most for LegacyMigrator: it runs as a delegatecall in the messenger and both bridges and can write storage the post-check never reads (successfulMessages, deposits).
Add a verify-l2 target for all 14 contracts on Basescan and list the verified links in config/mainnet/README.md before signing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added verify-l2 in 4d509a2 (FACILITATOR fix in 9639477). It is mainnet-only and purely additive; the deploy targets are unchanged. It verifies all 14 contracts on Basescan (Etherscan v2, chainid 8453) from config/mainnet/addresses.json. Each contract uses its deployment profile: solc 0.8.15/0.8.19/0.8.25, 5000 runs for both factories and 999999 for the rest, no bytecode hash, no via-ir. The ERC721 factory gets its constructor args. It stops on a missing or zero address and waits on each result (--watch), so any failed verification stops the run. FACILITATOR now requires VERIFIER_API_KEY=<key> make TASK_NETWORK=mainnet verify-l2 after deploy-remaining-l2. All 14 verified links must be filled into config/mainnet/README.md before generating validations or signing; the README has a slot per contract.

Standard JSON generated with the same settings compiles to the runtime bytecode deployed on Base Sepolia for all 13 implementations. ERC721Factory and EAS match after masking immutables. LegacyMigrator has no Sepolia deployment.

🤖 Reply generated with Toshi

@jackchuma

Copy link
Copy Markdown
Contributor Author

Review round 5

Reviewer: anthropic/claude-opus-5-5

  • Fixed: [P3] FACILITATOR verify-l2 command omitted VERIFIER_API_KEY and pointed at config/<network>/README.md for a mainnet-only step (opus) (9639477)
    Synced with main: c4663a9 (already current)
    Validation: 15 mainnet fork tests pass (devbox); make -n TASK_NETWORK=mainnet verify-l2 with fixture addresses; Sepolia runtime bytecode matches for all 13 implementations

🤖 Reply generated with Toshi

@jackchuma
jackchuma marked this pull request as ready for review October 27, 2026 20:55

@0xth4nh 0xth4nh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks Good to Thanh's Toshi

Co-authored-by: Toshi <toshi-noreply@coinbase.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants