From 57006ac3280dca476f71e9f2984a9d808649a3b1 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 2 Jul 2026 08:38:11 -0500 Subject: [PATCH 1/2] fix(bridge): migrate L1 deposits and quotes from Mailbox to Bridgehub 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 --- .cspell.json | 3 + ops/deploy_L1L2_bridge.sh | 6 +- script/DeployL1Bridge.s.sol | 11 +- src/bridge/L1Bridge.sol | 71 +++++++++--- src/bridge/interfaces/IL1Bridge.sol | 12 +- test/bridge/L1Bridge.t.sol | 165 ++++++++++++++++++++-------- 6 files changed, 197 insertions(+), 71 deletions(-) diff --git a/.cspell.json b/.cspell.json index 66a740a6..375f0f75 100644 --- a/.cspell.json +++ b/.cspell.json @@ -35,6 +35,9 @@ "IERC", "Mintable", "BOOTLOADER", + "Bridgehub", + "BRIDGEHUB", + "bridgehub", "devcontainer", "gasleft", "chainid", diff --git a/ops/deploy_L1L2_bridge.sh b/ops/deploy_L1L2_bridge.sh index 22dd49df..69428a19 100755 --- a/ops/deploy_L1L2_bridge.sh +++ b/ops/deploy_L1L2_bridge.sh @@ -131,7 +131,7 @@ main() { L2_VERIFIER_URL="${L2_VERIFIER_URL:?Please set L2_VERIFIER_URL in .env}" # Check required environment variables - required_vars=("NODL_ADMIN" "NODL_MINTER" "L2_BRIDGE_OWNER" "L1_MAILBOX" "L1_BRIDGE_OWNER" "L1_CHAIN_ID" "L2_CHAIN_NAME") + required_vars=("NODL_ADMIN" "NODL_MINTER" "L2_BRIDGE_OWNER" "L1_MAILBOX" "BRIDGEHUB" "L2_CHAIN_ID" "L1_BRIDGE_OWNER" "L1_CHAIN_ID" "L2_CHAIN_NAME") for var in "${required_vars[@]}"; do if [ -z "${!var:-}" ]; then print_error "Required environment variable $var is not set!" @@ -281,7 +281,9 @@ main() { LOG_FILE="logs/deploy_l1_bridge.log" print_info "Deploying L1 Bridge..." print_info "Owner: $L1_BRIDGE_OWNER" - print_info "Mailbox: $L1_MAILBOX" + print_info "Mailbox (proofs): $L1_MAILBOX" + print_info "Bridgehub: $BRIDGEHUB" + print_info "L2 Chain ID: $L2_CHAIN_ID" print_info "L1 Token: $L1_NODL_ADDR" print_info "L2 Bridge: $L2_BRIDGE_ADDR" diff --git a/script/DeployL1Bridge.s.sol b/script/DeployL1Bridge.s.sol index b376764e..771d66c0 100644 --- a/script/DeployL1Bridge.s.sol +++ b/script/DeployL1Bridge.s.sol @@ -9,24 +9,31 @@ import {L1Nodl} from "../src/L1Nodl.sol"; /// @notice Forge script to deploy L1Bridge on EVM networks (e.g., Sepolia) /// Env vars required: /// - L1_BRIDGE_OWNER (address) -/// - L1_MAILBOX (address) +/// - L1_MAILBOX (address) — Diamond proxy, used for L2->L1 proofs +/// - BRIDGEHUB (address) — Bridgehub, used for deposits and base-cost quotes +/// - L2_CHAIN_ID (uint) — chain id of the target L2 as registered on the Bridgehub /// - NODL_L1 (address) /// - L2_BRIDGE (address) /// Deployer key must have DEFAULT_ADMIN_ROLE on L1Nodl to grant MINTER_ROLE. contract DeployL1Bridge is Script { address internal ownerAddr; address internal l1Mailbox; + address internal bridgehub; + uint256 internal l2ChainId; address internal l1Token; address internal l2Bridge; function setUp() public { ownerAddr = vm.envAddress("L1_BRIDGE_OWNER"); l1Mailbox = vm.envAddress("L1_MAILBOX"); + bridgehub = vm.envAddress("BRIDGEHUB"); + l2ChainId = vm.envUint("L2_CHAIN_ID"); l1Token = vm.envAddress("L1_NODL"); l2Bridge = vm.envAddress("L2_BRIDGE"); vm.label(ownerAddr, "L1_BRIDGE_OWNER"); vm.label(l1Mailbox, "L1_MAILBOX"); + vm.label(bridgehub, "BRIDGEHUB"); vm.label(l1Token, "L1_NODL"); vm.label(l2Bridge, "L2_BRIDGE"); } @@ -34,7 +41,7 @@ contract DeployL1Bridge is Script { function run() public { vm.startBroadcast(); - L1Bridge bridge = new L1Bridge(ownerAddr, l1Mailbox, l1Token, l2Bridge); + L1Bridge bridge = new L1Bridge(ownerAddr, l1Mailbox, bridgehub, l2ChainId, l1Token, l2Bridge); L1Nodl nodl = L1Nodl(l1Token); bytes32 minterRole = keccak256("MINTER_ROLE"); diff --git a/src/bridge/L1Bridge.sol b/src/bridge/L1Bridge.sol index 4778ac1c..71a059ab 100644 --- a/src/bridge/L1Bridge.sol +++ b/src/bridge/L1Bridge.sol @@ -5,6 +5,10 @@ pragma solidity ^0.8.26; import {L1Nodl} from "../L1Nodl.sol"; // Use local submodule paths instead of unavailable @zksync package imports import {IMailbox} from "lib/era-contracts/l1-contracts/contracts/state-transition/chain-interfaces/IMailbox.sol"; +import { + IBridgehub, + L2TransactionRequestDirect +} from "lib/era-contracts/l1-contracts/contracts/bridgehub/IBridgehub.sol"; import {L2Message, TxStatus} from "lib/era-contracts/l1-contracts/contracts/common/Messaging.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; @@ -19,19 +23,30 @@ import {IWithdrawalMessage} from "./interfaces/IWithdrawalMessage.sol"; * @title L1Bridge * @notice L1 endpoint of the NODL token bridge for zkSync Era. * @dev Responsibilities: - * - Initiate deposits by enqueuing an L2 call to the counterpart L2 bridge through the Mailbox. + * - Initiate deposits by enqueuing an L2 call to the counterpart L2 bridge through the Bridgehub. * - Track deposit tx hashes to enable refunds if an L2 transaction fails. * - Finalize L2→L1 withdrawals by verifying message inclusion and minting on L1. * - Secured with Ownable (admin), Pausable (circuit breaker). + * + * Deposits and base-cost quotes go through the Bridgehub ({requestL2TransactionDirect} / + * {l2TransactionBaseCost}), since the Mailbox equivalents are deprecated. The Mailbox (Diamond + * proxy) is still used for the L2→L1 proof paths ({proveL1ToL2TransactionStatus} / + * {proveL2MessageInclusion}), which are not deprecated. */ contract L1Bridge is Ownable2Step, Pausable, IL1Bridge { // ============================= // State // ============================= - /// @notice The zkSync Era Mailbox contract on L1 (Diamond proxy). + /// @notice The zkSync Era Mailbox contract on L1 (Diamond proxy). Used for L2→L1 proofs only. IMailbox public immutable L1_MAILBOX; + /// @notice The zkSync Bridgehub contract on L1. Entry point for deposits and base-cost quotes. + IBridgehub public immutable BRIDGEHUB; + + /// @notice The chain id of the target L2, as registered on the Bridgehub. + uint256 public immutable L2_CHAIN_ID; + /// @notice The L1 NODL token instance. L1Nodl public immutable L1_NODL; @@ -51,6 +66,8 @@ contract L1Bridge is Ownable2Step, Pausable, IL1Bridge { /// @dev Zero address supplied where non-zero is required. error ZeroAddress(); + /// @dev Zero chain id supplied where non-zero is required. + error ZeroChainId(); /// @dev Amount must be greater than zero. error ZeroAmount(); /// @dev Unknown deposit tx hash for the provided sender. @@ -71,17 +88,32 @@ contract L1Bridge is Ownable2Step, Pausable, IL1Bridge { // ============================= /** - * @notice Initializes the bridge with the system Mailbox, token, and L2 bridge addresses. + * @notice Initializes the bridge with the system Mailbox, Bridgehub, token, and L2 bridge addresses. * @param _owner The admin address for Ownable controls. - * @param _l1Mailbox The L1 Mailbox (zkSync Era) proxy address. + * @param _l1Mailbox The L1 Mailbox (zkSync Era Diamond) proxy address, used for L2→L1 proofs. + * @param _bridgehub The L1 Bridgehub address, used for deposits and base-cost quotes. + * @param _l2ChainId The chain id of the target L2 as registered on the Bridgehub. * @param _l1Token The L1 NODL token address. * @param _l2Bridge The L2 bridge contract address. */ - constructor(address _owner, address _l1Mailbox, address _l1Token, address _l2Bridge) Ownable(_owner) { - if (_l1Mailbox == address(0) || _l1Token == address(0) || _l2Bridge == address(0)) { + constructor( + address _owner, + address _l1Mailbox, + address _bridgehub, + uint256 _l2ChainId, + address _l1Token, + address _l2Bridge + ) Ownable(_owner) { + if (_l1Mailbox == address(0) || _bridgehub == address(0) || _l1Token == address(0) || _l2Bridge == address(0)) + { revert ZeroAddress(); } + if (_l2ChainId == 0) { + revert ZeroChainId(); + } L1_MAILBOX = IMailbox(_l1Mailbox); + BRIDGEHUB = IBridgehub(_bridgehub); + L2_CHAIN_ID = _l2ChainId; L1_NODL = L1Nodl(_l1Token); L2_BRIDGE_ADDR = _l2Bridge; } @@ -117,7 +149,7 @@ contract L1Bridge is Ownable2Step, Pausable, IL1Bridge { view returns (uint256 baseCost) { - baseCost = L1_MAILBOX.l2TransactionBaseCost(tx.gasprice, _l2TxGasLimit, _l2TxGasPerPubdataByte); + baseCost = BRIDGEHUB.l2TransactionBaseCost(L2_CHAIN_ID, tx.gasprice, _l2TxGasLimit, _l2TxGasPerPubdataByte); } /** @@ -132,7 +164,7 @@ contract L1Bridge is Ownable2Step, Pausable, IL1Bridge { view returns (uint256 baseCost) { - baseCost = L1_MAILBOX.l2TransactionBaseCost(_l1GasPrice, _l2TxGasLimit, _l2TxGasPerPubdataByte); + baseCost = BRIDGEHUB.l2TransactionBaseCost(L2_CHAIN_ID, _l1GasPrice, _l2TxGasLimit, _l2TxGasPerPubdataByte); } // ============================= @@ -141,13 +173,16 @@ contract L1Bridge is Ownable2Step, Pausable, IL1Bridge { /** * @notice Initiates a deposit by burning on L1 and enqueuing an L2 finalizeDeposit call. - * @dev Caller must approve/burnable rights on the NODL token and provide msg.value to cover Mailbox costs. + * @dev Caller must approve/burnable rights on the NODL token and provide msg.value to cover the L2 + * transaction base cost. The full msg.value is passed to the Bridgehub as the request's mintValue + * (the Bridgehub requires them to be equal for ETH-based chains); any excess over the actual cost + * is refunded on L2 to the refund recipient. * @param _l2Receiver The L2 address to receive the bridged tokens. * @param _amount The amount of tokens to bridge. * @param _l2TxGasLimit Gas limit for the L2 call. * @param _l2TxGasPerPubdataByte Gas per pubdata byte for the L2 call. - * @param _refundRecipient Address receiving any ETH refund from the Mailbox. - * @return txHash The L2 transaction hash of the enqueued call. + * @param _refundRecipient Address receiving any ETH refund on L2. + * @return txHash The canonical L2 transaction hash of the enqueued call. */ function deposit( address _l2Receiver, @@ -168,8 +203,18 @@ contract L1Bridge is Ownable2Step, Pausable, IL1Bridge { bytes memory l2Calldata = abi.encodeCall(IL2Bridge.finalizeDeposit, (msg.sender, _l2Receiver, _amount)); address refundRecipient = _refundRecipient != address(0) ? _refundRecipient : msg.sender; - txHash = L1_MAILBOX.requestL2Transaction{value: msg.value}( - L2_BRIDGE_ADDR, 0, l2Calldata, _l2TxGasLimit, _l2TxGasPerPubdataByte, new bytes[](0), refundRecipient + txHash = BRIDGEHUB.requestL2TransactionDirect{value: msg.value}( + L2TransactionRequestDirect({ + chainId: L2_CHAIN_ID, + mintValue: msg.value, + l2Contract: L2_BRIDGE_ADDR, + l2Value: 0, + l2Calldata: l2Calldata, + l2GasLimit: _l2TxGasLimit, + l2GasPerPubdataByteLimit: _l2TxGasPerPubdataByte, + factoryDeps: new bytes[](0), + refundRecipient: refundRecipient + }) ); depositAmount[msg.sender][txHash] = _amount; diff --git a/src/bridge/interfaces/IL1Bridge.sol b/src/bridge/interfaces/IL1Bridge.sol index 709f9963..2ad1db0e 100644 --- a/src/bridge/interfaces/IL1Bridge.sol +++ b/src/bridge/interfaces/IL1Bridge.sol @@ -9,8 +9,8 @@ pragma solidity ^0.8.26; */ interface IL1Bridge { /** - * @notice Emitted when a deposit to L2 is initiated via the zkSync Mailbox. - * @param l2DepositTxHash The L2 transaction hash returned by the Mailbox for the enqueued L2 call. + * @notice Emitted when a deposit to L2 is initiated via the zkSync Bridgehub. + * @param l2DepositTxHash The canonical L2 transaction hash returned by the Bridgehub for the enqueued L2 call. * @param from The L1 sender who initiated the deposit. * @param to The L2 receiver that will receive/mint tokens on L2. * @param amount The token amount bridged. @@ -49,14 +49,14 @@ interface IL1Bridge { /** * @notice Initiates a token deposit to L2 by enqueuing a call to the L2 bridge. - * @dev The caller must send sufficient ETH in msg.value to cover the Mailbox base cost. - * Any excess will be refunded to `_refundRecipient`. + * @dev The caller must send sufficient ETH in msg.value to cover the L2 transaction base cost + * (see {quoteL2BaseCost} on the implementation). Any excess is refunded on L2 to `_refundRecipient`. * @param _l2Receiver The L2 address that will receive the bridged tokens. * @param _amount The token amount to bridge. * @param _l2TxGasLimit The L2 gas limit for the enqueued transaction. * @param _l2TxGasPerPubdataByte The gas per pubdata byte parameter for the L2 tx. - * @param _refundRecipient The L1 address to receive any ETH refund from the Mailbox. - * @return txHash The L2 transaction hash returned by the Mailbox. + * @param _refundRecipient The address to receive any ETH refund on L2. + * @return txHash The canonical L2 transaction hash returned by the Bridgehub. */ function deposit( address _l2Receiver, diff --git a/test/bridge/L1Bridge.t.sol b/test/bridge/L1Bridge.t.sol index 997706da..df7ff8fd 100644 --- a/test/bridge/L1Bridge.t.sol +++ b/test/bridge/L1Bridge.t.sol @@ -10,23 +10,17 @@ import {IL2Bridge} from "src/bridge/interfaces/IL2Bridge.sol"; import {IWithdrawalMessage} from "src/bridge/interfaces/IWithdrawalMessage.sol"; import {L1Nodl} from "src/L1Nodl.sol"; import {IMailbox} from "lib/era-contracts/l1-contracts/contracts/state-transition/chain-interfaces/IMailbox.sol"; +import {L2TransactionRequestDirect} from "lib/era-contracts/l1-contracts/contracts/bridgehub/IBridgehub.sol"; import {L2Message, TxStatus} from "lib/era-contracts/l1-contracts/contracts/common/Messaging.sol"; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; -/// @dev Minimal mock for zkSync Era Mailbox to drive L1Bridge tests. +/// @dev Minimal mock for zkSync Era Mailbox (Diamond proxy) to drive the L2->L1 proof paths. contract MockMailbox { /* not inheriting IMailbox on purpose */ mapping(bytes32 => bool) public l1ToL2Failed; // txHash => failed? mapping(uint256 => mapping(uint256 => bool)) public l2InclusionOk; // batch=>index => ok? - bytes32 public lastRequestedTxHash; - address public lastRefundRecipient; - uint256 public baseCostReturn; - uint256 public expectedBaseCostGasPrice; - uint256 public expectedBaseCostGasLimit; - uint256 public expectedBaseCostGasPerPubdata; - // Allow tests to toggle outcomes function setL1ToL2Failed(bytes32 txHash, bool failed) external { l1ToL2Failed[txHash] = failed; @@ -36,33 +30,6 @@ contract MockMailbox { /* not inheriting IMailbox on purpose */ l2InclusionOk[batch][index] = ok; } - function setBaseCostReturn(uint256 value) external { - baseCostReturn = value; - } - - function expectBaseCostParams(uint256 gasPrice, uint256 gasLimit, uint256 gasPerPubdata) external { - expectedBaseCostGasPrice = gasPrice; - expectedBaseCostGasLimit = gasLimit; - expectedBaseCostGasPerPubdata = gasPerPubdata; - } - - // --- Methods used by L1Bridge --- - function requestL2Transaction( - address _contractL2, - uint256 _l2Value, - bytes calldata _calldata, - uint256 _l2GasLimit, - uint256 _l2GasPerPubdataByte, - bytes[] calldata, /*_factoryDeps*/ - address _refundRecipient - ) external payable returns (bytes32) { - lastRefundRecipient = _refundRecipient; - lastRequestedTxHash = keccak256( - abi.encode(_contractL2, _l2Value, _calldata, _l2GasLimit, _l2GasPerPubdataByte, msg.value, _refundRecipient) - ); - return lastRequestedTxHash; - } - function proveL1ToL2TransactionStatus( bytes32 _l2TxHash, uint256, /*_l2BatchNumber*/ @@ -86,13 +53,78 @@ contract MockMailbox { /* not inheriting IMailbox on purpose */ return l2InclusionOk[_batchNumber][_index]; } - function l2TransactionBaseCost(uint256 _l1GasPrice, uint256 _l2GasLimit, uint256 _l2GasPerPubdataByte) +} + +/// @dev Minimal mock for the zkSync Bridgehub to drive deposits and base-cost quotes. +contract MockBridgehub { /* not inheriting IBridgehub on purpose */ + bytes32 public lastRequestedTxHash; + uint256 public lastChainId; + uint256 public lastMintValue; + address public lastL2Contract; + uint256 public lastL2Value; + uint256 public lastL2GasLimit; + uint256 public lastL2GasPerPubdata; + address public lastRefundRecipient; + uint256 public lastMsgValue; + + uint256 public baseCostReturn; + uint256 public expectedBaseCostChainId; + uint256 public expectedBaseCostGasPrice; + uint256 public expectedBaseCostGasLimit; + uint256 public expectedBaseCostGasPerPubdata; + + function setBaseCostReturn(uint256 value) external { + baseCostReturn = value; + } + + function expectBaseCostParams(uint256 chainId, uint256 gasPrice, uint256 gasLimit, uint256 gasPerPubdata) + external + { + expectedBaseCostChainId = chainId; + expectedBaseCostGasPrice = gasPrice; + expectedBaseCostGasLimit = gasLimit; + expectedBaseCostGasPerPubdata = gasPerPubdata; + } + + // --- Methods used by L1Bridge --- + function requestL2TransactionDirect(L2TransactionRequestDirect calldata _request) + external + payable + returns (bytes32) + { + // Mirrors the real Bridgehub check for ETH-based chains. + require(msg.value == _request.mintValue, "msg.value != mintValue"); + lastChainId = _request.chainId; + lastMintValue = _request.mintValue; + lastL2Contract = _request.l2Contract; + lastL2Value = _request.l2Value; + lastL2GasLimit = _request.l2GasLimit; + lastL2GasPerPubdata = _request.l2GasPerPubdataByteLimit; + lastRefundRecipient = _request.refundRecipient; + lastMsgValue = msg.value; + lastRequestedTxHash = keccak256( + abi.encode( + _request.chainId, + _request.l2Contract, + _request.l2Value, + _request.l2Calldata, + _request.l2GasLimit, + _request.l2GasPerPubdataByteLimit, + msg.value, + _request.refundRecipient + ) + ); + return lastRequestedTxHash; + } + + function l2TransactionBaseCost(uint256 _chainId, uint256 _gasPrice, uint256 _l2GasLimit, uint256 _l2GasPerPubdataByte) external view returns (uint256) { + require(_chainId == expectedBaseCostChainId, "unexpected chain id"); // The gas price of zero is allowed as `forge test --zksync` sets it to zero - require(_l1GasPrice == expectedBaseCostGasPrice || _l1GasPrice == 0, "unexpected gas price"); + require(_gasPrice == expectedBaseCostGasPrice || _gasPrice == 0, "unexpected gas price"); require(_l2GasLimit == expectedBaseCostGasLimit, "unexpected gas limit"); require(_l2GasPerPubdataByte == expectedBaseCostGasPerPubdata, "unexpected gas per pubdata"); return baseCostReturn; @@ -107,16 +139,19 @@ contract L1BridgeTest is Test { // Deployed contracts MockMailbox internal mailbox; + MockBridgehub internal bridgehub; L1Nodl internal token; L1Bridge internal bridge; // Config address internal constant L2_BRIDGE_ADDR = address(0x1234); + uint256 internal constant L2_CHAIN_ID = 271; function setUp() public { mailbox = new MockMailbox(); + bridgehub = new MockBridgehub(); token = new L1Nodl(ADMIN, ADMIN); - bridge = new L1Bridge(ADMIN, address(mailbox), address(token), L2_BRIDGE_ADDR); + bridge = new L1Bridge(ADMIN, address(mailbox), address(bridgehub), L2_CHAIN_ID, address(token), L2_BRIDGE_ADDR); vm.startPrank(ADMIN); bytes32 minterRole = keccak256("MINTER_ROLE"); @@ -143,7 +178,7 @@ contract L1BridgeTest is Test { assertEq(bridge.depositAmount(USER, txHash), amount, "deposit amount recorded"); assertEq(token.balanceOf(USER), 1_000_000 ether - amount, "user burned amount"); - assertEq(mailbox.lastRefundRecipient(), refundRecipient, "refund recipient passed to mailbox"); + assertEq(bridgehub.lastRefundRecipient(), refundRecipient, "refund recipient passed to bridgehub"); } function test_Deposit_Overload_DefaultRefundRecipient() public { @@ -163,7 +198,7 @@ contract L1BridgeTest is Test { assertEq(bridge.depositAmount(USER, txHash), amount, "deposit amount recorded"); assertEq(token.balanceOf(USER), 1_000_000 ether - amount, "user burned amount"); - assertEq(mailbox.lastRefundRecipient(), USER, "refund recipient is user"); + assertEq(bridgehub.lastRefundRecipient(), USER, "refund recipient is user"); } function test_Deposit_RefundRecipientZero_DefaultsToUser() public { @@ -178,7 +213,7 @@ contract L1BridgeTest is Test { vm.stopPrank(); assertEq(bridge.depositAmount(USER, txHash), amount, "deposit amount recorded"); - assertEq(mailbox.lastRefundRecipient(), USER, "refund recipient defaults to sender"); + assertEq(bridgehub.lastRefundRecipient(), USER, "refund recipient defaults to sender"); } function test_Deposit_Revert_ZeroAmount() public { @@ -357,12 +392,12 @@ contract L1BridgeTest is Test { uint256 gasPerPubdata = 800; uint256 quotedValue = 123; vm.txGasPrice(42 gwei); - mailbox.setBaseCostReturn(quotedValue); - mailbox.expectBaseCostParams(tx.gasprice, gasLimit, gasPerPubdata); + bridgehub.setBaseCostReturn(quotedValue); + bridgehub.expectBaseCostParams(L2_CHAIN_ID, tx.gasprice, gasLimit, gasPerPubdata); uint256 quote = bridge.quoteL2BaseCost(gasLimit, gasPerPubdata); - assertEq(quote, quotedValue, "returns quoted base cost from mailbox"); + assertEq(quote, quotedValue, "returns quoted base cost from bridgehub"); } function test_QuoteL2BaseCostAtGasPrice() public { @@ -370,11 +405,11 @@ contract L1BridgeTest is Test { uint256 gasPerPubdata = 900; uint256 gasPrice = 15 gwei; uint256 quotedValue = 456; - mailbox.setBaseCostReturn(quotedValue); - mailbox.expectBaseCostParams(gasPrice, gasLimit, gasPerPubdata); + bridgehub.setBaseCostReturn(quotedValue); + bridgehub.expectBaseCostParams(L2_CHAIN_ID, gasPrice, gasLimit, gasPerPubdata); uint256 quote = bridge.quoteL2BaseCostAtGasPrice(gasPrice, gasLimit, gasPerPubdata); - assertEq(quote, quotedValue, "returns mailbox quote"); + assertEq(quote, quotedValue, "returns bridgehub quote"); } function test_Pause_Gates_Functions() public { @@ -428,6 +463,40 @@ contract L1BridgeTest is Test { function test_Constructor_Revert_ZeroAddress() public { vm.expectRevert(abi.encodeWithSelector(L1Bridge.ZeroAddress.selector)); - new L1Bridge(ADMIN, address(0), address(token), L2_BRIDGE_ADDR); + new L1Bridge(ADMIN, address(0), address(bridgehub), L2_CHAIN_ID, address(token), L2_BRIDGE_ADDR); + } + + function test_Constructor_Revert_ZeroBridgehub() public { + vm.expectRevert(abi.encodeWithSelector(L1Bridge.ZeroAddress.selector)); + new L1Bridge(ADMIN, address(mailbox), address(0), L2_CHAIN_ID, address(token), L2_BRIDGE_ADDR); + } + + function test_Constructor_Revert_ZeroChainId() public { + vm.expectRevert(abi.encodeWithSelector(L1Bridge.ZeroChainId.selector)); + new L1Bridge(ADMIN, address(mailbox), address(bridgehub), 0, address(token), L2_BRIDGE_ADDR); + } + + function test_Deposit_PassesBridgehubRequestFields() public { + uint256 amount = 42 ether; + address l2Receiver = address(0x7777); + uint256 gasLimit = 750_000; + uint256 gasPerPubdata = 800; + address refundRecipient = address(0x9999); + uint256 fee = 0.01 ether; + + vm.deal(USER, fee); + vm.startPrank(USER); + token.approve(address(bridge), amount); + bytes32 txHash = bridge.deposit{value: fee}(l2Receiver, amount, gasLimit, gasPerPubdata, refundRecipient); + vm.stopPrank(); + + assertEq(bridgehub.lastChainId(), L2_CHAIN_ID, "chain id passed to bridgehub"); + assertEq(bridgehub.lastMintValue(), fee, "mintValue equals msg.value"); + assertEq(bridgehub.lastMsgValue(), fee, "msg.value forwarded to bridgehub"); + assertEq(bridgehub.lastL2Contract(), L2_BRIDGE_ADDR, "target is the L2 bridge"); + assertEq(bridgehub.lastL2Value(), 0, "no L2 value"); + assertEq(bridgehub.lastL2GasLimit(), gasLimit, "gas limit forwarded"); + assertEq(bridgehub.lastL2GasPerPubdata(), gasPerPubdata, "gas per pubdata forwarded"); + assertEq(txHash, bridgehub.lastRequestedTxHash(), "returns bridgehub canonical tx hash"); } } From 6ba4005e2c2bc8784b456489bce27f001e3d28ed Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 2 Jul 2026 08:50:31 -0500 Subject: [PATCH 2/2] ci: skip coverage PR comment for fork PRs 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 --- .github/workflows/checks.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 620436fe..28eecec2 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -66,6 +66,9 @@ jobs: run: apt-get update && apt-get install -y lcov - name: Report coverage to PR + # GITHUB_TOKEN is read-only on pull_request runs from forks, so the + # sticky comment can never post there; skip it and keep the job green. + if: github.event.pull_request.head.repo.full_name == github.repository uses: zgosalvez/github-actions-report-lcov@v4 with: coverage-files: coverage.lcov