Skip to content

fix(bridge): Bridgehub migration, hardened with legacy-withdrawal replay guard + live-state fork tests#122

Merged
aliXsed merged 7 commits into
mainfrom
fix/104-bridgehub-migration-hardened
Jul 9, 2026
Merged

fix(bridge): Bridgehub migration, hardened with legacy-withdrawal replay guard + live-state fork tests#122
aliXsed merged 7 commits into
mainfrom
fix/104-bridgehub-migration-hardened

Conversation

@aliXsed

@aliXsed aliXsed commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #121 by @DrVelvetFog — the Bridgehub transport migration there was correct and is included here unchanged (with authorship preserved in the commit history). This PR adds what live-state verification showed was still needed before the migration is deployable. Closes #104.

Summary

  • All of fix(bridge): migrate L1 deposits and quotes from deprecated Mailbox to Bridgehub #121: deposits via Bridgehub.requestL2TransactionDirect, quotes via Bridgehub.l2TransactionBaseCost, proof paths untouched on the Diamond, MockBridgehub unit coverage, fork-PR coverage CI fix.
  • New: LEGACY_BRIDGE replay guard. Withdrawal messages are (selector, receiver, amount) with no nonce; replay protection is each instance's own isWithdrawalFinalized map, which starts empty on a redeployment — and this migration forces a redeployment (L1_MAILBOX is immutable). Verified on a mainnet fork: a fresh bridge with MINTER_ROLE re-mints withdrawals the live bridge already paid out (~347M NODL replayable today, incl. a 286.7M and a 60.2M withdrawal, batch 511315). finalizeWithdrawal now reverts with WithdrawalFinalizedOnLegacyBridge for anything the predecessor already finalized.
  • New: mainnet-fork test suite (test/fork/L1BridgeMainnetFork.t.sol, self-skips without MAINNET_RPC_URL) — validates the migration against the real deployed zkSync contracts, not mocks, including a simulated Mailbox cutoff and a real historical withdrawal proof.
  • New: cutover runbook + deprecation canary (ops/bridgehub-migration-cutover.md, ops/check_zksync_deprecation.sh).

Live-state verification (2026-07-09)

Check Result
Full unit suite 1203 passed, 0 failed (fork tests skip without RPC)
Fork: deposit via real Bridgehub → priority op on Era Diamond
Fork: deposit survives simulated requestL2Transaction cutoff (old bridge breaks, new works)
Fork: Bridgehub quote == legacy Mailbox quote ✅ byte-identical
Fork: real withdrawal proof (batch 503366, idx 17, already paid) blocked by guard WithdrawalFinalizedOnLegacyBridge
Fork: same proof on an unguarded deployment ⚠️ re-mints — the guard is load-bearing
Era protocol versions mainnet v29.4, Sepolia v29.1 — deprecation not yet enforced (announcement, cutoff ~mid-Sept 2026)
yarn lint / yarn spellcheck 0 errors / 0 issues

Deployment (see ops/bridgehub-migration-cutover.md for the full runbook)

Constructor gains _legacyBridge (zero allowed only for first-ever deployments). Key sequencing rule: the old bridge must be paused (and optionally de-minted) in the same ops window the new bridge goes live — until then a withdrawal can be finalized on both instances. In-flight withdrawals are not stuck: the guard only blocks pairs the old bridge already paid, so pre-cutover withdrawals finalize on the new bridge.

Test plan

  • forge test — 1203/1203
  • MAINNET_RPC_URL=… forge test --match-contract L1BridgeMainnetForkTest — 5/5 against live mainnet state
  • ops/check_zksync_deprecation.sh mainnet|sepolia — reports v29.4 / v29.1, legacy path still accepted
  • Sepolia rehearsal per runbook (deposit → mint, withdraw → finalize, failed deposit → claim, legacy replay → revert)
  • Rerun Sepolia loop once the canary detects testnet enforcement, then schedule mainnet cutover

🤖 Generated with Claude Code

