fix(bridge): unblock the mainnet Bridgehub cutover - #124
Merged
Conversation
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>
LCOV of commit
|
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.
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
grantRolefrom the deployer keyDEFAULT_ADMIN_ROLEis Safe-held on both chains; deployer holds nothing. Confirmed on a fork: revertsAccessControlUnauthorizedAccount(0x49dd…1c62, DEFAULT_ADMIN_ROLE). Sepolia passed only because its deployer holds admin there.MINTER_ROLEover live NODL.forge verify-contractdoes not infer a seven-arg constructor.LEGACY_BRIDGEmiss was a warning; deploy steps skip whenL1_BRIDGE/L2_BRIDGEare setScripts now deploy only and print the required Safe calldata;
initialize()does the same when the L2 bridge owner is a contract.REDEPLOY=1makes both config conditions hard errors.Sequencing correction
finalizeWithdrawalpins the message sender to the immutableL2_BRIDGE_ADDR(L1Bridge.sol:313), and the cutover redeploysL2Bridge. 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_BRIDGEguard is consulted.LEGACY_BRIDGEstays set as defense-in-depth.Test plan
forge test— 1254 passed, 0 failedtest_Fork_NewL2Wiring_BlocksOldL2WithdrawalEvenUnguarded— A/B against the existing double-mint test: same real proof, same unguarded deployment, onlyL2_BRIDGE_ADDRdiffers → old L2 mints, new L2 revertsInvalidProofDeployL1Bridgedry-run on a mainnet fork — deploys, prints Safe calldata, 1,535,467 gasgrantRolefrom deployer reverts / from Safe succeeds, verified viacast callyarn lint— 0 errors;bash -nclean; guard logic exercisedNot addressed
eth_getLogswill not complete on a public endpoint.🤖 Generated with Claude Code