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/.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 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"); } }