Repository navigation
Conversation
Co-authored-by: Toshi <toshi-noreply@coinbase.com>
Co-authored-by: Toshi <toshi-noreply@coinbase.com>
🟡 Heimdall Review Status
|
Review round 1Reviewers: anthropic/claude-opus-5-5, cbhq-openai/gpt-6.1-sol (parallel)
🤖 Reply generated with Toshi |
Review round 2Reviewers: anthropic/claude-opus-5-5, cbhq-openai/gpt-6.1-sol (parallel): no findings. Spec check (anthropic/claude-opus-5-5) raised:
🤖 Reply generated with Toshi |
Co-authored-by: Toshi <toshi-noreply@coinbase.com>
Review round 3Reviewers: anthropic/claude-opus-5-5, cbhq-openai/gpt-6.1-sol (parallel)
🤖 Reply generated with Toshi |
Co-authored-by: Toshi <toshi-noreply@coinbase.com>
…2-predeploys-task
Source: https://github.com/base/base/tree/6efa75662b57c578d897fea2449035636f6ad9ad Co-authored-by: Toshi <toshi-noreply@coinbase.com>
| deploy gen-validation-cb gen-validation-sc approve-cb approve-sc execute: check-prover-hashes | ||
| deps-signer-tool: check-prover-hashes | ||
|
|
||
| deploy-remaining-l2: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Co-authored-by: Toshi <toshi-noreply@coinbase.com>
Review round 5Reviewer: anthropic/claude-opus-5-5
🤖 Reply generated with Toshi |
0xth4nh
left a comment
There was a problem hiding this comment.
Looks Good to Thanh's Toshi
Co-authored-by: Toshi <toshi-noreply@coinbase.com>
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:
DisputeGameFactory.setImplementation(621, AggregateVerifier 0.3.0): fork-gated proposal intervals, 600/30 before Denim and 6000/300 after.ProtocolVersions.registerUpgrade: Denim (upgrade id 13) at1793901600, with the minimum protocol version raised from v1.4.2 to v1.5.1.OptimismPortaldeposits to the L2 ProxyAdmin, each with a 300,000 L2 gas limit:The eight upgrades from the Sepolia task
archive/evm/2026-10-20-upgrade-remaining-l2-predeployswere 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 oneupgradeAndCall, it clears the legacy initializer slot, installs the final implementation and calls itsinitialize, 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 is0x4200…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 newExecuteDenimUpgradeWithAllL2Predeploysscript throughUPGRADE_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 toForkCondition::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 fromjust succinct vkeys --buildat the same commit. Provenance is inconfig/mainnet/.env. The same methods reproduce Sepolia's live hashes fromf3a459f. The prover enclave and zk images must be built from exactly6efa7566with 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:
sentMessagesstays at slot 0 andmsgNonceat slot 1. L1Block 1.7.0: layout identical through slot 8.totalProcessedstays 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._registrystays at slot 0. EAS_noncesstays at 0,_db,_timestampsand_revocationsOffchainstay 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.isOptimismMintableERC721mapping stays at slot 0. Bridge and remote chain id are constructor immutables (0x4200…0014, 1).1 << 160(initialized=1 behind a zero spacer). Slots 1 to 206 are unchanged. The newotherMessengeruses gap slot 207, which is zero live. The nonce and the idle sender0xdeadare preserved.depositsstays at slot 2.0x4200…0010) and is rewritten to the same value. The newdeploymentsmapping uses gap slot 2, and tokens created before the upgrade are not backfilled into it.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 portalprevBaseFeeadds roughly 3.9M burn gas while L1 basefee is below 1 gwei.setUpreads the live portalprevBaseFeeand 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, runmake 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
taskOriginConfigmust 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-envfails if the script reads an env var that the validation command does not pass, since signers'sign-taskruns 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):
make TASK_NETWORK=mainnet deploy, thenverify, thendeploy-l2, thendeploy-remaining-l2, each with a Ledger. This deploys AggregateVerifier, the 13 L2 implementations and the LegacyMigrator, and writesconfig/mainnet/addresses.json.addresses.jsonandrecords/, and fill the implementation addresses intoconfig/mainnet/README.md.base-signer.jsonandsecurity-council-signer.json(keeping task origin config), review them, commit, then collect signatures.simulate-current, then execute at least one hour before activation (ProtocolVersionsMIN_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 StandardBridgedepositsentry is synthetic, because sampled events showed no nonzero live entry. It is read through the publicdeposits(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 viabridgeERC20To, 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.testDirectMigratorCallRevertsandtestRejectsChangedSafeThreshold.Also checked: the multisig script builds from
active/evmwith default settings,forge fmt --checkpasses,make check-task-statuspasses, andcast keccakof the PCR0 equals the configured TEE hash.