You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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
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>
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.
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
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