DrVelvetFog and others added 6 commits July 2, 2026 08:38
ZKsync has deprecated Mailbox.requestL2Transaction (and its
l2TransactionBaseCost quoting) in favor of the Bridgehub, with the
legacy entry points scheduled to stop working around mid-September.
This routes L1Bridge deposits through Bridgehub.requestL2TransactionDirect
and sources base-cost quotes from Bridgehub.l2TransactionBaseCost,
scoped by the new L2_CHAIN_ID immutable.

The Mailbox (Diamond proxy) stays for the L2->L1 proof paths
(proveL1ToL2TransactionStatus / proveL2MessageInclusion), which are
not deprecated. External function signatures are unchanged; the
constructor gains _bridgehub and _l2ChainId, so this ships as a new
deployment rather than an upgrade (cutover notes in the PR).

- src/bridge/L1Bridge.sol: Bridgehub routing for deposit + quotes,
  mintValue = msg.value per the Bridgehub's ETH-chain invariant
- test: new MockBridgehub (mirrors the MockMailbox not-inheriting
  pattern); MockMailbox trimmed to the proof paths; new coverage for
  request-field mapping and zero-bridgehub / zero-chain-id constructor
  guards (26 bridge tests, full suite 1080 green)
- script/DeployL1Bridge.s.sol + ops/deploy_L1L2_bridge.sh: BRIDGEHUB
  and L2_CHAIN_ID env vars

Closes #104

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Coverage job's 'Report coverage to PR' step fails on pull requests
from forks with 'Resource not accessible by integration': GitHub caps
GITHUB_TOKEN at read-only for fork-triggered pull_request runs, so the
sticky comment can never be posted (same-repo PRs are unaffected and
keep the comment). Gate the step on the head repo being this repo; the
coverage run, artifact upload, and threshold check still execute for
fork PRs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… deployment

Withdrawal messages carry no nonce, so replay protection is each instance's
own isWithdrawalFinalized map — a redeployment (forced by the Bridgehub
migration, since L1_MAILBOX is immutable) would re-mint every withdrawal the
old bridge already paid out (~347M NODL currently replayable, verified on a
mainnet fork). New LEGACY_BRIDGE immutable makes finalizeWithdrawal revert
for (batch, index) pairs the predecessor already finalized.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n live state

Skipped unless MAINNET_RPC_URL is set. Against the real deployed zkSync
contracts: deposits enqueue a priority op via the Bridgehub, survive a
simulated legacy-Mailbox cutoff, quotes match the Mailbox exactly, and a
real historical withdrawal proof exercises both the proof path and the
LEGACY_BRIDGE replay guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runbook covers deployment sequencing (why the old bridge must stop minting
in the same ops window), in-flight deposit/withdrawal handling, and the
Sepolia rehearsal. The canary script reports the Era protocol version and
exits non-zero once the deprecated entrypoint starts reverting, for cron/CI.

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

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

LCOV of commit 6591d26 during checks #749

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%     28| 0.0%     2| 0.0%      3
  src/bridge/L1Bridge.sol                           | 0.0%     65| 0.0%     9| 0.0%     11
  test/bridge/L1Bridge.t.sol                        | 0.0%     37| 0.0%     9| 0.0%     11

@aliXsed aliXsed merged commit 5d70e85 into main Jul 9, 2026
3 checks passed
@aliXsed aliXsed deleted the fix/104-bridgehub-migration-hardened branch July 9, 2026 04:12
@DrVelvetFog

Copy link
Copy Markdown
Contributor

Appreciate this, @aliXsed — and that you fork-tested it against live state before merging. Nice catch turning the redeployment-sequencing note into the LEGACY_BRIDGE guard; a fresh instance's empty isWithdrawalFinalized map is exactly the kind of thing that bites on cutover. Happy to help with the deployment sequencing / MINTER_ROLE handoff whenever you schedule it.

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.

ZKsync Mailbox functions deprecation

2 participants