fix(bridge): Bridgehub migration, hardened with legacy-withdrawal replay guard + live-state fork tests#122
Merged
Conversation
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>
LCOV of commit
|
Contributor
|
Appreciate this, @aliXsed — and that you fork-tested it against live state before merging. Nice catch turning the redeployment-sequencing note into the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Bridgehub.requestL2TransactionDirect, quotes viaBridgehub.l2TransactionBaseCost, proof paths untouched on the Diamond,MockBridgehubunit coverage, fork-PR coverage CI fix.LEGACY_BRIDGEreplay guard. Withdrawal messages are(selector, receiver, amount)with no nonce; replay protection is each instance's ownisWithdrawalFinalizedmap, which starts empty on a redeployment — and this migration forces a redeployment (L1_MAILBOXis immutable). Verified on a mainnet fork: a fresh bridge withMINTER_ROLEre-mints withdrawals the live bridge already paid out (~347M NODL replayable today, incl. a 286.7M and a 60.2M withdrawal, batch 511315).finalizeWithdrawalnow reverts withWithdrawalFinalizedOnLegacyBridgefor anything the predecessor already finalized.test/fork/L1BridgeMainnetFork.t.sol, self-skips withoutMAINNET_RPC_URL) — validates the migration against the real deployed zkSync contracts, not mocks, including a simulated Mailbox cutoff and a real historical withdrawal proof.ops/bridgehub-migration-cutover.md,ops/check_zksync_deprecation.sh).Live-state verification (2026-07-09)
requestL2Transactioncutoff (old bridge breaks, new works)WithdrawalFinalizedOnLegacyBridgeyarn lint/yarn spellcheckDeployment (see
ops/bridgehub-migration-cutover.mdfor 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/1203MAINNET_RPC_URL=… forge test --match-contract L1BridgeMainnetForkTest— 5/5 against live mainnet stateops/check_zksync_deprecation.sh mainnet|sepolia— reports v29.4 / v29.1, legacy path still accepted🤖 Generated with Claude Code