Skip to content

fix(bridge): unblock the mainnet Bridgehub cutover - #124

Merged
aliXsed merged 1 commit into
mainfrom
fix/bridgehub-cutover-blockers
Jul 28, 2026
Merged

fix(bridge): unblock the mainnet Bridgehub cutover#124
aliXsed merged 1 commit into
mainfrom
fix/bridgehub-cutover-blockers

Conversation

@aliXsed

@aliXsed aliXsed commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Pre-flight for the mainnet Bridgehub cutover against live state surfaced three defects that would have broken it, plus a sequencing error that would have stranded user funds. None were reachable from the Sepolia rehearsal.

Blockers fixed

# Defect Impact if unfixed
1 Deploy scripts call grantRole from the deployer key Cutover halts. DEFAULT_ADMIN_ROLE is Safe-held on both chains; deployer holds nothing. Confirmed on a fork: reverts AccessControlUnauthorizedAccount(0x49dd…1c62, DEFAULT_ADMIN_ROLE). Sepolia passed only because its deployer holds admin there.
2 L1 verification invoked with no constructor args Unverified bridge holding MINTER_ROLE over live NODL. forge verify-contract does not infer a seven-arg constructor.
3 LEGACY_BRIDGE miss was a warning; deploy steps skip when L1_BRIDGE/L2_BRIDGE are set Either a bridge with no replay guard, or a cutover run that silently deploys nothing.

Scripts now deploy only and print the required Safe calldata; initialize() does the same when the L2 bridge owner is a contract. REDEPLOY=1 makes both config conditions hard errors.

Sequencing correction

finalizeWithdrawal pins the message sender to the immutable L2_BRIDGE_ADDR (L1Bridge.sol:313), and the cutover redeploys L2Bridge. So every in-flight withdrawal — necessarily initiated on the old L2 bridge — can only ever settle on the old L1 bridge.

The runbook said to pause the old bridge as promptly as possible. That would have stranded every in-flight withdrawal: burned on L2, with no L1 instance that will pay out. The double-finalization risk motivating the short gap does not apply here, because the sender pin means the two instances accept disjoint sets of withdrawals.

New order: pause old L2 bridge → drain in-flight withdrawals on the old L1 bridge → pause old L1 + revoke MINTER_ROLE.

The same pin means historical replay is already blocked before the LEGACY_BRIDGE guard is consulted. LEGACY_BRIDGE stays set as defense-in-depth.

Test plan

  • forge test — 1254 passed, 0 failed
  • Fork suite against live mainnet — 6/6 (was 5/5; +1 new)
  • New test_Fork_NewL2Wiring_BlocksOldL2WithdrawalEvenUnguarded — A/B against the existing double-mint test: same real proof, same unguarded deployment, only L2_BRIDGE_ADDR differs → old L2 mints, new L2 reverts InvalidProof
  • DeployL1Bridge dry-run on a mainnet fork — deploys, prints Safe calldata, 1,535,467 gas
  • grantRole from deployer reverts / from Safe succeeds, verified via cast call
  • yarn lint — 0 errors; bash -n clean; guard logic exercised
  • Dry-run each Safe transaction (Tenderly) before the real cutover

Not addressed

  • Deposit enumeration (runbook pre-check) still outstanding — needs an indexed RPC; a 2M-block eth_getLogs will not complete on a public endpoint.
  • Deployer L1 funding is 0.00297 ETH. Fine at today's 0.3–0.5 gwei, but a spike mid-cutover strands it. Top up to ~0.05 ETH before executing.
  • The runbook's own gate is still closed. Canary as of 2026-07-28: mainnet v29.5, Sepolia v29.1, legacy entrypoint still accepted on both. The post-enforcement Sepolia rerun has not happened, so this PR stages the cutover rather than triggering it.

🤖 Generated with Claude Code

Dry-running the cutover against live mainnet state surfaced three defects that
would have broken it, none of which the Sepolia rehearsal could catch.

The deploy scripts granted MINTER_ROLE from the broadcasting deployer key. On
mainnet that role is Safe-held and the deployer holds nothing, so both scripts
revert (AccessControlUnauthorizedAccount, confirmed on a fork). Sepolia passed
only because its deployer happens to hold DEFAULT_ADMIN_ROLE there. The scripts
now deploy only and emit the grant as calldata for the Safe; initialize() does
the same when the L2 bridge owner is a contract.

L1 verification was invoked with no constructor args for a seven-arg
constructor, so the cutover would have left an unverified bridge holding
MINTER_ROLE. Both L1 verify calls now pass encoded args.

A LEGACY_BRIDGE miss was only a warning, and the deploy steps silently skip
when L1_BRIDGE/L2_BRIDGE are set - as they are on a cutover. REDEPLOY=1 now
makes both conditions hard errors.

Also correct the cutover ordering. finalizeWithdrawal pins the message sender
to the immutable L2_BRIDGE_ADDR, and the cutover redeploys L2Bridge, so every
in-flight withdrawal (necessarily initiated on the old L2 bridge) can only ever
settle on the old L1 bridge. Pausing it promptly - as the runbook advised -
strands all of them. Pause the old L2 bridge first, drain, then pause L1. The
double-finalization risk that motivated the short gap does not apply, since the
sender pin means the two instances accept disjoint sets of withdrawals.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

LCOV of commit 4bacffa during checks #753

Summary coverage rate:
  lines......: 26.9% (939 of 3494 lines)
  functions..: 27.4% (150 of 547 functions)
  branches...: 27.7% (164 of 592 branches)

Files changed coverage rate:
                                                    |Lines       |Functions  |Branches    
  Filename                                          |Rate     Num|Rate    Num|Rate     Num
  ========================================================================================
  script/DeployL1Bridge.s.sol                       | 0.0%     27| 0.0%     2| 0.0%      3
  script/DeployL2Bridge.s.sol                       | 0.0%     15| 0.0%     2|    -      0

@aliXsed
aliXsed merged commit 73deb56 into main Jul 28, 2026
3 checks passed
@aliXsed
aliXsed deleted the fix/bridgehub-cutover-blockers branch July 28, 2026 03:45
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.

1 participant