HIGH severity: Failed Messenger Can Render the Canonical Methods Useless. The Arbitrum_Adapter and Arbitrum_SpokePool contracts use the `_transferViaOFT` function to bridge tokens via LayerZero's OFT standard. The logic enters this path if the associated messenger address is non-zero, without fallback mechanisms in case the messenger fails. If the messenger reverts during...

Vulnerable code:
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import "./interfaces/AdapterInterface.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IOFT } from "../interfaces/IOFT.sol";
import "../external/interfaces/CCTPInterfaces.sol";
import "../libraries/CircleCCTPAdapter.sol";
import "../libraries/OFTTransportAdapterWithStore.sol";
import { ArbitrumInboxLike as ArbitrumL1InboxLike, ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridge.sol";

/**
 * @notice Contract containing logic to send messages from L1 to Arbitrum.
 * @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be
 * called via delegatecall, which will execute this contract's logic within the context of the originating contract.
 * For example, the HubPool will delegatecall these functions, therefore its only necessary that the HubPool's methods
 * that call this contract's logic guard against reentrancy.
 */

// solhint-disable-next-line contract-name-camelcase
contract Arbitrum_Adapter is AdapterInterface, CircleCCTPAdapter, OFTTransportAdapterWithStore {
    using SafeERC20 for IERC20;

    // Amount of ETH allocated to pay for the base submission fee. The base submission fee is a parameter unique to
    // retryable transactions; the user is charged the base submission fee to cover the storage costs of keeping their
    // ticket’s calldata in the retry buffer. (current base submission fee is queryable via
    // ArbRetryableTx.getSubmissionPrice). ArbRetryableTicket precompile interface exists at L2 address
    // 0x000000000000000000000000000000000000006E.
    uint256 public constant L2_MAX_SUBMISSION_COST = 0.01e18;

    // L2 Gas price bid for immediate L2 execution attempt (queryable via standard eth*gasPrice RPC)
    uint256 public constant L2_GAS_PRICE = 5e9; // 5 gWei

    // Native token expected to be sent in L2 message. Should be 0 for only use case of this constant, which
    // includes is sending messages from L1 to L2.
    uint256 public constant L2_CALL_VALUE = 0;

    // Gas limit for L2 execution of a cross chain token transfer sent via the inbox.
    uint32 public constant RELAY_TOKENS_L2_GAS_LIMIT = 300_000;
    // Gas limit for L2 execution of a message sent via the inbox.
    uint32 public constant RELAY_MESSAGE_L2_GAS_LIMIT = 2_000_000;

    address public constant L1_DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;

    // This address on L2 receives extra ETH that is left over after relaying a message via the inbox.
    address public immutable L2_REFUND_L2_ADDRESS;

    // Inbox system contract to send messages to Arbitrum. Token bridges use this to send tokens to L2.
    // https://github.com/OffchainLabs/nitro-contracts/blob/f7894d3a6d4035ba60f51a7f1334f0f2d4f02dce/src/bridge/Inbox.sol
    ArbitrumL1InboxLike public immutable L1_INBOX;

    // Router contract to send tokens to Arbitrum. Routes to correct gateway to bridge tokens. Internally this
    // contract calls the Inbox.
    // Generic gateway: https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol
    ArbitrumL1ERC20GatewayLike public immutable L1_ERC20_GATEWAY_ROUTER;

    /**
     * @notice Constructs new Adapter.
     * @param _l1ArbitrumInbox Inbox helper contract to send messages to Arbitrum.
     * @param _l1ERC20GatewayRouter ERC20 gateway router contract to send tokens to Arbitrum.
     * @param _l2RefundL2Address L2 address to receive gas refunds on after a message is relayed.
     * @param _l1Usdc USDC address on L1.
     * @param _cctpTokenMessenger TokenMessenger contract to bridge via CCTP.
     * @param _adapterStore Helper storage contract to support bridging via OFT
     * @param _oftDstEid destination endpoint id for OFT messaging
     * @param _oftFeeCap A fee cap we apply to OFT bridge native payment. A good default is 1 ether
     */
    constructor(
        ArbitrumL1InboxLike _l1ArbitrumInbox,
        ArbitrumL1ERC20GatewayLike _l1ERC20GatewayRouter,
        address _l2RefundL2Address,
        IERC20 _l1Usdc,
        ITokenMessenger _cctpTokenMessenger,
        address _adapterStore,
        uint32 _oftDstEid,
        uint256 _oftFeeCap
    )
        CircleCCTPAdapter(_l1Usdc, _cctpTokenMessenger, CircleDomainIds.Arbitrum)
        OFTTransportAdapterWithStore(_oftDstEid, _oftFeeCap, _adapterStore)
    {
        L1_INBOX = _l1ArbitrumInbox;
        L1_ERC20_GATEWAY_ROUTER = _l1ERC20GatewayRouter;
        L2_REFUND_L2_ADDRESS = _l2RefundL2Address;
    }

    /**
     * @notice Send cross-chain message to target on Arbitrum.
     * @notice This contract must hold at least getL1CallValue() amount of ETH to send a message via the Inbox
     * successfully, or the message will get stuck.
     * @param target Contract on Arbitrum that will receive message.
     * @param message Data to send to target.
     */
    function relayMessage(address target, bytes memory message) external payable override {
        uint256 requiredL1CallValue = _contractHasSufficientEthBalance(RELAY_MESSAGE_L2_GAS_LIMIT);

        L1_INBOX.createRetryableTicket{ value: requiredL1CallValue }(
            target, // destAddr destination L2 contract address
            L2_CALL_VALUE, // l2CallValue call value for retryable L2 message
            L2_MAX_SUBMISSION_COST, // maxSubmissionCost Max gas deducted from user's L2 balance to cover base fee
            L2_REFUND_L2_ADDRESS, // excessFeeRefundAddress maxgas * gasprice - execution cost gets credited here on L2
            L2_REFUND_L2_ADDRESS, // callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled
            RELAY_MESSAGE_L2_GAS_LIMIT, // maxGas Max gas deducted from user's L2 balance to cover L2 execution
            L2_GAS_PRICE, // gasPriceBid price bid for L2 execution
            message // data ABI encoded data of L2 message
        );

        emit MessageRelayed(target, message);
    }

    /**
     * @notice Bridge tokens to Arbitrum.
     * @notice This contract must hold at least getL1CallValue() amount of ETH to send a message via the Inbox
     * successfully, or the message will get stuck.
     * @param l1Token L1 token to deposit.
     * @param l2Token L2 token to receive.
     * @param amount Amount of L1 tokens to deposit and L2 tokens to receive.
     * @param to Bridge recipient.
     */
    function relayTokens(
        address l1Token,
        address l2Token, // l2Token is unused for Arbitrum.
        uint256 amount,
        address to
    ) external payable override {
        address oftMessenger = _getOftMessenger(l1Token);

        // Check if the token needs to use any of the custom bridge solutions first
        if (_isCCTPEnabled() && l1Token == address(usdcToken)) {
            _transferUsdc(to, amount);
        } else if (oftMessenger != address(0)) {
            _transferViaOFT(IERC20(l1Token), IOFT(oftMessenger), to, amount);
        }
        // If not, we can use the Arbitrum gateway
        else {
            uint256 requiredL1CallValue = _contractHasSufficientEthBalance(RELAY_TOKENS_L2_GAS_LIMIT);

            // Approve the gateway, not the router, to spend the hub pool's balance. The gateway, which is different
            // per L1 token, will temporarily escrow the tokens to be bridged and pull them from this contract.
            address erc20Gateway = L1_ERC20_GATEWAY_ROUTER.getGateway(l1Token);
            IERC20(l1Token).safeIncreaseAllowance(erc20Gateway, amount);

            // `outboundTransfer` expects that the caller includes a bytes message as the last param that includes the
            // maxSubmissionCost to use when creating an L2 retryable ticket: https://github.com/OffchainLabs/arbitrum/blob/e98d14873dd77513b569771f47b5e05b72402c5e/packages/arb-bridge-peripherals/contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol#L232
            bytes memory data = abi.encode(L2_MAX_SUBMISSION_COST, "");

            // Note: Legacy routers don't have the outboundTransferCustomRefund method, so default to using
            // outboundTransfer(). Legacy routers are used for the following tokens that are currently enabled:
            // - DAI: the implementation of `outboundTransfer` at the current DAI custom gateway
            //        (https://etherscan.io/address/0xD3B5b60020504bc3489D6949d545893982BA3011#writeContract) sets the
            //        sender as the refund address so the aliased HubPool should receive excess funds. Implementation here:
            //        https://github.com/makerdao/arbitrum-dai-bridge/blob/11a80385e2622968069c34d401b3d54a59060e87/contracts/l1/L1DaiGateway.sol#L109
            if (l1Token == L1_DAI) {
                // This means that the excess ETH to pay for the L2 transaction will be sent to the aliased
                // contract address on L2, which we'd have to retrieve via a custom adapter, the Arbitrum_RescueAdapter.
                // To do so, in a single transaction: 1) setCrossChainContracts to Arbitrum_RescueAdapter, 2) relayMessage
                // with function data = abi.encode(amountToRescue), 3) setCrossChainContracts back to this adapter.
                L1_ERC20_GATEWAY_ROUTER.outboundTransfer{ value: requiredL1CallValue }(
                    l1Token,
                    to,
                    amount,
                    RELAY_TOKENS_L2_GAS_LIMIT,
                    L2_GAS_PRICE,
                    data
                );
            } else {
                L1_ERC20_GATEWAY_ROUTER.outboundTransferCustomRefund{ value: requiredL1CallValue }(
                    l1Token,
                    L2_REFUND_L2_ADDRESS,
                    to,
                    amount,
                    RELAY_TOKENS_L2_GAS_LIMIT,
                    L2_GAS_PRICE,
                    data
                );
            }
        }
        emit TokensRelayed(l1Token, l2Token, amount, to);
    }

    /**
     * @notice Returns required amount of ETH to send a message via the Inbox.
     * @param l2GasLimit L2 gas limit for the message.
     * @return amount of ETH that this contract needs to hold in order for relayMessage to succeed.
     */
    function getL1CallValue(uint32 l2GasLimit) public pure returns (uint256) {
        return L2_MAX_SUBMISSION_COST + L2_GAS_PRICE * l2GasLimit;
    }

    function _contractHasSufficientEthBalance(uint32 l2GasLimit) internal view returns (uint256) {
        uint256 requiredL1CallValue = getL1CallValue(l2GasLimit);
        require(address(this).balance >= requiredL1CallValue, "Insufficient ETH balance");
        return requiredL1CallValue;
    }
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import "./MerkleLib.sol";
import "./erc7683/ERC7683.sol";
import "./erc7683/ERC7683Permit2Lib.sol";
import "./external/interfaces/WETH9Interface.sol";
import "./interfaces/SpokePoolMessageHandler.sol";
import "./interfaces/SpokePoolInterface.sol";
import "./interfaces/V3SpokePoolInterface.sol";
import "./upgradeable/MultiCallerUpgradeable.sol";
import "./upgradeable/EIP712CrossChainUpgradeable.sol";
import "./upgradeable/AddressLibUpgradeable.sol";
import "./libraries/AddressConverters.sol";
import "./libraries/OFTTransportAdapter.sol";

import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/utils/math/SignedMath.sol";

/**
 * @title SpokePool
 * @notice Base contract deployed on source and destination chains enabling depositors to transfer assets from source to
 * destination. Deposit orders are fulfilled by off-chain relayers who also interact with this contract. Deposited
 * tokens are locked on the source chain and relayers send the recipient the desired token currency and amount
 * on the destination chain. Locked source chain tokens are later sent over the canonical token bridge to L1 HubPool.
 * Relayers are refunded with destination tokens out of this contract after another off-chain actor, a "data worker",
 * submits a proof that the relayer correctly submitted a relay on this SpokePool.
 * @custom:security-contact bugs@across.to
 */
abstract contract SpokePool is
    V3SpokePoolInterface,
    SpokePoolInterface,
    UUPSUpgradeable,
    ReentrancyGuardUpgradeable,
    MultiCallerUpgradeable,
    EIP712CrossChainUpgradeable,
    IDestinationSettler,
    OFTTransportAdapter
{
    using SafeERC20Upgradeable for IERC20Upgradeable;
    using AddressLibUpgradeable for address;
    using Bytes32ToAddress for bytes32;
    using AddressToBytes32 for address;

    // Address of the L1 contract that acts as the owner of this SpokePool. This should normally be set to the HubPool
    // address. The crossDomainAdmin address is unused when the SpokePool is deployed to the same chain as the HubPool.
    address public crossDomainAdmin;

    // Address of the L1 contract that will send tokens to and receive tokens from this contract to fund relayer
    // refunds and slow relays.
    address public withdrawalRecipient;

    // Note: The following two storage variables prefixed with DEPRECATED used to be variables that could be set by
    // the cross-domain admin. Admins ended up not changing these in production, so to reduce
    // gas in deposit/fill functions, we are converting them to private variables to maintain the contract
    // storage layout and replacing them with immutable or constant variables, because retrieving a constant
    // value is cheaper than retrieving a storage variable. Please see out the immutable/constant variable section.
    WETH9Interface private DEPRECATED_wrappedNativeToken;
    uint32 private DEPRECATED_depositQuoteTimeBuffer;

    // `numberOfDeposits` acts as a counter to generate unique deposit identifiers for this spoke pool.
    // It is a uint32 that increments with each `depositV3` call. In the `FundsDeposited` event, it is
    // implicitly cast to uint256 by setting its most significant bits to 0, reducing the risk of ID collisions
    // with unsafe deposits. However, this variable's name could be improved (e.g., `depositNonceCounter`)
    // since it does not accurately reflect the total number of deposits, as `unsafeDeposit` can bypass this increment.
    uint32 public numberOfDeposits;

    // Whether deposits and fills are disabled.
    bool public pausedFills;
    bool public pausedDeposits;

    // This contract can store as many root bundles as the HubPool chooses to publish here.
    RootBundle[] public rootBundles;

    // Origin token to destination token routings can be turned on or off, which can enable or disable deposits.
    mapping(address => mapping(uint256 => bool)) private DEPRECATED_enabledDepositRoutes;

    // Each relay is associated with the hash of parameters that uniquely identify the original deposit and a relay
    // attempt for that deposit. The relay itself is just represented as the amount filled so far. The total amount to
    // relay, the fees, and the agents are all parameters included in the hash key.
    mapping(bytes32 => uint256) private DEPRECATED_relayFills;

    // Note: We will likely un-deprecate the fill and deposit counters to implement a better
    // dynamic LP fee mechanism but for now we'll deprecate it to reduce bytecode
    // in deposit/fill functions. These counters are designed to implement a fee mechanism that is based on a
    // canonical history of deposit and fill events and how they update a virtual running balance of liabilities and
    // assets, which then determines the LP fee charged to relays.

    // This keeps track of the worst-case liabilities due to fills.
    // It is never reset. Users should only rely on it to determine the worst-case increase in liabilities between
    // two points. This is used to provide frontrunning protection to ensure the relayer's assumptions about the state
    // upon which their expected repayments are based will not change before their transaction is mined.
    mapping(address => uint256) private DEPRECATED_fillCounter;

    // This keeps track of the total running deposits for each token. This allows depositors to protect themselves from
    // frontrunning that might change their worst-case quote.
    mapping(address => uint256) private DEPRECATED_depositCounter;

    // This tracks the number of identical refunds that have been requested.
    // The intention is to allow an off-chain system to know when this could be a duplicate and ensure that the other
    // requests are known and accounted for.
    mapping(bytes32 => uint256) private DEPRECATED_refundsRequested;

    // Mapping of V3 relay hashes to fill statuses. Distinguished from relayFills
    // to eliminate any chance of collision between pre and post V3 relay hashes.
    mapping(bytes32 => uint256) public fillStatuses;

    // Mapping of L2TokenAddress to relayer to outstanding refund amount. Used when a relayer repayment fails for some
    // reason (eg blacklist) to track their outstanding liability, thereby letting them claim it later.
    mapping(address => mapping(address => uint256)) public relayerRefund;

    // Mapping of L2 token address to L2 IOFT messenger address. Required to support bridging via OFT standard
    mapping(address => address) public oftMessengers;

    /**************************************************************
     *                CONSTANT/IMMUTABLE VARIABLES                *
     **************************************************************/
    // Constant and immutable variables do not take up storage slots and are instead added to the contract bytecode
    // at compile time. The difference between them is that constant variables must be declared inline, meaning
    // that they cannot be changed in production without changing the contract code, while immutable variables
    // can be set in the constructor. Therefore we use the immutable keyword for variables that we might want to be
    // different for each child contract (one obvious example of this is the wrappedNativeToken) or that we might
    // want to update in the future like depositQuoteTimeBuffer. Constants are unlikely to ever be changed.

    // Address of wrappedNativeToken contract for this network. If an origin token matches this, then the caller can
    // optionally instruct this contract to wrap native tokens when depositing (ie ETH->WETH or MATIC->WMATIC).
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
    WETH9Interface public immutable wrappedNativeToken;

    // Any deposit quote times greater than or less than this value to the current contract time is blocked. Forces
    // caller to use an approximately "current" realized fee.
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
    uint32 public immutable depositQuoteTimeBuffer;

    // The fill deadline can only be set this far into the future from the timestamp of the deposit on this contract.
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
    uint32 public immutable fillDeadlineBuffer;

    uint256 public constant MAX_TRANSFER_SIZE = 1e36;

    bytes32 public constant UPDATE_BYTES32_DEPOSIT_DETAILS_HASH =
        keccak256(
            "UpdateDepositDetails(uint256 depositId,uint256 originChainId,uint256 updatedOutputAmount,bytes32 updatedRecipient,bytes updatedMessage)"
        );
    // Default chain Id used to signify that no repayment is requested, for example when executing a slow fill.
    uint256 public constant EMPTY_REPAYMENT_CHAIN_ID = 0;
    // Default address used to signify that no relayer should be credited with a refund, for example
    // when executing a slow fill.
    bytes32 public constant EMPTY_RELAYER = bytes32(0);
    // This is the magic value that signals to the off-chain validator
    // that this deposit can never expire. A deposit with this fill deadline should always be eligible for a
    // slow fill, meaning that its output token and input token must be "equivalent". Therefore, this value is only
    // used as a fillDeadline in deposit(), a soon to be deprecated function that also hardcodes outputToken to
    // the zero address, which forces the off-chain validator to replace the output token with the equivalent
    // token for the input token. By using this magic value, off-chain validators do not have to keep
    // this event in their lookback window when querying for expired deposits.
    uint32 public constant INFINITE_FILL_DEADLINE = type(uint32).max;

    // One year in seconds. If `exclusivityParameter` is set to a value less than this, then the emitted
    // exclusivityDeadline in a deposit event will be set to the current time plus this value.
    uint32 public constant MAX_EXCLUSIVITY_PERIOD_SECONDS = 31_536_000;
    /****************************************
     *                EVENTS                *
     ****************************************/
    event SetXDomainAdmin(address indexed newAdmin);
    event SetWithdrawalRecipient(address indexed newWithdrawalRecipient);
    event EnabledDepositRoute(address indexed originToken, uint256 indexed destinationChainId, bool enabled);
    event RelayedRootBundle(
        uint32 indexed rootBundleId,
        bytes32 indexed relayerRefundRoot,
        bytes32 indexed slowRelayRoot
    );
    event ExecutedRelayerRefundRoot(
        uint256 amountToReturn,
        uint256 indexed chainId,
        uint256[] refundAmounts,
        uint32 indexed rootBundleId,
        uint32 indexed leafId,
        address l2TokenAddress,
        address[] refundAddresses,
        bool deferredRefunds,
        address caller
    );
    event TokensBridged(
        uint256 amountToReturn,
        uint256 indexed chainId,
        uint32 indexed leafId,
        bytes32 indexed l2TokenAddress,
        address caller
    );
    event EmergencyDeletedRootBundle(uint256 indexed rootBundleId);
    event PausedDeposits(bool isPaused);
    event PausedFills(bool isPaused);
    event SetOFTMessenger(address indexed token, address indexed messenger);

    error OFTTokenMismatch();

    /**
     * @notice Construct the SpokePool. Normally, logic contracts used in upgradeable proxies shouldn't
     * have constructors since the following code will be executed within the logic contract's state, not the
     * proxy contract's state. However, if we restrict the constructor to setting only immutable variables, then
     * we are safe because immutable variables are included in the logic contract's bytecode rather than its storage.
     * @dev Do not leave an implementation contract uninitialized. An uninitialized implementation contract can be
     * taken over by an attacker, which may impact the proxy. To prevent the implementation contract from being
     * used, you should invoke the _disableInitializers function in the constructor to automatically lock it when
     * it is deployed:
     * @param _wrappedNativeTokenAddress wrappedNativeToken address for this network to set.
     * @param _depositQuoteTimeBuffer depositQuoteTimeBuffer to set. Quote timestamps can't be set more than this amount
     * into the past from the block time of the deposit.
     * @param _fillDeadlineBuffer fillDeadlineBuffer to set. Fill deadlines can't be set more than this amount
     * into the future from the block time of the deposit.
     * @param _oftDstEid destination endpoint id for OFT messaging
     * @param _oftFeeCap fee cap in native token when paying for cross-chain OFT transfers
     */
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor(
        address _wrappedNativeTokenAddress,
        uint32 _depositQuoteTimeBuffer,
        uint32 _fillDeadlineBuffer,
        uint32 _oftDstEid,
        uint256 _oftFeeCap
    ) OFTTransportAdapter(_oftDstEid, _oftFeeCap) {
        wrappedNativeToken = WETH9Interface(_wrappedNativeTokenAddress);
        depositQuoteTimeBuffer = _depositQuoteTimeBuffer;
        fillDeadlineBuffer = _fillDeadlineBuffer;
        _disableInitializers();
    }

    /**
     * @notice Construct the base SpokePool.
     * @param _initialDepositId Starting deposit ID. Set to 0 unless this is a re-deployment in order to mitigate
     * relay hash collisions.
     * @param _crossDomainAdmin Cross domain admin to set. Can be changed by admin.
     * @param _withdrawalRecipient Address which receives token withdrawals. Can be changed by admin. For Spoke Pools on L2, this will
     * likely be the hub pool.
     */
    function __SpokePool_init(
        uint32 _initialDepositId,
        address _crossDomainAdmin,
        address _withdrawalRecipient
    ) public onlyInitializing {
        numberOfDeposits = _initialDepositId;
        __EIP712_init("ACROSS-V2", "1.0.0");
        __UUPSUpgradeable_init();
        __ReentrancyGuard_init();
        _setCrossDomainAdmin(_crossDomainAdmin);
        _setWithdrawalRecipient(_withdrawalRecipient);
    }

    /****************************************
     *               MODIFIERS              *
     ****************************************/

    /**
     * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by
     * {upgradeTo} and {upgradeToAndCall}.
     * @dev This should be set to cross domain admin for specific SpokePool.
     */
    modifier onlyAdmin() {
        _requireAdminSender();
        _;
    }

    modifier unpausedDeposits() {
        if (pausedDeposits) revert DepositsArePaused();
        _;
    }

    modifier unpausedFills() {
        if (pausedFills) revert FillsArePaused();
        _;
    }

    /**************************************
     *          ADMIN FUNCTIONS           *
     **************************************/

    // Allows cross domain admin to upgrade UUPS proxy implementation.
    function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {}

    /**
     * @notice Pauses deposit-related functions. This is intended to be used if this contract is deprecated or when
     * something goes awry.
     * @dev Affects `deposit()` but not `speedUpDeposit()`, so that existing deposits can be sped up and still
     * relayed.
     * @param pause true if the call is meant to pause the system, false if the call is meant to unpause it.
     */
    function pauseDeposits(bool pause) public override onlyAdmin nonReentrant {
        pausedDeposits = pause;
        emit PausedDeposits(pause);
    }

    /**
     * @notice Pauses fill-related functions. This is intended to be used if this contract is deprecated or when
     * something goes awry.
     * @dev Affects fillRelayWithUpdatedDeposit() and fillRelay().
     * @param pause true if the call is meant to pause the system, false if the call is meant to unpause it.
     */
    function pauseFills(bool pause) public override onlyAdmin nonReentrant {
        pausedFills = pause;
        emit PausedFills(pause);
    }

    /**
     * @notice Change cross domain admin address. Callable by admin only.
     * @param newCrossDomainAdmin New cross domain admin.
     */
    function setCrossDomainAdmin(address newCrossDomainAdmin) public override onlyAdmin nonReentrant {
        _setCrossDomainAdmin(newCrossDomainAdmin);
    }

    /**
     * @notice Change L1 withdrawal recipient address. Callable by admin only.
     * @param newWithdrawalRecipient New withdrawal recipient address.
     */
    function setWithdrawalRecipient(address newWithdrawalRecipient) public override onlyAdmin nonReentrant {
        _setWithdrawalRecipient(newWithdrawalRecipient);
    }

    /**
     * @notice This method stores a new root bundle in this contract that can be executed to refund relayers, fulfill
     * slow relays, and send funds back to the HubPool on L1. This method can only be called by the admin and is
     * designed to be called as part of a cross-chain message from the HubPool's executeRootBundle method.
     * @param relayerRefundRoot Merkle root containing relayer refund leaves that can be individually executed via
     * executeRelayerRefundLeaf().
     * @param slowRelayRoot Merkle root containing slow relay fulfillment leaves that can be individually executed via
     * executeSlowRelayLeaf().
     */
    function relayRootBundle(bytes32 relayerRefundRoot, bytes32 slowRelayRoot) public override onlyAdmin nonReentrant {
        uint32 rootBundleId = uint32(rootBundles.length);
        RootBundle storage rootBundle = rootBundles.push();
        rootBundle.relayerRefundRoot = relayerRefundRoot;
        rootBundle.slowRelayRoot = slowRelayRoot;
        emit RelayedRootBundle(rootBundleId, relayerRefundRoot, slowRelayRoot);
    }

    /**
     * @notice This method is intended to only be used in emergencies where a bad root bundle has reached the
     * SpokePool.
     * @param rootBundleId Index of the root bundle that needs to be deleted. Note: this is intentionally a uint256
     * to ensure that a small input range doesn't limit which indices this method is able to reach.
     */
    function emergencyDeleteRootBundle(uint256 rootBundleId) public override onlyAdmin nonReentrant {
        // Deleting a struct containing a mapping does not delete the mapping in Solidity, therefore the bitmap's
        // data will still remain potentially leading to vulnerabilities down the line. The way around this would
        // be to iterate through every key in the mapping and resetting the value to 0, but this seems expensive and
        // would require a new list in storage to keep track of keys.
        //slither-disable-next-line mapping-deletion
        delete rootBundles[rootBundleId];
        emit EmergencyDeletedRootBundle(rootBundleId);
    }

    /**
     * @notice Add token -> OFTMessenger relationship. Callable only by admin.
     * @param token token address on the current chain
     * @param messenger IOFT contract address on the current chain for the specified token. Acts as a 'mailbox'
     */
    function setOftMessenger(address token, address messenger) public onlyAdmin nonReentrant {
        _setOftMessenger(token, messenger);
    }

    /**************************************
     *    LEGACY DEPOSITOR FUNCTIONS      *
     **************************************/

    /**
     * @dev DEPRECATION NOTICE: this function is deprecated and will be removed in the future.
     *      Please use deposit (under DEPOSITOR FUNCTIONS below) or depositV3 instead.
     * @notice Called by user to bridge funds from origin to destination chain. Depositor will effectively lock
     * tokens in this contract and receive a destination token on the destination chain. The origin => destination
     * token mapping is stored on the L1 HubPool.
     * @notice The caller must first approve this contract to spend amount of originToken.
     * @notice The originToken => destinationChainId must be enabled.
     * @notice This method is payable because the caller is able to deposit native token if the originToken is
     * wrappedNativeToken and this function will handle wrapping the native token to wrappedNativeToken.
     * @dev Produces a FundsDeposited event with an infinite expiry, meaning that this deposit can never expire.
     * Moreover, the event's outputToken is set to 0x0 meaning that this deposit can always be slow filled.
     * @param recipient Address to receive funds at on destination chain.
     * @param originToken Token to lock into this contract to initiate deposit.
     * @param amount Amount of tokens to deposit. Will be amount of tokens to receive less fees.
     * @param destinationChainId Denotes network where user will receive funds from SpokePool by a relayer.
     * @param relayerFeePct % of deposit amount taken out to incentivize a fast relayer.
     * @param quoteTimestamp Timestamp used by relayers to compute this deposit's realizedLPFeePct which is paid
     * to LP pool on HubPool.
     * @param message Arbitrary data that can be used to pass additional information to the recipient along with the tokens.
     * Note: this is intended to be used to pass along instructions for how a contract should use or allocate the tokens.
     */
    function depositDeprecated_5947912356(
        address recipient,
        address originToken,
        uint256 amount,
        uint256 destinationChainId,
        int64 relayerFeePct,
        uint32 quoteTimestamp,
        bytes memory message,
        uint256 // maxCount. Deprecated.
    ) public payable nonReentrant unpausedDeposits {
        _deposit(
            msg.sender,
            recipient,
            originToken,
            amount,
            destinationChainId,
            relayerFeePct,
            quoteTimestamp,
            message
        );
    }

    /**
     * @dev DEPRECATION NOTICE: this function is deprecated and will be removed in the future.
     *      Please use the other deposit or depositV3 instead.
     * @notice The only difference between depositFor and deposit is that the depositor address stored
     * in the relay hash can be overridden by the caller. This means that the passed in depositor
     * can speed up the deposit, which is useful if the deposit is taken from the end user to a middle layer
     * contract, like an aggregator or the SpokePoolVerifier, before calling deposit on this contract.
     * @notice The caller must first approve this contract to spend amount of originToken.
     * @notice The originToken => destinationChainId must be enabled.
     * @notice This method is payable because the caller is able to deposit native token if the originToken is
     * wrappedNativeToken and this function will handle wrapping the native token to wrappedNativeToken.
     * @param depositor Address who is credited for depositing funds on origin chain and can speed up the deposit.
     * @param recipient Address to receive funds at on destination chain.
     * @param originToken Token to lock into this contract to initiate deposit.
     * @param amount Amount of tokens to deposit. Will be amount of tokens to receive less fees.
     * @param destinationChainId Denotes network where user will receive funds from SpokePool by a relayer.
     * @param relayerFeePct % of deposit amount taken out to incentivize a fast relayer.
     * @param quoteTimestamp Timestamp used by relayers to compute this deposit's realizedLPFeePct which is paid
     * to LP pool on HubPool.
     * @param message Arbitrary data that can be used to pass additional information to the recipient along with the tokens.
     * Note: this is intended to be used to pass along instructions for how a contract should use or allocate the tokens.
     */
    function depositFor(
        address depositor,
        address recipient,
        address originToken,
        uint256 amount,
        uint256 destinationChainId,
        int64 relayerFeePct,
        uint32 quoteTimestamp,
        bytes memory message,
        uint256 // maxCount. Deprecated.
    ) public payable nonReentrant unpausedDeposits {
        _deposit(depositor, recipient, originToken, amount, destinationChainId, relayerFeePct, quoteTimestamp, message);
    }

    /********************************************
     *            DEPOSITOR FUNCTIONS           *
     ********************************************/

    /**
     * @notice Previously, this function allowed the caller to specify the exclusivityDeadline, otherwise known as the
     * as exact timestamp on the destination chain before which only the exclusiveRelayer could fill the deposit. Now,
     * the caller is expected to pass in a number that will be interpreted either as an offset or a fixed
     * timestamp depending on its value.
     * @notice Request to bridge input token cross chain to a destination chain and receive a specified amount
     * of output tokens. The fee paid to relayers and the system should be captured in the spread between output
     * amount and input amount when adjusted to be denominated in the input token. A relayer on the destination
     * chain will send outputAmount of outputTokens to the recipient and receive inputTokens on a repayment
     * chain of their choice. Therefore, the fee should account for destination fee transaction costs,
     * the relayer's opportunity cost of capital while they wait to be refunded following an optimistic challenge
     * window in the HubPool, and the system fee that they'll be charged.
     * @dev On the destination chain, the hash of the deposit data will be used to uniquely identify this deposit, so
     * modifying any params in it will result in a different hash and a different deposit. The hash will comprise
     * all parameters to this function along with this chain's chainId(). Relayers are only refunded for filling
     * deposits with deposit hashes that map exactly to the one emitted by this contract.
     * @param depositor The account credited with the deposit who can request to "speed up" this deposit by modifying
     * the output amount, recipient, and message.
     * @param recipient The account receiving funds on the destination chain. Can be an EOA or a contract. If
     * the output token is the wrapped native token for the chain, then the recipient will receive native token if
     * an EOA or wrapped native token if a contract.
     * @param inputToken The token pulled from the caller's account and locked into this contract to
     * initiate the deposit. The equivalent of this token on the relayer's repayment chain of choice will be sent
     * as a refund. If this is equal to the wrapped native token then the caller can optionally pass in native token as
     * msg.value, as long as msg.value = inputTokenAmount.
     * @param outputToken The token that the relayer will send to the recipient on the destination chain. Must be an
     * ERC20.
     * @param inputAmount The amount of input tokens to pull from the caller's account and lock into this contract.
     * This amount will be sent to the relayer on their repayment chain of choice as a refund following an optimistic
     * challenge window in the HubPool, less a system fee.
     * @param outputAmount The amount of output tokens that the relayer will send to the recipient on the destination.
     * @param destinationChainId The destination chain identifier. Must be enabled along with the input token
     * as a valid deposit route from this spoke pool or this transaction will revert.
     * @param exclusiveRelayer The relayer that will be exclusively allowed to fill this deposit before the
     * exclusivity deadline timestamp. This must be a valid, non-zero address if the exclusivity deadline is
     * greater than the current block.timestamp. If the exclusivity deadline is < currentTime, then this must be
     * address(0), and vice versa if this is address(0).
     * @param quoteTimestamp The HubPool timestamp that is used to determine the system fee paid by the depositor.
     *  This must be set to some time between [currentTime - depositQuoteTimeBuffer, currentTime]
     * where currentTime is block.timestamp on this chain or this transaction will revert.
     * @param fillDeadline The deadline for the relayer to fill the deposit. After this destination chain timestamp,
     * the fill will revert on the destination chain. Must be set before currentTime + fillDeadlineBuffer, where
     * currentTime is block.timestamp on this chain or this transaction will revert.
     * @param exclusivityParameter This value is used to set the exclusivity deadline timestamp in the emitted deposit
     * event. Before this destination chain timestamp, only the exclusiveRelayer (if set to a non-zero address),
     * can fill this deposit. There are three ways to use this parameter:
     *     1. NO EXCLUSIVITY: If this value is set to 0, then a timestamp of 0 will be emitted,
     *        meaning that there is no exclusivity period.
     *     2. OFFSET: If this value is less than MAX_EXCLUSIVITY_PERIOD_SECONDS, then add this value to
     *        the block.timestamp to derive the exclusive relayer deadline. Note that using the parameter in this way
     *        will expose the filler of the deposit to the risk that the block.timestamp of this event gets changed
     *        due to a chain-reorg, which would also change the exclusivity timestamp.
     *     3. TIMESTAMP: Otherwise, set this value as the exclusivity deadline timestamp.
     * which is the deadline for the exclusiveRelayer to fill the deposit.
     * @param message The message to send to the recipient on the destination chain if the recipient is a contract.
     * If the message is not empty, the recipient contract must implement handleV3AcrossMessage() or the fill will revert.
     */
    function deposit(
        bytes32 depositor,
        bytes32 recipient,
        bytes32 inputToken,
        bytes32 outputToken,
        uint256 inputAmount,
        uint256 outputAmount,
        uint256 destinationChainId,
        bytes32 exclusiveRelayer,
        uint32 quoteTimestamp,
        uint32 fillDeadline,
        uint32 exclusivityParameter,
        bytes calldata message
    ) public payable override nonReentrant unpausedDeposits {
        // Increment the `numberOfDeposits` counter to ensure a unique deposit ID for this spoke pool.
        DepositV3Params memory params = DepositV3Params({
            depositor: depositor,
            recipient: recipient,
            inputToken: inputToken,
            outputToken: outputToken,
            inputAmount: inputAmount,
            outputAmount: outputAmount,
            destinationChainId: destinationChainId,
            exclusiveRelayer: exclusiveRelayer,
            depositId: numberOfDeposits++,
            quoteTimestamp: quoteTimestamp,
            fillDeadline: fillDeadline,
            exclusivityParameter: exclusivityParameter,
            message: message
        });
        _depositV3(params);
    }

    /**
     * @notice A version of `deposit` that accepts `address` types for backward compatibility.
     * This function allows bridging of input tokens cross-chain to a destination chain, receiving a specified amount of output tokens.
     * The relayer is refunded in input tokens on a repayment chain of their choice, minus system fees, after an optimistic challenge
     * window. The exclusivity period is specified as an offset from the current block timestamp.
     *
     * @dev This version mirrors the original `depositV3` function, but uses `address` types for `depositor`, `recipient`,
     * `inputToken`, `outputToken`, and `exclusiveRelayer` for compatibility with contracts using the `address` type.
     *
     * The key functionality and logic remain identical, ensuring interoperability across both versions.
     *
     * @param depositor The account credited with the deposit who can request to "speed up" this deposit by modifying
     * the output amount, recipient, and message.
     * @param recipient The account receiving funds on the destination chain. Can be an EOA or a contract. If
     * the output token is the wrapped native token for the chain, then the recipient will receive native token if
     * an EOA or wrapped native token if a contract.
     * @param inputToken The token pulled from the caller's account and locked into this contract to initiate the deposit.
     * The equivalent of this token on the relayer's repayment chain of choice will be sent as a refund. If this is equal
     * to the wrapped native token, the caller can optionally pass in native token as msg.value, provided msg.value = inputTokenAmount.
     * @param outputToken The token that the relayer will send to the recipient on the destination chain. Must be an ERC20.
     * @param inputAmount The amount of input tokens pulled from the caller's account and locked into this contract. This
     * amount will be sent to the relayer as a refund following an optimistic challenge window in the HubPool, less a system fee.
     * @param outputAmount The amount of output tokens that the relayer will send to the recipient on the destination.
     * @param destinationChainId The destination chain identifier. Must be enabled along with the input token as a valid
     * deposit route from this spoke pool or this transaction will revert.
     * @param exclusiveRelayer The relayer exclusively allowed to fill this deposit before the exclusivity deadline.
     * @param quoteTimestamp The HubPool timestamp that determines the system fee paid by the depositor. This must be set
     * between [currentTime - depositQuoteTimeBuffer, currentTime] where currentTime is block.timestamp on this chain.
     * @param fillDeadline The deadline for the relayer to fill the deposit. After this destination chain timestamp, the fill will
     * revert on the destination chain. Must be set before currentTime + fillDeadlineBuffer, where currentTime is block.timestamp
     * on this chain.
     * @param exclusivityParameter This value is used to set the exclusivity deadline timestamp in the emitted deposit
     * event. Before this destination chain timestamp, only the exclusiveRelayer (if set to a non-zero address),
     * can fill this deposit. There are three ways to use this parameter:
     *     1. NO EXCLUSIVITY: If this value is set to 0, then a timestamp of 0 will be emitted,
     *        meaning that there is no exclusivity period.
     *     2. OFFSET: If this value is less than MAX_EXCLUSIVITY_PERIOD_SECONDS, then add this value to
     *        the block.timestamp to derive the exclusive relayer deadline. Note that using the parameter in this way
     *        will expose the filler of the deposit to the risk that the block.timestamp of this event gets changed
     *        due to a chain-reorg, which would also change the exclusivity timestamp.
     *     3. TIMESTAMP: Otherwise, set this value as the exclusivity deadline timestamp.
     * which is the deadline for the exclusiveRelayer to fill the deposit.
     * @param message The message to send to the recipient on the destination chain if the recipient is a contract. If the
     * message is not empty, the recipient contract must implement `handleV3AcrossMessage()` or the fill will revert.
     */
    function depositV3(
        address depositor,
        address recipient,
        address inputToken,
        address outputToken,
        uint256 inputAmount,
        uint256 outputAmount,
        uint256 destinationChainId,
        address exclusiveRelayer,
        uint32 quoteTimestamp,
        uint32 fillDeadline,
        uint32 exclusivityParameter,
        bytes calldata message
    ) public payable override {
        deposit(
            depositor.toBytes32(),
            recipient.toBytes32(),
            inputToken.toBytes32(),
            outputToken.toBytes32(),
            inputAmount,
            outputAmount,
            destinationChainId,
            exclusiveRelayer.toBytes32(),
            quoteTimestamp,
            fillDeadline,
            exclusivityParameter,
            message
        );
    }

    /**
     * @notice See deposit for details. This function is identical to deposit except that it does not use the
     * global deposit ID counter as a deposit nonce, instead allowing the caller to pass in a deposit nonce. This
     * function is designed to be used by anyone who wants to pre-compute their resultant relay data hash, which
     * could be useful for filling a deposit faster and avoiding any risk of a relay hash unexpectedly changing
     * due to another deposit front-running this one and incrementing the global deposit ID counter.
     * @dev This is labeled "unsafe" because there is no guarantee that the depositId emitted in the resultant
     * FundsDeposited event is unique which means that the
     * corresponding fill might collide with an existing relay hash on the destination chain SpokePool,
     * which would make this deposit unfillable. In this case, the depositor would subsequently receive a refund
     * of `inputAmount` of `inputToken` on the origin chain after the fill deadline. Re-using a depositNonce is very
     * dangerous when combined with `speedUpDeposit`, as a speed up signature can be re-used for any deposits
     * with the same deposit ID.
     * @dev On the destination chain, the hash of the deposit data will be used to uniquely identify this deposit, so
     * modifying any params in it will result in a different hash and a different deposit. The hash will comprise
     * all parameters to this function along with this chain's chainId(). Relayers are only refunded for filling
     * deposits with deposit hashes that map exactly to the one emitted by this contract.
     * @param depositNonce The nonce that uniquely identifies this deposit. This function will combine this parameter
     * with the msg.sender address to create a unique uint256 depositNonce and ensure that the msg.sender cannot
     * use this function to front-run another depositor's unsafe deposit. This function guarantees that the resultant
     * deposit nonce will not collide with a safe uint256 deposit nonce whose 24 most significant bytes are always 0.
     * @param depositor See identically named parameter in depositV3() comments.
     * @param recipient See identically named parameter in depositV3() comments.
     * @param inputToken See identically named parameter in depositV3() comments.
     * @param outputToken See identically named parameter in depositV3() comments.
     * @param inputAmount See identically named parameter in depositV3() comments.
     * @param outputAmount See identically named parameter in depositV3() comments.
     * @param destinationChainId See identically named parameter in depositV3() comments.
     * @param exclusiveRelayer See identically named parameter in depositV3() comments.
     * @param quoteTimestamp See identically named parameter in depositV3() comments.
     * @param fillDeadline See identically named parameter in depositV3() comments.
     * @param exclusivityParameter See identically named parameter in depositV3() comments.
     * @param message See identically named parameter in depositV3() comments.
     */
    function unsafeDeposit(
        bytes32 depositor,
        bytes32 recipient,
        bytes32 inputToken,
        bytes32 outputToken,
        uint256 inputAmount,
        uint256 outputAmount,
        uint256 destinationChainId,
        bytes32 exclusiveRelayer,
        uint256 depositNonce,
        uint32 quoteTimestamp,
        uint32 fillDeadline,
        uint32 exclusivityParameter,
        bytes calldata message
    ) public payable nonReentrant unpausedDeposits {
        // @dev Create the uint256 deposit ID by concatenating the msg.sender and depositor address with the inputted
        // depositNonce parameter. The resultant 32 byte string will be hashed and then casted to an "unsafe"
        // uint256 deposit ID. The probability that the resultant ID collides with a "safe" deposit ID is
        // equal to the chance that the first 28 bytes of the hash are 0, which is too small for us to consider.

        uint256 depositId = getUnsafeDepositId(msg.sender, depositor, depositNonce);
        DepositV3Params memory params = DepositV3Params({
            depositor: depositor,
            recipient: recipient,
            inputToken: inputToken,
            outputToken: outputToken,
            inputAmount: inputAmount,
            outputAmount: outputAmount,
            destinationChainId: destinationChainId,
            exclusiveRelayer: exclusiveRelayer,
            depositId: depositId,
            quoteTimestamp: quoteTimestamp,
            fillDeadline: fillDeadline,
            exclusivityParameter: exclusivityParameter,
            message: message
        });
        _depositV3(params);
    }

    /**
     * @notice Submits deposit and sets quoteTimestamp to current Time. Sets fill and exclusivity
     * deadlines as offsets added to the current time. This function is designed to be called by users
     * such as Multisig contracts who do not have certainty when their transaction will mine.
     * @param depositor The account credited with the deposit who can request to "speed up" this deposit by modifying
     * the output amount, recipient, and message.
     * @param recipient The account receiving funds on the destination chain. Can be an EOA or a contract. If
     * the output token is the wrapped native token for the chain, then the recipient will receive native token if
     * an EOA or wrapped native token if a contract.
     * @param inputToken The token pulled from the caller's account and locked into this contract to
     * initiate the deposit. The equivalent of this token on the relayer's repayment chain of choice will be sent
     * as a refund. If this is equal to the wrapped native token then the caller can optionally pass in native token as
     * msg.value, as long as msg.value = inputTokenAmount.
     * @param outputToken The token that the relayer will send to the recipient on the destination chain. Must be an
     * ERC20.
     * @param inputAmount The amount of input tokens to pull from the caller's account and lock into this contract.
     * This amount will be sent to the relayer on their repayment chain of choice as a refund following an optimistic
     * challenge window in the HubPool, plus a system fee.
     * @param outputAmount The amount of output tokens that the relayer will send to the recipient on the destination.
     * @param destinationChainId The destination chain identifier. Must be enabled along with the input token
     * as a valid deposit route from this spoke pool or this transaction will revert.
     * @param exclusiveRelayer The relayer that will be exclusively allowed to fill this deposit before the
     * exclusivity deadline timestamp.
     * @param fillDeadlineOffset Added to the current time to set the fill deadline, which is the deadline for the
     * relayer to fill the deposit. After this destination chain timestamp, the fill will revert on the
     * destination chain.
     * @param exclusivityParameter See identically named parameter in deposit() comments.
     * @param message The message to send to the recipient on the destination chain if the recipient is a contract.
     * If the message is not empty, the recipient contract must implement handleV3AcrossMessage() or the fill will revert.
     */
    function depositNow(
        bytes32 depositor,
        bytes32 recipient,
        bytes32 inputToken,
        bytes32 outputToken,
        uint256 inputAmount,
        uint256 outputAmount,
        uint256 destinationChainId,
        bytes32 exclusiveRelayer,
        uint32 fillDeadlineOffset,
        uint32 exclusivityParameter,
        bytes calldata message
    ) external payable override {
        deposit(
            depositor,
            recipient,
            inputToken,
            outputToken,
            inputAmount,
            outputAmount,
            destinationChainId,
            exclusiveRelayer,
            uint32(getCurrentTime()),
            uint32(getCurrentTime()) + fillDeadlineOffset,
            exclusivityParameter,
            message
        );
    }

    /**
     * @notice A version of `depositNow` that supports addresses as input types for backward compatibility.
     * This function submits a deposit and sets `quoteTimestamp` to the current time. The `fill` and `exclusivity` deadlines
     * are set as offsets added to the current time. It is designed to be called by users, including Multisig contracts, who may
     * not have certainty when their transaction will be mined.
     *
     * @dev This version is identical to the original `depositV3Now` but uses `address` types for `depositor`, `recipient`,
     * `inputToken`, `outputToken`, and `exclusiveRelayer` to support compatibility with older systems.
     * It maintains the same logic and purpose, ensuring interoperability with both versions.
     *
     * @param depositor The account credited with the deposit, who can request to "speed up" this deposit by modifying
     * the output amount, recipient, and message.
     * @param recipient The account receiving funds on the destination chain. Can be an EOA or a contract. If
     * the output token is the wrapped native token for the chain, then the recipient will receive the native token if
     * an EOA or wrapped native token if a contract.
     * @param inputToken The token pulled from the caller's account and locked into this contract to initiate the deposit.
     * Equivalent tokens on the relayer's repayment chain will be sent as a refund. If this is the wrapped native token,
     * msg.value must equal inputTokenAmount when passed.
     * @param outputToken The token the relayer will send to the recipient on the destination chain. Must be an ERC20.
     * @param inputAmount The amount of input tokens pulled from the caller's account and locked into this contract.
     * This amount will be sent to the relayer as a refund following an optimistic challenge window in the HubPool, plus a system fee.
     * @param outputAmount The amount of output tokens the relayer will send to the recipient on the destination.
     * @param destinationChainId The destination chain identifier. Must be enabled with the input token as a valid deposit route
     * from this spoke pool, or the transaction will revert.
     * @param exclusiveRelayer The relayer exclusively allowed to fill the deposit before the exclusivity deadline.
     * @param fillDeadlineOffset Added to the current time to set the fill deadline. After this timestamp, fills on the
     * destination chain will revert.
     * @param exclusivityParameter See identically named parameter in deposit() comments.
     * @param message The message to send to the recipient on the destination chain. If the recipient is a contract, it must
     * implement `handleV3AcrossMessage()` if the message is not empty, or the fill will revert.
     */
    function depositV3Now(
        address depositor,
        address recipient,
        address inputToken,
        address outputToken,
        uint256 inputAmount,
        uint256 outputAmount,
        uint256 destinationChainId,
        address exclusiveRelayer,
        uint32 fillDeadlineOffset,
        uint32 exclusivityParameter,
        bytes calldata message
    ) external payable override {
        depositV3(
            depositor,
            recipient,
            inputToken,
            outputToken,
            inputAmount,
            outputAmount,
            destinationChainId,
            exclusiveRelayer,
            uint32(getCurrentTime()),
            uint32(getCurrentTime()) + fillDeadlineOffset,
            exclusivityParameter,
            message
        );
    }

    /**
     * @notice Depositor can use this function to signal to relayer to use updated output amount, recipient,
     * and/or message. The speed up signature uniquely identifies the speed up based only on
     * depositor, deposit ID and origin chain, so using this function in conjunction with unsafeDeposit is risky
     * due to the chance of repeating a deposit ID.
     * @dev the depositor and depositId must match the params in a FundsDeposited event that the depositor
     * wants to speed up. The relayer has the option but not the obligation to use this updated information
     * when filling the deposit via fillRelayWithUpdatedDeposit().
     * @param depositor Depositor that must sign the depositorSignature and was the original depositor.
     * @param depositId Deposit ID to speed up.
     * @param updatedOutputAmount New output amount to use for this deposit. Should be lower than previous value
     * otherwise relayer has no incentive to use this updated value.
     * @param updatedRecipient New recipient to use for this deposit. Can be modified if the recipient is a contract
     * that expects to receive a message from the relay and for some reason needs to be modified.
     * @param updatedMessage New message to use for this deposit. Can be modified if the recipient is a contract
     * that expects to receive a message from the relay and for some reason needs to be modified.
     * @param depositorSignature Signed EIP712 hashstruct containing the deposit ID. Should be signed by the depositor
     * account. If depositor is a contract, then should implement EIP1271 to sign as a contract. See
     * _verifyUpdateV3DepositMessage() for more details about how this signature should be constructed.
     */
    function speedUpDeposit(
        bytes32 depositor,
        uint256 depositId,
        uint256 updatedOutputAmount,
        bytes32 updatedRecipient,
        bytes calldata updatedMessage,
        bytes calldata depositorSignature
    ) public override nonReentrant {
        _verifyUpdateV3DepositMessage(
            depositor.toAddress(),
            depositId,
            chainId(),
            updatedOutputAmount,
            updatedRecipient,
            updatedMessage,
            depositorSignature,
            UPDATE_BYTES32_DEPOSIT_DETAILS_HASH
        );

        // Assuming the above checks passed, a relayer can take the signature and the updated deposit information
        // from the following event to submit a fill with updated relay data.
        emit RequestedSpeedUpDeposit(
            updatedOutputAmount,
            depositId,
            depositor,
            updatedRecipient,
            updatedMessage,
            depositorSignature
        );
    }

    /**
     * @notice A version of `speedUpDeposit` using `address` types for backward compatibility.
     * This function allows the depositor to signal to the relayer to use updated output amount, recipient, and/or message
     * when filling a deposit. This can be useful when the deposit needs to be modified after the original transaction has
     * been mined.
     *
     * @dev The `depositor` and `depositId` must match the parameters in a `FundsDeposited` event that the depositor wants to speed up.
     * The relayer is not obligated but has the option to use this updated information when filling the deposit using
     * `fillRelayWithUpdatedDeposit()`. This version uses `address` types for compatibility with systems relying on
     * `address`-based implementations.
     *
     * @param depositor The depositor that must sign the `depositorSignature` and was the original depositor.
     * @param depositId The deposit ID to speed up.
     * @param updatedOutputAmount The new output amount to use for this deposit. It should be lower than the previous value,
     * otherwise the relayer has no incentive to use this updated value.
     * @param updatedRecipient The new recipient for this deposit. Can be modified if the original recipient is a contract that
     * expects to receive a message from the relay and needs to be changed.
     * @param updatedMessage The new message for this deposit. Can be modified if the recipient is a contract that expects
     * to receive a message from the relay and needs to be updated.
     * @param depositorSignature The signed EIP712 hashstruct containing the deposit ID. Should be signed by the depositor account.
     * If the depositor is a contract, it should implement EIP1271 to sign as a contract. See `_verifyUpdateV3DepositMessage()`
     * for more details on how the signature should be constructed.
     */
    function speedUpV3Deposit(
        address depositor,
        uint256 depositId,
        uint256 updatedOutputAmount,
        address updatedRecipient,
        bytes calldata updatedMessage,
        bytes calldata depositorSignature
    ) public {
        _verifyUpdateV3DepositMessage(
            depositor,
            depositId,
            chainId(),
            updatedOutputAmount,
            updatedRecipient.toBytes32(),
            updatedMessage,
            depositorSignature,
            UPDATE_BYTES32_DEPOSIT_DETAILS_HASH
        );

        // Assuming the above checks passed, a relayer can take the signature and the updated deposit information
        // from the following event to submit a fill with updated relay data.
        emit RequestedSpeedUpDeposit(
            updatedOutputAmount,
            depositId,
            depositor.toBytes32(),
            updatedRecipient.toBytes32(),
            updatedMessage,
            depositorSignature
        );
    }

    /**************************************
     *         RELAYER FUNCTIONS          *
     **************************************/

    /**
     * @notice Fulfill request to bridge cross chain by sending specified output tokens to the recipient.
     * @dev The fee paid to relayers and the system should be captured in the spread between output
     * amount and input amount when adjusted to be denominated in the input token. A relayer on the destination
     * chain will send outputAmount of outputTokens to the recipient and receive inputTokens on a repayment
     * chain of their choice. Therefore, the fee should account for destination fee transaction costs, the
     * relayer's opportunity cost of capital while they wait to be refunded following an optimistic challenge
     * window in the HubPool, and a system fee charged to relayers.
     * @dev The hash of the relayData will be used to uniquely identify the deposit to fill, so
     * modifying any params in it will result in a different hash and a different deposit. The hash will comprise
     * all parameters passed to deposit() on the origin chain along with that chain's chainId(). This chain's
     * chainId() must therefore match the destinationChainId passed into deposit.
     * Relayers are only refunded for filling deposits with deposit hashes that map exactly to the one emitted by the
     * origin SpokePool therefore the relayer should not modify any params in relayData.
     * @dev Cannot fill more than once. Partial fills are not supported.
     * @param relayData struct containing all the data needed to identify the deposit to be filled. Should match
     * all the same-named parameters emitted in the origin chain FundsDeposited event.
     * - depositor: The account credited with the deposit who can request to "speed up" this deposit by modifying
     * the output amount, recipient, and message.
     * - recipient The account receiving funds on this chain. Can be an EOA or a contract. If
     * the output token is the wrapped native token for the chain, then the recipient will receive native token if
     * an EOA or wrapped native token if a contract.
     * - inputToken: The token pulled from the caller's account to initiate the deposit. The equivalent of this
     * token on the repayment chain will be sent as a refund to the caller.
     * - outputToken The token that the caller will send to the recipient on the destination chain. Must be an
     * ERC20.
     * - inputAmount: This amount, less a system fee, will be sent to the caller on their repayment chain of choice as a refund
     * following an optimistic challenge window in the HubPool.
     * - outputAmount: The amount of output tokens that the caller will send to the recipient.
     * - originChainId: The origin chain identifier.
     * - exclusiveRelayer The relayer that will be exclusively allowed to fill this deposit before the
     * exclusivity deadline timestamp.
     * - fillDeadline The deadline for the caller to fill the deposit. After this timestamp,
     * the fill will revert on the destination chain.
     * - exclusivityDeadline: The deadline for the exclusive relayer to fill the deposit. After this
     * timestamp, anyone can fill this deposit. Note that if this value was set in deposit by adding an offset
     * to the deposit's block.timestamp, there is re-org risk for the caller of this method because the event's
     * block.timestamp can change. Read the comments in `deposit` about the `exclusivityParameter` for more details.
     * - message The message to send to the recipient if the recipient is a contract that implements a
     * handleV3AcrossMessage() public function
     * @param repaymentChainId Chain of SpokePool where relayer wants to be refunded after the challenge window has
     * passed. Will receive inputAmount of the equivalent token to inputToken on the repayment chain.
     * @param repaymentAddress Address the relayer wants to be receive their refund at.
     */
    function fillRelay(
        V3RelayData memory relayData,
        uint256 repaymentChainId,
        bytes32 repaymentAddress
    ) public override nonReentrant unpausedFills {
        // Exclusivity deadline is inclusive and is the latest timestamp that the exclusive relayer has sole right
        // to fill the relay.
        if (
            _fillIsExclusive(relayData.exclusivityDeadline, uint32(getCurrentTime())) &&
            relayData.exclusiveRelayer.toAddress() != msg.sender
        ) {
            revert NotExclusiveRelayer();
        }

        V3RelayExecutionParams memory relayExecution = V3RelayExecutionParams({
            relay: relayData,
            relayHash: getV3RelayHash(relayData),
            updatedOutputAmount: relayData.outputAmount,
            updatedRecipient: relayData.recipient,
            updatedMessage: relayData.message,
            repaymentChainId: repaymentChainId
        });

        _fillRelayV3(relayExecution, repaymentAddress, false);
    }

    // Exposes the same function as fillRelay but with a legacy V3RelayData struct that takes in address types. Inner
    // function fillV3Relay() applies reentrancy & non-paused checks.
    function fillV3Relay(V3RelayDataLegacy calldata relayData, uint256 repaymentChainId) public override {
        // Convert V3RelayDataLegacy to V3RelayData using the .toBytes32() method.
        V3RelayData memory convertedRelayData = V3RelayData({
            depositor: relayData.depositor.toBytes32(),
            recipient: relayData.recipient.toBytes32(),
            exclusiveRelayer: relayData.exclusiveRelayer.toBytes32(),
            inputToken: relayData.inputToken.toBytes32(),
            outputToken: relayData.outputToken.toBytes32(),
            inputAmount: relayData.inputAmount,
            outputAmount: relayData.outputAmount,
            originChainId: relayData.originChainId,
            depositId: relayData.depositId,
            fillDeadline: relayData.fillDeadline,
            exclusivityDeadline: relayData.exclusivityDeadline,
            message: relayData.message
        });

        fillRelay(convertedRelayData, repaymentChainId, msg.sender.toBytes32());
    }

    /**
     * @notice Identical to fillV3Relay except that the relayer wants to use a depositor's updated output amount,
     * recipient, and/or message. The relayer should only use this function if they can supply a message signed
     * by the depositor that contains the fill's matching deposit ID along with updated relay parameters.
     * If the signature can be verified, then this function will emit a FilledV3Event that will be used by
     * the system for refund verification purposes. In other words, this function is an alternative way to fill a
     * a deposit than fillV3Relay.
     * @dev Subject to same exclusivity deadline rules as fillV3Relay().
     * @param relayData struct containing all the data needed to identify the deposit to be filled. See fillV3Relay().
     * @param repaymentChainId Chain of SpokePool where relayer wants to be refunded after the challenge window has
     * passed. See fillV3Relay().
     * @param repaymentAddress Address the relayer wants to be receive their refund at.
     * @param updatedOutputAmount New output amount to use for this deposit.
     * @param updatedRecipient New recipient to use for this deposit.
     * @param updatedMessage New message to use for this deposit.
     * @param depositorSignature Signed EIP712 hashstruct containing the deposit ID. Should be signed by the depositor
     * account.
     */
    function fillRelayWithUpdatedDeposit(
        V3RelayData calldata relayData,
        uint256 repaymentChainId,
        bytes32 repaymentAddress,
        uint256 updatedOutputAmount,
        bytes32 updatedRecipient,
        bytes calldata updatedMessage,
        bytes calldata depositorSignature
    ) public override nonReentrant unpausedFills {
        // Exclusivity deadline is inclusive and is the latest timestamp that the exclusive relayer has sole right
        // to fill the relay.
        if (
            _fillIsExclusive(relayData.exclusivityDeadline, uint32(getCurrentTime())) &&
            relayData.exclusiveRelayer.toAddress() != msg.sender
        ) {
            revert NotExclusiveRelayer();
        }

        V3RelayExecutionParams memory relayExecution = V3RelayExecutionParams({
            relay: relayData,
            relayHash: getV3RelayHash(relayData),
            updatedOutputAmount: updatedOutputAmount,
            updatedRecipient: updatedRecipient,
            updatedMessage: updatedMessage,
            repaymentChainId: repaymentChainId
        });

        _verifyUpdateV3DepositMessage(
            relayData.depositor.toAddress(),
            relayData.depositId,
            relayData.originChainId,
            updatedOutputAmount,
            updatedRecipient,
            updatedMessage,
            depositorSignature,
            UPDATE_BYTES32_DEPOSIT_DETAILS_HASH
        );

        _fillRelayV3(relayExecution, repaymentAddress, false);
    }

    /**
     * @notice Request Across to send LP funds to this contract to fulfill a slow fill relay
     * for a deposit in the next bundle.
     * @dev Slow fills are not possible unless the input and output tokens are "equivalent", i.e.
     * they route to the same L1 token via PoolRebalanceRoutes.
     * @dev Slow fills are created by inserting slow fill objects into a merkle tree that is included
     * in the next HubPool "root bundle". Once the optimistic challenge window has passed, the HubPool
     * will relay the slow root to this chain via relayRootBundle(). Once the slow root is relayed,
     * the slow fill can be executed by anyone who calls executeSlowRelayLeaf().
     * @dev Cannot request a slow fill if the fill deadline has passed.
     * @dev Cannot request a slow fill if the relay has already been filled or a slow fill has already been requested.
     * @param relayData struct containing all the data needed to identify the deposit that should be
     * slow filled. If any of the params are missing or different from the origin chain deposit,
     * then Across will not include a slow fill for the intended deposit.
     */
    function requestSlowFill(V3RelayData calldata relayData) public override nonReentrant unpausedFills {
        uint32 currentTime = uint32(getCurrentTime());
        // If a depositor has set an exclusivity deadline, then only the exclusive relayer should be able to
        // fast fill within this deadline. Moreover, the depositor should expect to get *fast* filled within
        // this deadline, not slow filled. As a simplifying assumption, we will not allow slow fills to be requested
        // during this exclusivity period.
        if (_fillIsExclusive(relayData.exclusivityDeadline, currentTime)) {
            revert NoSlowFillsInExclusivityWindow();
        }
        if (relayData.fillDeadline < currentTime) revert ExpiredFillDeadline();

        bytes32 relayHash = getV3RelayHash(relayData);
        if (fillStatuses[relayHash] != uint256(FillStatus.Unfilled)) revert InvalidSlowFillRequest();
        fillStatuses[relayHash] = uint256(FillStatus.RequestedSlowFill);

        emit RequestedSlowFill(
            relayData.inputToken,
            relayData.outputToken,
            relayData.inputAmount,
            relayData.outputAmount,
            relayData.originChainId,
            relayData.depositId,
            relayData.fillDeadline,
            relayData.exclusivityDeadline,
            relayData.exclusiveRelayer,
            relayData.depositor,
            relayData.recipient,
            _hashNonEmptyMessage(relayData.message)
        );
    }

    /**
     * @notice Fills a single leg of a particular order on the destination chain
     * @dev ERC-7683 fill function.
     * @param orderId Unique order identifier for this order
     * @param originData Data emitted on the origin to parameterize the fill
     * @param fillerData Data provided by the filler to inform the fill or express their preferences
     */
    function fill(
        bytes32 orderId,
        bytes calldata originData,
        bytes calldata fillerData
    ) external {
        if (keccak256(abi.encode(originData, chainId())) != orderId) {
            revert WrongERC7683OrderId();
        }

        // Ensure that the call is not malformed. If the call is malformed, abi.decode will fail.
        V3SpokePoolInterface.V3RelayData memory relayData = abi.decode(originData, (V3SpokePoolInterface.V3RelayData));
        AcrossDestinationFillerData memory destinationFillerData = abi.decode(
            fillerData,
            (AcrossDestinationFillerData)
        );

        // Must do a delegatecall because the function requires the inputs to be calldata.
        (bool success, bytes memory data) = address(this).delegatecall(
            abi.encodeCall(
                V3SpokePoolInterface.fillRelay,
                (relayData, destinationFillerData.repaymentChainId, msg.sender.toBytes32())
            )
        );
        if (!success) {
            revert LowLevelCallFailed(data);
        }
    }

    /**************************************
     *         DATA WORKER FUNCTIONS      *
     **************************************/

    /**
     * @notice Executes a slow relay leaf stored as part of a root bundle relayed by the HubPool.
     * @dev Executing a slow fill leaf is equivalent to filling the relayData so this function cannot be used to
     * double fill a recipient. The relayData that is filled is included in the slowFillLeaf and is hashed
     * like any other fill sent through a fill method.
     * @dev There is no relayer credited with filling this relay since funds are sent directly out of this contract.
     * @param slowFillLeaf Contains all data necessary to uniquely identify a relay for this chain. This struct is
     * hashed and included in a merkle root that is relayed to all spoke pools.
     * - relayData: struct containing all the data needed to identify the original deposit to be slow filled.
     * - chainId: chain identifier where slow fill leaf should be executed. If this doesn't match this chain's
     * chainId, then this function will revert.
     * - updatedOutputAmount: Amount to be sent to recipient out of this contract's balance. Can be set differently
     * from relayData.outputAmount to charge a different fee because this deposit was "slow" filled. Usually,
     * this will be set higher to reimburse the recipient for waiting for the slow fill.
     * @param rootBundleId Unique ID of root bundle containing slow relay root that this leaf is contained in.
     * @param proof Inclusion proof for this leaf in slow relay root in root bundle.
     */
    function executeSlowRelayLeaf(
        V3SlowFill calldata slowFillLeaf,
        uint32 rootBundleId,
        bytes32[] calldata proof
    ) public override nonReentrant {
        V3RelayData memory relayData = slowFillLeaf.relayData;

        _preExecuteLeafHook(relayData.outputToken.toAddress());

        // @TODO In the future consider allowing way for slow fill leaf to be created with updated
        // deposit params like outputAmount, message and recipient.
        V3RelayExecutionParams memory relayExecution = V3RelayExecutionParams({
            relay: relayData,
            relayHash: getV3RelayHash(relayData),
            updatedOutputAmount: slowFillLeaf.updatedOutputAmount,
            updatedRecipient: relayData.recipient,
            updatedMessage: relayData.message,
            repaymentChainId: EMPTY_REPAYMENT_CHAIN_ID // Repayment not relevant for slow fills.
        });

        _verifyV3SlowFill(relayExecution, rootBundleId, proof);

        // - No relayer to refund for slow fill executions.
        _fillRelayV3(relayExecution, EMPTY_RELAYER, true);
    }

    /**
     * @notice Executes a relayer refund leaf stored as part of a root bundle. Will send the relayer the amount they
     * sent to the recipient plus a relayer fee.
     * @param rootBundleId Unique ID of root bundle containing relayer refund root that this leaf is contained in.
     * @param relayerRefundLeaf Contains all data necessary to reconstruct leaf contained in root bundle and to
     * refund relayer. This data structure is explained in detail in the SpokePoolInterface.
     * @param proof Inclusion proof for this leaf in relayer refund root in root bundle.
     */
    function executeRelayerRefundLeaf(
        uint32 rootBundleId,
        SpokePoolInterface.RelayerRefundLeaf memory relayerRefundLeaf,
        bytes32[] memory proof
    ) public payable virtual override nonReentrant {
        _preExecuteLeafHook(relayerRefundLeaf.l2TokenAddress);

        if (relayerRefundLeaf.chainId != chainId()) revert InvalidChainId();

        RootBundle storage rootBundle = rootBundles[rootBundleId];

        // Check that proof proves that relayerRefundLeaf is contained within the relayer refund root.
        // Note: This should revert if the relayerRefundRoot is uninitialized.
        if (!MerkleLib.verifyRelayerRefund(rootBundle.relayerRefundRoot, relayerRefundLeaf, proof)) {
            revert InvalidMerkleProof();
        }

        _setClaimedLeaf(rootBundleId, relayerRefundLeaf.leafId);

        bool deferredRefunds = _distributeRelayerRefunds(
            relayerRefundLeaf.chainId,
            relayerRefundLeaf.amountToReturn,
            relayerRefundLeaf.refundAmounts,
            relayerRefundLeaf.leafId,
            relayerRefundLeaf.l2TokenAddress,
            relayerRefundLeaf.refundAddresses
        );

        emit ExecutedRelayerRefundRoot(
            relayerRefundLeaf.amountToReturn,
            relayerRefundLeaf.chainId,
            relayerRefundLeaf.refundAmounts,
            rootBundleId,
            relayerRefundLeaf.leafId,
            relayerRefundLeaf.l2TokenAddress,
            relayerRefundLeaf.refundAddresses,
            deferredRefunds,
            msg.sender
        );
    }

    /**
     * @notice Enables a relayer to claim outstanding repayments. Should virtually never be used, unless for some reason
     * relayer repayment transfer fails for reasons such as token transfer reverts due to blacklisting. In this case,
     * the relayer can still call this method and claim the tokens to a new address.
     * @param l2TokenAddress Address of the L2 token to claim refunds for.
     * @param refundAddress Address to send the refund to.
     */
    function claimRelayerRefund(bytes32 l2TokenAddress, bytes32 refundAddress) external {
        uint256 refund = relayerRefund[l2TokenAddress.toAddress()][msg.sender];
        if (refund == 0) revert NoRelayerRefundToClaim();
        relayerRefund[l2TokenAddress.toAddress()][msg.sender] = 0;
        IERC20Upgradeable(l2TokenAddress.toAddress()).safeTransfer(refundAddress.toAddress(), refund);

        emit ClaimedRelayerRefund(l2TokenAddress, refundAddress, refund, msg.sender);
    }

    /**************************************
     *           VIEW FUNCTIONS           *
     **************************************/

    /**
     * @notice Returns chain ID for this network.
     * @dev Some L2s like ZKSync don't support the CHAIN_ID opcode so we allow the implementer to override this.
     */
    function chainId() public view virtual override returns (uint256) {
        return block.chainid;
    }

    /**
     * @notice Gets the current time.
     * @return uint for the current timestamp.
     */
    function getCurrentTime() public view virtual returns (uint256) {
        return block.timestamp; // solhint-disable-line not-rely-on-time
    }

    /**
     * @notice Returns the deposit ID for an unsafe deposit. This function is used to compute the deposit ID
     * in unsafeDeposit and is provided as a convenience.
     * @dev msgSender and depositor are both used as inputs to allow passthrough depositors to create unique
     * deposit hash spaces for unique depositors.
     * @param msgSender The caller of the transaction used as input to produce the deposit ID.
     * @param depositor The depositor address used as input to produce the deposit ID.
     * @param depositNonce The nonce used as input to produce the deposit ID.
     * @return The deposit ID for the unsafe deposit.
     */
    function getUnsafeDepositId(
        address msgSender,
        bytes32 depositor,
        uint256 depositNonce
    ) public pure returns (uint256) {
        return uint256(keccak256(abi.encodePacked(msgSender, depositor, depositNonce)));
    }

    function getRelayerRefund(address l2TokenAddress, address refundAddress) public view returns (uint256) {
        return relayerRefund[l2TokenAddress][refundAddress];
    }

    function getV3RelayHash(V3RelayData memory relayData) public view returns (bytes32) {
        return keccak256(abi.encode(relayData, chainId()));
    }

    /**************************************
     *         INTERNAL FUNCTIONS         *
     **************************************/

    function _depositV3(DepositV3Params memory params) internal {
        // Verify depositor is a valid EVM address.
        params.depositor.checkAddress();

        // Require that quoteTimestamp has a maximum age so that depositors pay an LP fee based on recent HubPool usage.
        // It is assumed that cross-chain timestamps are normally loosely in-sync, but clock drift can occur. If the
        // SpokePool time stalls or lags significantly, it is still possible to make deposits by setting quoteTimestamp
        // within the configured buffer. The owner should pause deposits/fills if this is undesirable.
        // This will underflow if quoteTimestamp is more than depositQuoteTimeBuffer;
        // this is safe but will throw an unintuitive error.

        // slither-disable-next-line timestamp
        uint256 currentTime = getCurrentTime();
        if (currentTime < params.quoteTimestamp || currentTime - params.quoteTimestamp > depositQuoteTimeBuffer)
            revert InvalidQuoteTimestamp();

        // fillDeadline is relative to the destination chain.
        // Don’t allow fillDeadline to be more than several bundles into the future.
        // This limits the maximum required lookback for dataworker and relayer instances.
        if (params.fillDeadline > currentTime + fillDeadlineBuffer) revert InvalidFillDeadline();

        // There are three cases for setting the exclusivity deadline using the exclusivity parameter:
        // 1. If this parameter is 0, then there is no exclusivity period and emit 0 for the deadline. This
        //    means that fillers of this deposit do not have to worry about the block.timestamp of this event changing
        //    due to re-orgs when filling this deposit.
        // 2. If the exclusivity parameter is less than or equal to MAX_EXCLUSIVITY_PERIOD_SECONDS, then the exclusivity
        //    deadline is set to the block.timestamp of this event plus the exclusivity parameter. This means that the
        //    filler of this deposit assumes re-org risk when filling this deposit because the block.timestamp of this
        //    event affects the exclusivity deadline.
        // 3. Otherwise, interpret this parameter as a timestamp and emit it as the exclusivity deadline. This means
        //    that the filler of this deposit will not assume re-org risk related to the block.timestamp of this
        //    event changing.
        uint32 exclusivityDeadline = params.exclusivityParameter;
        if (exclusivityDeadline > 0) {
            if (exclusivityDeadline <= MAX_EXCLUSIVITY_PERIOD_SECONDS) {
                exclusivityDeadline += uint32(currentTime);
            }

            // As a safety measure, prevent caller from inadvertently locking funds during exclusivity period
            //  by forcing them to specify an exclusive relayer.
            if (params.exclusiveRelayer == bytes32(0)) revert InvalidExclusiveRelayer();
        }

        // If the address of the origin token is a wrappedNativeToken contract and there is a msg.value with the
        // transaction then the user is sending the native token. In this case, the native token should be
        // wrapped.
        if (params.inputToken == address(wrappedNativeToken).toBytes32() && msg.value > 0) {
            if (msg.value != params.inputAmount) revert MsgValueDoesNotMatchInputAmount();
            wrappedNativeToken.deposit{ value: msg.value }();
            // Else, it is a normal ERC20. In this case pull the token from the caller as per normal.
            // Note: this includes the case where the L2 caller has WETH (already wrapped ETH) and wants to bridge them.
            // In this case the msg.value will be set to 0, indicating a "normal" ERC20 bridging action.
        } else {
            // msg.value should be 0 if input token isn't the wrapped native token.
            if (msg.value != 0) revert MsgValueDoesNotMatchInputAmount();
            IERC20Upgradeable(params.inputToken.toAddress()).safeTransferFrom(
                msg.sender,
                address(this),
                params.inputAmount
            );
        }

        emit FundsDeposited(
            params.inputToken,
            params.outputToken,
            params.inputAmount,
            params.outputAmount,
            params.destinationChainId,
            params.depositId,
            params.quoteTimestamp,
            params.fillDeadline,
            exclusivityDeadline,
            params.depositor,
            params.recipient,
            params.exclusiveRelayer,
            params.message
        );
    }

    function _deposit(
        address depositor,
        address recipient,
        address originToken,
        uint256 amount,
        uint256 destinationChainId,
        int64 relayerFeePct,
        uint32 quoteTimestamp,
        bytes memory message
    ) internal {
        // We limit the relay fees to prevent the user spending all their funds on fees.
        if (SignedMath.abs(relayerFeePct) >= 0.5e18) revert InvalidRelayerFeePct();
        if (amount > MAX_TRANSFER_SIZE) revert MaxTransferSizeExceeded();

        // Require that quoteTimestamp has a maximum age so that depositors pay an LP fee based on recent HubPool usage.
        // It is assumed that cross-chain timestamps are normally loosely in-sync, but clock drift can occur. If the
        // SpokePool time stalls or lags significantly, it is still possible to make deposits by setting quoteTimestamp
        // within the configured buffer. The owner should pause deposits if this is undesirable. This will underflow if
        // quoteTimestamp is more than depositQuoteTimeBuffer; this is safe but will throw an unintuitive error.

        // slither-disable-next-line timestamp
        if (getCurrentTime() - quoteTimestamp > depositQuoteTimeBuffer) revert InvalidQuoteTimestamp();

        // Increment count of deposits so that deposit ID for this spoke pool is unique.
        uint32 newDepositId = numberOfDeposits++;

        // If the address of the origin token is a wrappedNativeToken contract and there is a msg.value with the
        // transaction then the user is sending ETH. In this case, the ETH should be deposited to wrappedNativeToken.
        if (originToken == address(wrappedNativeToken) && msg.value > 0) {
            if (msg.value != amount) revert MsgValueDoesNotMatchInputAmount();
            wrappedNativeToken.deposit{ value: msg.value }();
            // Else, it is a normal ERC20. In this case pull the token from the user's wallet as per normal.
            // Note: this includes the case where the L2 user has WETH (already wrapped ETH) and wants to bridge them.
            // In this case the msg.value will be set to 0, indicating a "normal" ERC20 bridging action.
        } else {
            IERC20Upgradeable(originToken).safeTransferFrom(msg.sender, address(this), amount);
        }

        emit FundsDeposited(
            originToken.toBytes32(), // inputToken
            bytes32(0), // outputToken. Setting this to 0x0 means that the outputToken should be assumed to be the
            // canonical token for the destination chain matching the inputToken. Therefore, this deposit
            // can always be slow filled.
            // - setting token to 0x0 will signal to off-chain validator that the "equivalent"
            // token as the inputToken for the destination chain should be replaced here.
            amount, // inputAmount
            _computeAmountPostFees(amount, relayerFeePct), // outputAmount
            // - output amount will be the deposit amount less relayerFeePct, which should now be set
            // equal to realizedLpFeePct + gasFeePct + capitalCostFeePct where (gasFeePct + capitalCostFeePct)
            // is equal to the old usage of `relayerFeePct`.
            destinationChainId,
            newDepositId,
            quoteTimestamp,
            INFINITE_FILL_DEADLINE, // fillDeadline. Default to infinite expiry because
            // expired deposits refunds could be a breaking change for existing users of this function.
            0, // exclusivityDeadline. Setting this to 0 along with the exclusiveRelayer to 0x0 means that there
            // is no exclusive deadline
            depositor.toBytes32(),
            recipient.toBytes32(),
            bytes32(0), // exclusiveRelayer. Setting this to 0x0 will signal to off-chain validator that there
            // is no exclusive relayer.
            message
        );
    }

    function _distributeRelayerRefunds(
        uint256 _chainId,
        uint256 amountToReturn,
        uint256[] memory refundAmounts,
        uint32 leafId,
        address l2TokenAddress,
        address[] memory refundAddresses
    ) internal returns (bool deferredRefunds) {
        uint256 numRefunds = refundAmounts.length;
        if (refundAddresses.length != numRefunds) revert InvalidMerkleLeaf();

        if (numRefunds > 0) {
            uint256 spokeStartBalance = IERC20Upgradeable(l2TokenAddress).balanceOf(address(this));
            uint256 totalRefundedAmount = 0; // Track the total amount refunded.

            // Send each relayer refund address the associated refundAmount for the L2 token address.
            // Note: Even if the L2 token is not enabled on this spoke pool, we should still refund relayers.
            for (uint256 i = 0; i < numRefunds; ++i) {
                if (refundAmounts[i] > 0) {
                    totalRefundedAmount += refundAmounts[i];

                    // Only if the total refunded amount exceeds the spoke starting balance, should we revert. This
                    // ensures that bundles are atomic, if we have sufficient balance to refund all relayers and
                    // prevents can only re-pay some of the relayers.
                    if (totalRefundedAmount > spokeStartBalance) revert InsufficientSpokePoolBalanceToExecuteLeaf();

                    bool success = _noRevertTransfer(l2TokenAddress, refundAddresses[i], refundAmounts[i]);

                    // If the transfer failed then track a deferred transfer for the relayer. Given this function would
                    // have reverted if there was insufficient balance, this will only happen if the transfer call
                    // reverts. This will only occur if the underlying transfer method on the l2Token reverts due to
                    // recipient blacklisting or other related modifications to the l2Token.transfer method.
                    if (!success) {
                        relayerRefund[l2TokenAddress][refundAddresses[i]] += refundAmounts[i];
                        deferredRefunds = true;
                    }
                }
            }
        }
        // If leaf's amountToReturn is positive, then send L2 --> L1 message to bridge tokens back via
        // chain-specific bridging method.
        if (amountToReturn > 0) {
            _bridgeTokensToHubPool(amountToReturn, l2TokenAddress);

            emit TokensBridged(amountToReturn, _chainId, leafId, l2TokenAddress.toBytes32(), msg.sender);
        }
    }

    // Re-implementation of OZ _callOptionalReturnBool to use private logic. Function executes a transfer and returns a
    // bool indicating if the external call was successful, rather than reverting. Original method:
    // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/28aed34dc5e025e61ea0390c18cac875bfde1a78/contracts/token/ERC20/utils/SafeERC20.sol#L188
    function _noRevertTransfer(
        address token,
        address to,
        uint256 amount
    ) internal returns (bool) {
        bool success;
        uint256 returnSize;
        uint256 returnValue;
        bytes memory data = abi.encodeCall(IERC20Upgradeable.transfer, (to, amount));
        assembly {
            success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
            returnSize := returndatasize()
            returnValue := mload(0)
        }
        return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
    }

    function _setCrossDomainAdmin(address newCrossDomainAdmin) internal {
        if (newCrossDomainAdmin == address(0)) revert InvalidCrossDomainAdmin();
        crossDomainAdmin = newCrossDomainAdmin;
        emit SetXDomainAdmin(newCrossDomainAdmin);
    }

    function _setWithdrawalRecipient(address newWithdrawalRecipient) internal {
        if (newWithdrawalRecipient == address(0)) revert InvalidWithdrawalRecipient();
        withdrawalRecipient = newWithdrawalRecipient;
        emit SetWithdrawalRecipient(newWithdrawalRecipient);
    }

    function _preExecuteLeafHook(address) internal virtual {
        // This method by default is a no-op. Different child spoke pools might want to execute functionality here
        // such as wrapping any native tokens owned by the contract into wrapped tokens before proceeding with
        // executing the leaf.
    }

    // Should be overriden by implementing contract depending on how L2 handles sending tokens to L1.
    function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal virtual;

    function _setClaimedLeaf(uint32 rootBundleId, uint32 leafId) internal {
        RootBundle storage rootBundle = rootBundles[rootBundleId];

        // Verify the leafId in the leaf has not yet been claimed.
        if (MerkleLib.isClaimed(rootBundle.claimedBitmap, leafId)) revert ClaimedMerkleLeaf();

        // Set leaf as claimed in bitmap. This is passed by reference to the storage rootBundle.
        MerkleLib.setClaimed(rootBundle.claimedBitmap, leafId);
    }

    function _verifyUpdateV3DepositMessage(
        address depositor,
        uint256 depositId,
        uint256 originChainId,
        uint256 updatedOutputAmount,
        bytes32 updatedRecipient,
        bytes memory updatedMessage,
        bytes memory depositorSignature,
        bytes32 hashType
    ) internal view {
        // A depositor can request to modify an un-relayed deposit by signing a hash containing the updated
        // details and information uniquely identifying the deposit to relay. This information ensures
        // that this signature cannot be re-used for other deposits.
        bytes32 expectedTypedDataV4Hash = _hashTypedDataV4(
            keccak256(
                abi.encode(
                    hashType,
                    depositId,
                    originChainId,
                    updatedOutputAmount,
                    updatedRecipient,
                    keccak256(updatedMessage)
                )
            ),
            originChainId
        );
        _verifyDepositorSignature(depositor, expectedTypedDataV4Hash, depositorSignature);
    }

    // This function is isolated and made virtual to allow different L2's to implement chain specific recovery of
    // signers from signatures because some L2s might not support ecrecover. To be safe, consider always reverting
    // this function for L2s where ecrecover is different from how it works on Ethereum, otherwise there is the
    // potential to forge a signature from the depositor using a different private key than the original depositor's.
    function _verifyDepositorSignature(
        address depositor,
        bytes32 ethSignedMessageHash,
        bytes memory depositorSignature
    ) internal view virtual {
        // Note:
        // - We don't need to worry about re-entrancy from a contract deployed at the depositor address since the method
        //   `SignatureChecker.isValidSignatureNow` is a view method. Re-entrancy can happen, but it cannot affect state.
        // - EIP-1271 signatures are supported. This means that a signature valid now, may not be valid later and vice-versa.
        // - For an EIP-1271 signature to work, the depositor contract address must map to a deployed contract on the destination
        //   chain that can validate the signature.
        // - Regular signatures from an EOA are also supported.
        bool isValid = SignatureChecker.isValidSignatureNow(depositor, ethSignedMessageHash, depositorSignature);
        if (!isValid) revert InvalidDepositorSignature();
    }

    function _verifyV3SlowFill(
        V3RelayExecutionParams memory relayExecution,
        uint32 rootBundleId,
        bytes32[] memory proof
    ) internal view {
        V3SlowFill memory slowFill = V3SlowFill({
            relayData: relayExecution.relay,
            chainId: chainId(),
            updatedOutputAmount: relayExecution.updatedOutputAmount
        });

        if (!MerkleLib.verifyV3SlowRelayFulfillment(rootBundles[rootBundleId].slowRelayRoot, slowFill, proof)) {
            revert InvalidMerkleProof();
        }
    }

    function _computeAmountPostFees(uint256 amount, int256 feesPct) private pure returns (uint256) {
        return (amount * uint256(int256(1e18) - feesPct)) / 1e18;
    }

    // Unwraps ETH and does a transfer to a recipient address. If the recipient is a smart contract then sends wrappedNativeToken.
    function _unwrapwrappedNativeTokenTo(address payable to, uint256 amount) internal {
        if (address(to).isContract()) {
            IERC20Upgradeable(address(wrappedNativeToken)).safeTransfer(to, amount);
        } else {
            wrappedNativeToken.withdraw(amount);
            AddressLibUpgradeable.sendValue(to, amount);
        }
    }

    // @param relayer: relayer who is actually credited as filling this deposit. Can be different from
    // exclusiveRelayer if passed exclusivityDeadline or if slow fill.
    function _fillRelayV3(
        V3RelayExecutionParams memory relayExecution,
        bytes32 relayer,
        bool isSlowFill
    ) internal {
        V3RelayData memory relayData = relayExecution.relay;

        if (relayData.fillDeadline < getCurrentTime()) revert ExpiredFillDeadline();

        bytes32 relayHash = relayExecution.relayHash;

        // If a slow fill for this fill was requested then the relayFills value for this hash will be
        // FillStatus.RequestedSlowFill. Therefore, if this is the status, then this fast fill
        // will be replacing the slow fill. If this is a slow fill execution, then the following variable
        // is trivially true. We'll emit this value in the FilledRelay
        // event to assist the Dataworker in knowing when to return funds back to the HubPool that can no longer
        // be used for a slow fill execution.
        FillType fillType = isSlowFill
            ? FillType.SlowFill // The following is true if this is a fast fill that was sent after a slow fill request.
            : (
                fillStatuses[relayExecution.relayHash] == uint256(FillStatus.RequestedSlowFill)
                    ? FillType.ReplacedSlowFill
                    : FillType.FastFill
            );

        // @dev This function doesn't support partial fills. Therefore, we associate the relay hash with
        // an enum tracking its fill status. All filled relays, whether slow or fast fills, are set to the Filled
        // status. However, we also use this slot to track whether this fill had a slow fill requested. Therefore
        // we can include a bool in the FilledRelay event making it easy for the dataworker to compute if this
        // fill was a fast fill that replaced a slow fill and therefore this SpokePool has excess funds that it
        // needs to send back to the HubPool.
        if (fillStatuses[relayHash] == uint256(FillStatus.Filled)) revert RelayFilled();
        fillStatuses[relayHash] = uint256(FillStatus.Filled);

        // @dev Before returning early, emit events to assist the dataworker in being able to know which fills were
        // successful.
        emit FilledRelay(
            relayData.inputToken,
            relayData.outputToken,
            relayData.inputAmount,
            relayData.outputAmount,
            relayExecution.repaymentChainId,
            relayData.originChainId,
            relayData.depositId,
            relayData.fillDeadline,
            relayData.exclusivityDeadline,
            relayData.exclusiveRelayer,
            relayer,
            relayData.depositor,
            relayData.recipient,
            _hashNonEmptyMessage(relayData.message),
            V3RelayExecutionEventInfo({
                updatedRecipient: relayExecution.updatedRecipient,
                updatedMessageHash: _hashNonEmptyMessage(relayExecution.updatedMessage),
                updatedOutputAmount: relayExecution.updatedOutputAmount,
                fillType: fillType
            })
        );

        address outputToken = relayData.outputToken.toAddress();
        uint256 amountToSend = relayExecution.updatedOutputAmount;
        address recipientToSend = relayExecution.updatedRecipient.toAddress();
        // If relay token is wrappedNativeToken then unwrap and send native token.
        // Stack too deep.
        if (relayData.outputToken.toAddress() == address(wrappedNativeToken)) {
            // Note: useContractFunds is True if we want to send funds to the recipient directly out of this contract,
            // otherwise we expect the caller to send funds to the recipient. If useContractFunds is True and the
            // recipient wants wrappedNativeToken, then we can assume that wrappedNativeToken is already in the
            // contract, otherwise we'll need the user to send wrappedNativeToken to this contract. Regardless, we'll
            // need to unwrap it to native token before sending to the user.
            if (!isSlowFill) IERC20Upgradeable(outputToken).safeTransferFrom(msg.sender, address(this), amountToSend);
            _unwrapwrappedNativeTokenTo(payable(recipientToSend), amountToSend);
            // Else, this is a normal ERC20 token. Send to recipient.
        } else {
            // Note: Similar to note above, send token directly from the contract to the user in the slow relay case.
            if (!isSlowFill) IERC20Upgradeable(outputToken).safeTransferFrom(msg.sender, recipientToSend, amountToSend);
            else IERC20Upgradeable(outputToken).safeTransfer(recipientToSend, amountToSend);
        }

        bytes memory updatedMessage = relayExecution.updatedMessage;
        if (updatedMessage.length > 0 && recipientToSend.isContract()) {
            AcrossMessageHandler(recipientToSend).handleV3AcrossMessage(
                outputToken,
                amountToSend,
                msg.sender,
                updatedMessage
            );
        }
    }

    // Determine whether the exclusivityDeadline implies active exclusivity.
    function _fillIsExclusive(uint32 exclusivityDeadline, uint32 currentTime) internal pure returns (bool) {
        return exclusivityDeadline >= currentTime;
    }

    // Helper for emitting message hash. For easier easier human readability we return bytes32(0) for empty message.
    function _hashNonEmptyMessage(bytes memory message) internal pure returns (bytes32) {
        if (message.length == 0) return bytes32(0);
        else return keccak256(message);
    }

    function _setOftMessenger(address _token, address _messenger) internal {
        if (IOFT(_messenger).token() != _token) {
            revert OFTTokenMismatch();
        }
        oftMessengers[_token] = _messenger;
        emit SetOFTMessenger(_token, _messenger);
    }

    function _getOftMessenger(address _token) internal view returns (address) {
        return oftMessengers[_token];
    }

    // Implementing contract needs to override this to ensure that only the appropriate cross chain admin can execute
    // certain admin functions. For L2 contracts, the cross chain admin refers to some L1 address or contract, and for
    // L1, this would just be the same admin of the HubPool.
    function _requireAdminSender() internal virtual;

    // Added to enable the this contract to receive native token (ETH). Used when unwrapping wrappedNativeToken.
    receive() external payable {}

    // Reserve storage slots for future versions of this base contract to add state variables without
    // affecting the storage layout of child contracts. Decrement the size of __gap whenever state variables
    // are added. This is at bottom of contract to make sure it's always at the end of storage.
    uint256[997] private __gap;
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IOFT, SendParam, MessagingFee, OFTReceipt } from "../interfaces/IOFT.sol";
import { AddressToBytes32 } from "../libraries/AddressConverters.sol";

/**
 * @notice Facilitate bridging tokens via LayerZero's OFT.
 * @dev This contract is intended to be inherited by other chain-specific adapters and spoke pools.
 * @custom:security-contact bugs@across.to
 */
contract OFTTransportAdapter {
    using SafeERC20 for IERC20;
    using AddressToBytes32 for address;

    bytes public constant EMPTY_MSG_BYTES = new bytes(0);

    /**
     * @dev a fee cap we check against before sending a message with value to OFTMessenger as fees.
     * @dev this cap should be pretty conservative (high) to not interfere with operations under normal conditions.
     */
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
    uint256 public immutable OFT_FEE_CAP;

    /**
     * @notice The destination endpoint id in the OFT messaging protocol.
     * @dev Source https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts.
     */
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
    uint32 public immutable OFT_DST_EID;

    error OftFeeCapExceeded();
    error OftInsufficientBalanceForFee();
    error OftIncorrectAmountReceivedLD();

    /**
     * @notice intiailizes the OFTTransportAdapter contract.
     * @param _oftDstEid the endpoint ID that OFT protocol will transfer funds to.
     * @param _feeCap a fee cap we check against before sending a message with value to OFTMessenger as fees.
     */
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor(uint32 _oftDstEid, uint256 _feeCap) {
        OFT_DST_EID = _oftDstEid;
        OFT_FEE_CAP = _feeCap;
    }

    /**
     * @notice transfer token to the other dstEid (e.g. chain) via OFT messaging protocol
     * @dev the caller has to provide both _token and _messenger. The caller is responsible for knowing the correct _messenger
     * @param _token token we're sending on current chain.
     * @param _messenger corresponding OFT messenger on current chain.
     * @param _to address to receive a trasnfer on the destination chain.
     * @param _amount amount to send.
     */
    function _transferViaOFT(
        IERC20 _token,
        IOFT _messenger,
        address _to,
        uint256 _amount
    ) internal {
        bytes32 to = _to.toBytes32();

        SendParam memory sendParam = SendParam(
            OFT_DST_EID,
            to,
            /**
             * _amount, _amount here specify `amountLD` and `minAmountLD`. Setting `minAmountLD` equal to `amountLD` protects us
             * from any changes to the sent amount due to internal OFT contract logic, e.g. `_removeDust`. Meaning that if any
             * dust is subtracted, the `.send()` should revert
             */
            _amount,
            _amount,
            /**
             * EMPTY_MSG_BYTES, EMPTY_MSG_BYTES, EMPTY_MSG_BYTES here specify `extraOptions`, `composeMsg` and `oftCmd`.
             * These can be set to empty bytes arrays for the purposes of sending a simple cross-chain transfer.
             */
            EMPTY_MSG_BYTES,
            EMPTY_MSG_BYTES,
            EMPTY_MSG_BYTES
        );

        // `false` in the 2nd param here refers to `bool _payInLzToken`. We will pay in native token, so set to `false`
        MessagingFee memory fee = _messenger.quoteSend(sendParam, false);
        // Create a stack variable to optimize gas usage on subsequent reads
        uint256 nativeFee = fee.nativeFee;
        if (nativeFee > OFT_FEE_CAP) revert OftFeeCapExceeded();
        if (nativeFee > address(this).balance) revert OftInsufficientBalanceForFee();

        // Approve the exact _amount for `_messenger` to spend. Fee will be paid in native token
        _token.forceApprove(address(_messenger), _amount);

        (, OFTReceipt memory oftReceipt) = _messenger.send{ value: nativeFee }(sendParam, fee, address(this));

        // The HubPool expects that the amount received by the SpokePool is exactly the sent amount
        if (_amount != oftReceipt.amountReceivedLD) revert OftIncorrectAmountReceivedLD();
    }
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import "./MerkleLib.sol";
import "./interfaces/HubPoolInterface.sol";
import "./Lockable.sol";

import "./interfaces/LpTokenFactoryInterface.sol";
import "./external/interfaces/WETH9Interface.sol";

import "@uma/core/contracts/common/implementation/Testable.sol";
import "@uma/core/contracts/common/implementation/MultiCaller.sol";
import "@uma/core/contracts/common/interfaces/AddressWhitelistInterface.sol";

import "@uma/core/contracts/data-verification-mechanism/interfaces/FinderInterface.sol";
import "@uma/core/contracts/data-verification-mechanism/interfaces/IdentifierWhitelistInterface.sol";
import "@uma/core/contracts/data-verification-mechanism/interfaces/StoreInterface.sol";
import "@uma/core/contracts/data-verification-mechanism/implementation/Constants.sol";

import "@uma/core/contracts/optimistic-oracle-v2/interfaces/SkinnyOptimisticOracleInterface.sol";
import "@uma/core/contracts/common/interfaces/ExpandedIERC20.sol";

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";

/**
 * @notice Contract deployed on Ethereum that houses L1 token liquidity for all SpokePools. A dataworker can interact
 * with merkle roots stored in this contract via inclusion proofs to instruct this contract to send tokens to L2
 * SpokePools via "pool rebalances" that can be used to pay out relayers on those networks. This contract is also
 * responsible for publishing relayer refund and slow relay merkle roots to SpokePools.
 * @notice This contract is meant to act as the cross chain administrator and owner of all L2 spoke pools, so all
 * governance actions and pool rebalances originate from here and bridge instructions to L2s.
 * @dev This contract should be deprecated by the year 2106, at which point uint32 timestamps will roll over. This is
 * an issue for this contract because fee calculations will become bizarre when multiplying by negative time deltas.
 * Before this date, this contract should be paused from accepting new root bundles and all LP tokens should be
 * disabled by the admin.
 * @custom:security-contact bugs@across.to
 */
contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable {
    using SafeERC20 for IERC20;
    using Address for address;

    // Only one root bundle can be stored at a time. Once all pool rebalance leaves are executed, a new proposal
    // can be submitted.
    RootBundle public rootBundleProposal;

    // Mapping of L1 token addresses to the associated pool information.
    mapping(address => PooledToken) public pooledTokens;

    // Stores paths from L1 token + destination ID to destination token. Since different tokens on L1 might map to
    // to the same address on different destinations, we hash (L1 token address, destination ID) to
    // use as a key that maps to a destination token. This mapping is used to direct pool rebalances from
    // HubPool to SpokePool, and also is designed to be used as a lookup for off-chain data workers to determine
    // which L1 tokens to relay to SpokePools to refund relayers. The admin can set the "destination token"
    // to 0x0 to disable a pool rebalance route and block executeRootBundle() from executing.
    mapping(bytes32 => address) private poolRebalanceRoutes;

    // Mapping of chainId to the associated adapter and spokePool contracts.
    mapping(uint256 => CrossChainContract) public crossChainContracts;

    mapping(address => uint256) public unclaimedAccumulatedProtocolFees;

    // Whether the bundle proposal process is paused.
    bool public paused;

    // WETH contract for Ethereum.
    WETH9Interface public immutable weth;

    // Helper factory to deploy new LP tokens for enabled L1 tokens
    LpTokenFactoryInterface public immutable lpTokenFactory;

    // Finder contract for this network.
    FinderInterface public immutable finder;

    // Address that captures protocol fees. Accumulated protocol fees can be claimed by this address.
    address public protocolFeeCaptureAddress;

    // Token used to bond the data worker for proposing relayer refund bundles.
    IERC20 public bondToken;

    // Each root bundle proposal must stay in liveness for this period of time before it can be considered finalized.
    // It can be disputed only during this period of time. Defaults to 2 hours, like the rest of the UMA ecosystem.
    uint32 public liveness = 7200;

    // When root bundles are disputed a price request is enqueued with the DVM to resolve the resolution.
    bytes32 public identifier = "ACROSS-V2";

    // Interest rate payment that scales the amount of pending fees per second paid to LPs. 0.0000015e18 will pay out
    // the full amount of fees entitled to LPs in ~ 7.72 days assuming no contract interactions. If someone interacts
    // with the contract then the LP rewards are smeared sublinearly over the window (i.e spread over the remaining
    // period for each interaction which approximates a decreasing exponential function).
    uint256 public lpFeeRatePerSecond = 1500000000000;

    // Percentage of lpFees that are captured by the protocol and claimable by the protocolFeeCaptureAddress.
    uint256 public protocolFeeCapturePct;

    // The computed bond amount as the UMA Store's final fee multiplied by the bondTokenFinalFeeMultiplier.
    uint256 public bondAmount;

    event Paused(bool indexed isPaused);

    event EmergencyRootBundleDeleted(
        bytes32 indexed poolRebalanceRoot,
        bytes32 indexed relayerRefundRoot,
        bytes32 slowRelayRoot,
        address indexed proposer
    );

    event ProtocolFeeCaptureSet(address indexed newProtocolFeeCaptureAddress, uint256 indexed newProtocolFeeCapturePct);

    event ProtocolFeesCapturedClaimed(address indexed l1Token, uint256 indexed accumulatedFees);

    event BondSet(address indexed newBondToken, uint256 newBondAmount);

    event LivenessSet(uint256 newLiveness);

    event IdentifierSet(bytes32 newIdentifier);

    event CrossChainContractsSet(uint256 l2ChainId, address adapter, address spokePool);

    event L1TokenEnabledForLiquidityProvision(address l1Token, address lpToken);

    event L2TokenDisabledForLiquidityProvision(address l1Token, address lpToken);

    event LiquidityAdded(
        address indexed l1Token,
        uint256 amount,
        uint256 lpTokensMinted,
        address indexed liquidityProvider
    );
    event LiquidityRemoved(
        address indexed l1Token,
        uint256 amount,
        uint256 lpTokensBurnt,
        address indexed liquidityProvider
    );
    event SetPoolRebalanceRoute(
        uint256 indexed destinationChainId,
        address indexed l1Token,
        address indexed destinationToken
    );
    event SetEnableDepositRoute(
        uint256 indexed originChainId,
        uint256 indexed destinationChainId,
        address indexed originToken,
        bool depositsEnabled
    );
    event ProposeRootBundle(
        uint32 challengePeriodEndTimestamp,
        uint8 poolRebalanceLeafCount,
        uint256[] bundleEvaluationBlockNumbers,
        bytes32 indexed poolRebalanceRoot,
        bytes32 indexed relayerRefundRoot,
        bytes32 slowRelayRoot,
        address indexed proposer
    );
    event RootBundleExecuted(
        uint256 groupIndex,
        uint256 indexed leafId,
        uint256 indexed chainId,
        address[] l1Tokens,
        uint256[] bundleLpFees,
        int256[] netSendAmounts,
        int256[] runningBalances,
        address indexed caller
    );
    event SpokePoolAdminFunctionTriggered(uint256 indexed chainId, bytes message);

    event RootBundleDisputed(address indexed disputer, uint256 requestTime);

    event RootBundleCanceled(address indexed disputer, uint256 requestTime);

    modifier noActiveRequests() {
        require(!_activeRequest(), "Proposal has unclaimed leaves");
        _;
    }

    modifier unpaused() {
        require(!paused, "Contract is paused");
        _;
    }

    modifier zeroOptimisticOracleApproval() {
        _;
        bondToken.safeApprove(address(_getOptimisticOracle()), 0);
    }

    /**
     * @notice Construct HubPool.
     * @param _lpTokenFactory LP Token factory address used to deploy LP tokens for new collateral types.
     * @param _finder Finder address.
     * @param _weth WETH address.
     * @param _timer Timer address.
     */
    constructor(
        LpTokenFactoryInterface _lpTokenFactory,
        FinderInterface _finder,
        WETH9Interface _weth,
        address _timer
    ) Testable(_timer) {
        lpTokenFactory = _lpTokenFactory;
        finder = _finder;
        weth = _weth;
        protocolFeeCaptureAddress = owner();
    }

    /*************************************************
     *                ADMIN FUNCTIONS                *
     *************************************************/

    /**
     * @notice Pauses the bundle proposal and execution process. This is intended to be used during upgrades or when
     * something goes awry.
     * @param pause true if the call is meant to pause the system, false if the call is meant to unpause it.
     */
    function setPaused(bool pause) public onlyOwner nonReentrant {
        paused = pause;
        emit Paused(pause);
    }

    /**
     * @notice This allows for the deletion of the active proposal in case of emergency.
     * @dev This is primarily intended to rectify situations where an unexecutable bundle gets through liveness in the
     * case of a non-malicious bug in the proposal/dispute code. Without this function, the contract would be
     * indefinitely blocked, migration would be required, and in-progress transfers would never be repaid.
     */
    function emergencyDeleteProposal() public onlyOwner nonReentrant {
        RootBundle memory _rootBundleProposal = rootBundleProposal;
        delete rootBundleProposal;
        if (_rootBundleProposal.unclaimedPoolRebalanceLeafCount > 0)
            bondToken.safeTransfer(_rootBundleProposal.proposer, bondAmount);
        emit EmergencyRootBundleDeleted(
            _rootBundleProposal.poolRebalanceRoot,
            _rootBundleProposal.relayerRefundRoot,
            _rootBundleProposal.slowRelayRoot,
            _rootBundleProposal.proposer
        );
    }

    /**
     * @notice Sends message to SpokePool from this contract. Callable only by owner.
     * @dev This function has permission to call onlyAdmin functions on the SpokePool, so it's imperative that this
     * contract only allows the owner to call this method directly or indirectly.
     * @param chainId Chain with SpokePool to send message to.
     * @param functionData ABI encoded function call to send to SpokePool, but can be any arbitrary data technically.
     */
    function relaySpokePoolAdminFunction(uint256 chainId, bytes memory functionData)
        public
        override
        onlyOwner
        nonReentrant
    {
        _relaySpokePoolAdminFunction(chainId, functionData);
    }

    /**
     * @notice Sets protocolFeeCaptureAddress and protocolFeeCapturePct. Callable only by owner.
     * @param newProtocolFeeCaptureAddress New protocol fee capture address.
     * @param newProtocolFeeCapturePct New protocol fee capture %.
     */
    function setProtocolFeeCapture(address newProtocolFeeCaptureAddress, uint256 newProtocolFeeCapturePct)
        public
        override
        onlyOwner
        nonReentrant
    {
        require(newProtocolFeeCapturePct <= 1e18, "Bad protocolFeeCapturePct");
        require(newProtocolFeeCaptureAddress != address(0), "Bad protocolFeeCaptureAddress");
        protocolFeeCaptureAddress = newProtocolFeeCaptureAddress;
        protocolFeeCapturePct = newProtocolFeeCapturePct;
        emit ProtocolFeeCaptureSet(newProtocolFeeCaptureAddress, newProtocolFeeCapturePct);
    }

    /**
     * @notice Sets bond token and amount. Callable only by owner.
     * @param newBondToken New bond currency.
     * @param newBondAmount New bond amount.
     */
    function setBond(IERC20 newBondToken, uint256 newBondAmount)
        public
        override
        onlyOwner
        noActiveRequests
        nonReentrant
    {
        // Bond should not equal final fee otherwise every proposal will get cancelled in a dispute.
        // In practice we expect that bond amounts are set >> final fees so this shouldn't be an inconvenience.
        // The only way for the bond amount to be equal to the final fee is if the newBondAmount == 0.
        require(newBondAmount != 0, "bond equal to final fee");

        // Check that this token is on the whitelist.
        AddressWhitelistInterface addressWhitelist = AddressWhitelistInterface(
            finder.getImplementationAddress(OracleInterfaces.CollateralWhitelist)
        );
        require(addressWhitelist.isOnWhitelist(address(newBondToken)), "Not on whitelist");

        // The bond should be the passed in bondAmount + the final fee.
        bondToken = newBondToken;
        uint256 _bondAmount = newBondAmount + _getBondTokenFinalFee();
        bondAmount = _bondAmount;
        emit BondSet(address(newBondToken), _bondAmount);
    }

    /**
     * @notice Sets root bundle proposal liveness period. Callable only by owner.
     * @param newLiveness New liveness period.
     */
    function setLiveness(uint32 newLiveness) public override onlyOwner nonReentrant {
        require(newLiveness > 10 minutes, "Liveness too short");
        liveness = newLiveness;
        emit LivenessSet(newLiveness);
    }

    /**
     * @notice Sets identifier for root bundle disputes. Callable only by owner.
     * @param newIdentifier New identifier.
     */
    function setIdentifier(bytes32 newIdentifier) public override onlyOwner noActiveRequests nonReentrant {
        IdentifierWhitelistInterface identifierWhitelist = IdentifierWhitelistInterface(
            finder.getImplementationAddress(OracleInterfaces.IdentifierWhitelist)
        );
        require(identifierWhitelist.isIdentifierSupported(newIdentifier), "Identifier not supported");
        identifier = newIdentifier;
        emit IdentifierSet(newIdentifier);
    }

    /**
     * @notice Sets cross chain relay helper contracts for L2 chain ID. Callable only by owner.
     * @dev We do not block setting the adapter or SpokePool to invalid/zero addresses because we want to allow the
     * admin to block relaying roots to the spoke pool for emergency recovery purposes.
     * @param l2ChainId Chain to set contracts for.
     * @param adapter Adapter used to relay messages and tokens to spoke pool. Deployed on current chain.
     * @param spokePool Recipient of relayed messages and tokens on spoke pool. Deployed on l2ChainId.
     */

    function setCrossChainContracts(
        uint256 l2ChainId,
        address adapter,
        address spokePool
    ) public override onlyOwner nonReentrant {
        crossChainContracts[l2ChainId] = CrossChainContract(adapter, spokePool);
        emit CrossChainContractsSet(l2ChainId, adapter, spokePool);
    }

    /**
     * @notice Store canonical destination token counterpart for l1 token. Callable only by owner.
     * @dev Admin can set destinationToken to 0x0 to effectively disable executing any root bundles with leaves
     * containing this l1 token + destination chain ID combination.
     * @param destinationChainId Destination chain where destination token resides.
     * @param l1Token Token enabled for liquidity in this pool, and the L1 counterpart to the destination token on the
     * destination chain ID.
     * @param destinationToken Destination chain counterpart of L1 token.
     */
    function setPoolRebalanceRoute(
        uint256 destinationChainId,
        address l1Token,
        address destinationToken
    ) public override onlyOwner nonReentrant {
        poolRebalanceRoutes[_poolRebalanceRouteKey(l1Token, destinationChainId)] = destinationToken;
        emit SetPoolRebalanceRoute(destinationChainId, l1Token, destinationToken);
    }

    /**
     * @notice Sends cross-chain message to SpokePool on originChainId to enable or disable deposit route from that
     * SpokePool to another one. Callable only by owner.
     * @dev Admin is responsible for ensuring that `originToken` is linked to some L1 token on this contract, via
     * poolRebalanceRoutes(), and that this L1 token also has a counterpart on the destination chain. If either
     * condition fails, then the deposit will be unrelayable by off-chain relayers because they will not know which
     * token to relay to recipients on the destination chain, and data workers wouldn't know which L1 token to send
     * to the destination chain to refund the relayer.
     * @param originChainId Chain where token deposit occurs.
     * @param destinationChainId Chain where token depositor wants to receive funds.
     * @param originToken Token sent in deposit.
     * @param depositsEnabled Set to true to whitelist this route for deposits, set to false if caller just wants to
     * map the origin token + destination ID to the destination token address on the origin chain's SpokePool.
     */
    function setDepositRoute(
        uint256 originChainId,
        uint256 destinationChainId,
        address originToken,
        bool depositsEnabled
    ) public override nonReentrant onlyOwner {
        _relaySpokePoolAdminFunction(
            originChainId,
            abi.encodeWithSignature(
                "setEnableRoute(address,uint256,bool)",
                originToken,
                destinationChainId,
                depositsEnabled
            )
        );
        emit SetEnableDepositRoute(originChainId, destinationChainId, originToken, depositsEnabled);
    }

    /**
     * @notice Enables LPs to provide liquidity for L1 token. Deploys new LP token for L1 token if appropriate.
     * Callable only by owner.
     * @param l1Token Token to provide liquidity for.
     */
    function enableL1TokenForLiquidityProvision(address l1Token) public override onlyOwner nonReentrant {
        // If token is being enabled for the first time, create a new LP token and set the timestamp once. We don't
        // want to ever reset this timestamp otherwise fees that have accrued will be lost since the last update. This
        // could happen for example if an L1 token is enabled, disabled, and then enabled again.
        if (pooledTokens[l1Token].lpToken == address(0)) {
            pooledTokens[l1Token].lpToken = lpTokenFactory.createLpToken(l1Token);
            pooledTokens[l1Token].lastLpFeeUpdate = uint32(getCurrentTime());
        }

        pooledTokens[l1Token].isEnabled = true;

        emit L1TokenEnabledForLiquidityProvision(l1Token, pooledTokens[l1Token].lpToken);
    }

    /**
     * @notice Disables LPs from providing liquidity for L1 token. Callable only by owner.
     * @param l1Token Token to disable liquidity provision for.
     */
    function disableL1TokenForLiquidityProvision(address l1Token) public override onlyOwner nonReentrant {
        pooledTokens[l1Token].isEnabled = false;
        emit L2TokenDisabledForLiquidityProvision(l1Token, pooledTokens[l1Token].lpToken);
    }

    /**
     * @notice Enables the owner of the protocol to haircut reserves in the event of an irrecoverable loss of funds on
     * one of the L2s. Consider funds are lent out onto a L2 that dies irrecoverably. This value will offset the
     * exchangeRateCurrent such that all LPs receive a pro rata loss of the reserves. Should be used in conjunction
     * with pause logic to prevent LPs from adding/withdrawing liquidity during the haircut process.
     * Callable only by owner.
     * @param l1Token Token to execute the haircut on.
     * @param haircutAmount The amount of reserves to haircut the LPs by.
     */
    function haircutReserves(address l1Token, int256 haircutAmount) public onlyOwner nonReentrant {
        // Note that we do not call sync first in this method. The Owner should call this manually before haircutting.
        // This is done in the event sync reverts due to too low balance in the contract relative to bond amount.
        pooledTokens[l1Token].utilizedReserves -= haircutAmount;
    }

    /*************************************************
     *          LIQUIDITY PROVIDER FUNCTIONS         *
     *************************************************/

    /**
     * @notice Deposit liquidity into this contract to earn LP fees in exchange for funding relays on SpokePools.
     * Caller is essentially loaning their funds to be sent from this contract to the SpokePool, where it will be used
     * to repay a relayer, and ultimately receives their loan back after the tokens are bridged back to this contract
     * via the canonical token bridge. Then, the caller's loans are used again. This loan cycle repeats continuously
     * and the caller, or "liquidity provider" earns a continuous fee for their credit that they are extending relayers.
     * @notice Caller will receive an LP token representing their share of this pool. The LP token's redemption value
     * increments from the time that they enter the pool to reflect their accrued fees.
     * @notice The caller of this function must approve this contract to spend l1TokenAmount of l1Token.
     * @param l1Token Token to deposit into this contract.
     * @param l1TokenAmount Amount of liquidity to provide.
     */
    function addLiquidity(address l1Token, uint256 l1TokenAmount) public payable override nonReentrant unpaused {
        require(pooledTokens[l1Token].isEnabled, "Token not enabled");
        // If this is the weth pool and the caller sends msg.value then the msg.value must match the l1TokenAmount.
        // Else, msg.value must be set to 0.
        require(((address(weth) == l1Token) && msg.value == l1TokenAmount) || msg.value == 0, "Bad msg.value");

        // Since _exchangeRateCurrent() reads this contract's balance and updates contract state using it, it must be
        // first before transferring any tokens to this contract to ensure synchronization.
        uint256 lpTokensToMint = (l1TokenAmount * 1e18) / _exchangeRateCurrent(l1Token);
        pooledTokens[l1Token].liquidReserves += l1TokenAmount;
        ExpandedIERC20(pooledTokens[l1Token].lpToken).mint(msg.sender, lpTokensToMint);

        if (address(weth) == l1Token && msg.value > 0) WETH9Interface(address(l1Token)).deposit{ value: msg.value }();
        else IERC20(l1Token).safeTransferFrom(msg.sender, address(this), l1TokenAmount);

        emit LiquidityAdded(l1Token, l1TokenAmount, lpTokensToMint, msg.sender);
    }

    /**
     * @notice Burns LP share to redeem for underlying l1Token original deposit amount plus fees.
     * @param l1Token Token to redeem LP share for.
     * @param lpTokenAmount Amount of LP tokens to burn. Exchange rate between L1 token and LP token can be queried
     * via public exchangeRateCurrent method.
     * @param sendEth Set to True if L1 token is WETH and user wants to receive ETH. Note that if caller
     * is a contract, then the contract should have a way to receive ETH if this value is set to True. Similarly,
     * if this value is set to False, then the calling contract should have a way to handle WETH.
     */
    function removeLiquidity(
        address l1Token,
        uint256 lpTokenAmount,
        bool sendEth
    ) public override nonReentrant unpaused {
        require(address(weth) == l1Token || !sendEth, "Cant send eth");
        uint256 l1TokensToReturn = (lpTokenAmount * _exchangeRateCurrent(l1Token)) / 1e18;

        ExpandedIERC20(pooledTokens[l1Token].lpToken).burnFrom(msg.sender, lpTokenAmount);
        // Note this method does not make any liquidity utilization checks before letting the LP redeem their LP tokens.
        // If they try access more funds than available (i.e l1TokensToReturn > liquidReserves) this will underflow.
        pooledTokens[l1Token].liquidReserves -= l1TokensToReturn;

        if (sendEth) {
            weth.withdraw(l1TokensToReturn);
            Address.sendValue(payable(msg.sender), l1TokensToReturn); // This will revert if the caller is a contract that does not implement a fallback function.
        } else {
            IERC20(address(l1Token)).safeTransfer(msg.sender, l1TokensToReturn);
        }
        emit LiquidityRemoved(l1Token, l1TokensToReturn, lpTokenAmount, msg.sender);
    }

    /**
     * @notice Returns exchange rate of L1 token to LP token.
     * @param l1Token L1 token redeemable by burning LP token.
     * @return Amount of L1 tokens redeemable for 1 unit LP token.
     */
    function exchangeRateCurrent(address l1Token) public override nonReentrant returns (uint256) {
        return _exchangeRateCurrent(l1Token);
    }

    /**
     * @notice Returns % of liquid reserves currently being "used" and sitting in SpokePools.
     * @param l1Token L1 token to query utilization for.
     * @return % of liquid reserves currently being "used" and sitting in SpokePools.
     */
    function liquidityUtilizationCurrent(address l1Token) public override nonReentrant returns (uint256) {
        return _liquidityUtilizationPostRelay(l1Token, 0);
    }

    /**
     * @notice Returns % of liquid reserves currently being "used" and sitting in SpokePools and accounting for
     * relayedAmount of tokens to be withdrawn from the pool.
     * @param l1Token L1 token to query utilization for.
     * @param relayedAmount The higher this amount, the higher the utilization.
     * @return % of liquid reserves currently being "used" and sitting in SpokePools plus the relayedAmount.
     */
    function liquidityUtilizationPostRelay(address l1Token, uint256 relayedAmount)
        public
        nonReentrant
        returns (uint256)
    {
        return _liquidityUtilizationPostRelay(l1Token, relayedAmount);
    }

    /**
     * @notice Synchronize any balance changes in this contract with the utilized & liquid reserves. This should be done
     * at the conclusion of a L2->L1 token transfer via the canonical token bridge, when this contract's reserves do not
     * reflect its true balance due to new tokens being dropped onto the contract at the conclusion of a bridging action.
     */
    function sync(address l1Token) public override nonReentrant {
        _sync(l1Token);
    }

    /*************************************************
     *             DATA WORKER FUNCTIONS             *
     *************************************************/

    /**
     * @notice Publish a new root bundle along with all of the block numbers that the merkle roots are relevant for.
     * This is used to aid off-chain validators in evaluating the correctness of this bundle. Caller stakes a bond that
     * can be slashed if the root bundle proposal is invalid, and they will receive it back if accepted.
     * @notice After proposeRootBundle is called, if the any props are wrong then this proposal can be challenged.
     * Once the challenge period passes, then the roots are no longer disputable, and only executeRootBundle can be
     * called; moreover, this method can't be called again until all leaves are executed.
     * @param bundleEvaluationBlockNumbers should contain the latest block number for all chains, even if there are no
     * relays contained on some of them. The usage of this variable should be defined in an off chain UMIP.
     * @notice The caller of this function must approve this contract to spend bondAmount of bondToken.
     * @param poolRebalanceLeafCount Number of leaves contained in pool rebalance root. Max is # of whitelisted chains.
     * @param poolRebalanceRoot Pool rebalance root containing leaves that sends tokens from this contract to SpokePool.
     * @param relayerRefundRoot Relayer refund root to publish to SpokePool where a data worker can execute leaves to
     * refund relayers on their chosen refund chainId.
     * @param slowRelayRoot Slow relay root to publish to Spoke Pool where a data worker can execute leaves to
     * fulfill slow relays.
     */
    function proposeRootBundle(
        uint256[] calldata bundleEvaluationBlockNumbers,
        uint8 poolRebalanceLeafCount,
        bytes32 poolRebalanceRoot,
        bytes32 relayerRefundRoot,
        bytes32 slowRelayRoot
    ) public override nonReentrant noActiveRequests unpaused {
        // Note: this is to prevent "empty block" style attacks where someone can make empty proposals that are
        // technically valid but not useful. This could also potentially be enforced at the UMIP-level.
        require(poolRebalanceLeafCount > 0, "Bundle must have at least 1 leaf");

        uint32 challengePeriodEndTimestamp = uint32(getCurrentTime()) + liveness;

        delete rootBundleProposal; // Only one bundle of roots can be executed at a time. Delete the previous bundle.

        rootBundleProposal.challengePeriodEndTimestamp = challengePeriodEndTimestamp;
        rootBundleProposal.unclaimedPoolRebalanceLeafCount = poolRebalanceLeafCount;
        rootBundleProposal.poolRebalanceRoot = poolRebalanceRoot;
        rootBundleProposal.relayerRefundRoot = relayerRefundRoot;
        rootBundleProposal.slowRelayRoot = slowRelayRoot;
        rootBundleProposal.proposer = msg.sender;

        // Pull bondAmount of bondToken from the caller.
        bondToken.safeTransferFrom(msg.sender, address(this), bondAmount);

        emit ProposeRootBundle(
            challengePeriodEndTimestamp,
            poolRebalanceLeafCount,
            bundleEvaluationBlockNumbers,
            poolRebalanceRoot,
            relayerRefundRoot,
            slowRelayRoot,
            msg.sender
        );
    }

    /**
     * @notice Executes a pool rebalance leaf as part of the currently published root bundle. Will bridge any tokens
     * from this contract to the SpokePool designated in the leaf, and will also publish relayer refund and slow
     * relay roots to the SpokePool on the network specified in the leaf.
     * @dev In some cases, will instruct spokePool to send funds back to L1.
     * @param chainId ChainId number of the target spoke pool on which the bundle is executed.
     * @param groupIndex If set to 0, then relay roots to SpokePool via cross chain bridge. Used by off-chain validator
     * to organize leaves with the same chain ID and also set which leaves should result in relayed messages.
     * @param bundleLpFees Array representing the total LP fee amount per token in this bundle for all bundled relays.
     * @param netSendAmounts Array representing the amount of tokens to send to the SpokePool on the target chainId.
     * @param runningBalances Array used to track any unsent tokens that are not included in the netSendAmounts.
     * @param leafId Index of this executed leaf within the poolRebalance tree.
     * @param l1Tokens Array of all the tokens associated with the bundleLpFees, nedSendAmounts and runningBalances.
     * @param proof Inclusion proof for this leaf in pool rebalance root in root bundle.
     */

    function executeRootBundle(
        uint256 chainId,
        uint256 groupIndex,
        uint256[] memory bundleLpFees,
        int256[] memory netSendAmounts,
        int256[] memory runningBalances,
        uint8 leafId,
        address[] memory l1Tokens,
        bytes32[] calldata proof
    ) public nonReentrant unpaused {
        require(getCurrentTime() > rootBundleProposal.challengePeriodEndTimestamp, "Not passed liveness");

        // Verify the leafId in the poolRebalanceLeaf has not yet been claimed.
        require(!MerkleLib.isClaimed1D(rootBundleProposal.claimedBitMap, leafId), "Already claimed");

        // Verify the props provided generate a leaf that, along with the proof, are included in the merkle root.
        require(
            MerkleLib.verifyPoolRebalance(
                rootBundleProposal.poolRebalanceRoot,
                PoolRebalanceLeaf({
                    chainId: chainId,
                    groupIndex: groupIndex,
                    bundleLpFees: bundleLpFees,
                    netSendAmounts: netSendAmounts,
                    runningBalances: runningBalances,
                    leafId: leafId,
                    l1Tokens: l1Tokens
                }),
                proof
            ),
            "Bad Proof"
        );
        // Grouping code that uses adapter and spokepool to avoid stack too deep warning.
        // Get cross chain helpers for leaf's destination chain ID. This internal method will revert if either helper
        // is set improperly.
        (address adapter, address spokePool) = _getInitializedCrossChainContracts(chainId);

        // Set the leafId in the claimed bitmap.
        rootBundleProposal.claimedBitMap = MerkleLib.setClaimed1D(rootBundleProposal.claimedBitMap, leafId);

        // Decrement the unclaimedPoolRebalanceLeafCount.
        --rootBundleProposal.unclaimedPoolRebalanceLeafCount;

        // Relay each L1 token to destination chain.
        // Note: if any of the keccak256(l1Tokens, chainId) combinations are not mapped to a destination token address,
        // then this internal method will revert. In this case the admin will have to associate a destination token
        // with each l1 token. If the destination token mapping was missing at the time of the proposal, we assume
        // that the root bundle would have been disputed because the off-chain data worker would have been unable to
        // determine if the relayers used the correct destination token for a given origin token.
        _sendTokensToChainAndUpdatePooledTokenTrackers(
            adapter,
            spokePool,
            chainId,
            l1Tokens,
            netSendAmounts,
            bundleLpFees
        );

        // Check bool used by data worker to prevent relaying redundant roots to SpokePool.
        if (groupIndex == 0) {
            // Relay root bundles to spoke pool on destination chain by
            // performing delegatecall to use the adapter's code with this contract's context.

            // We are ok with this low-level call since the adapter address is set by the admin and we've
            // already checked that its not the zero address.
            // solhint-disable-next-line avoid-low-level-calls
            (bool success, ) = adapter.delegatecall(
                abi.encodeWithSignature(
                    "relayMessage(address,bytes)",
                    spokePool, // target. This should be the spokePool on the L2.
                    abi.encodeWithSignature(
                        "relayRootBundle(bytes32,bytes32)",
                        rootBundleProposal.relayerRefundRoot,
                        rootBundleProposal.slowRelayRoot
                    ) // message
                )
            );
            require(success, "delegatecall failed");
        }

        // Transfer the bondAmount back to the proposer, if this the last executed leaf. Only sending this once all
        // leaves have been executed acts to force the data worker to execute all bundles or they won't receive their bond.
        if (rootBundleProposal.unclaimedPoolRebalanceLeafCount == 0)
            bondToken.safeTransfer(rootBundleProposal.proposer, bondAmount);

        emit RootBundleExecuted(
            groupIndex,
            leafId,
            chainId,
            l1Tokens,
            bundleLpFees,
            netSendAmounts,
            runningBalances,
            msg.sender
        );
    }

    /**
     * @notice Caller stakes a bond to dispute the current root bundle proposal assuming it has not passed liveness
     * yet. The proposal is deleted, allowing a follow-up proposal to be submitted, and the dispute is sent to the
     * optimistic oracle to be adjudicated. Can only be called within the liveness period of the current proposal.
     * @notice The caller of this function must approve this contract to spend bondAmount of l1Token.
     */
    function disputeRootBundle() public nonReentrant zeroOptimisticOracleApproval {
        uint32 currentTime = uint32(getCurrentTime());
        require(currentTime <= rootBundleProposal.challengePeriodEndTimestamp, "Request passed liveness");

        // Request price from OO and dispute it.
        uint256 finalFee = _getBondTokenFinalFee();

        // This method will request a price from the OO and dispute it. Note that we set the ancillary data to
        // the empty string (""). The root bundle that is being disputed was the most recently proposed one with a
        // block number less than or equal to the dispute block time. All of this root bundle data can be found in
        // the ProposeRootBundle event params. Moreover, the optimistic oracle will stamp the requester's address
        // (i.e. this contract address) meaning that ancillary data for a dispute originating from another HubPool
        // will always be distinct from a dispute originating from this HubPool. Moreover, since
        // bundleEvaluationNumbers for a root bundle proposal are not stored in this contract, DVM voters will always
        // have to look up the ProposeRootBundle event to evaluate a dispute, therefore there is no point emitting extra
        // data in this ancillary data that is already included in the ProposeRootBundle event.

        // If the finalFee is larger than the bond amount, the bond amount needs to be reset before a request can go
        // through. Cancel to avoid a revert. Similarly, if the final fee == bond amount, then the proposer bond
        // set in the optimistic oracle would be 0. The optimistic oracle would then default the bond to be equal
        // to the final fee, which would mean that the allowance set to the bondAmount would be insufficient and the
        // requestAndProposePriceFor() call would revert. Source: https://github.com/UMAprotocol/protocol/blob/5b37ea818a28479c01e458389a83c3e736306b17/packages/core/contracts/oracle/implementation/SkinnyOptimisticOracle.sol#L321
        if (finalFee >= bondAmount) {
            _cancelBundle();
            return;
        }

        SkinnyOptimisticOracleInterface optimisticOracle = _getOptimisticOracle();

        // Only approve exact tokens to avoid more tokens than expected being pulled into the OptimisticOracle.
        bondToken.safeIncreaseAllowance(address(optimisticOracle), bondAmount);
        try
            optimisticOracle.requestAndProposePriceFor(
                identifier,
                currentTime,
                "",
                bondToken,
                // Set reward to 0, since we'll settle proposer reward payouts directly from this contract after a root
                // proposal has passed the challenge period.
                0,
                // Set the Optimistic oracle proposer bond for the request. We can assume that bondAmount > finalFee.
                bondAmount - finalFee,
                // Set the Optimistic oracle liveness for the price request.
                liveness,
                rootBundleProposal.proposer,
                // Canonical value representing "True"; i.e. the proposed relay is valid.
                int256(1e18)
            )
        returns (uint256) {
            // Ensure that approval == 0 after the call so the increaseAllowance call below doesn't allow more tokens
            // to transfer than intended.
            bondToken.safeApprove(address(optimisticOracle), 0);
        } catch {
            // Cancel the bundle since the proposal failed.
            _cancelBundle();
            return;
        }

        // Dispute the request that we just sent.
        SkinnyOptimisticOracleInterface.Request memory ooPriceRequest = SkinnyOptimisticOracleInterface.Request({
            proposer: rootBundleProposal.proposer,
            disputer: address(0),
            currency: bondToken,
            settled: false,
            proposedPrice: int256(1e18),
            resolvedPrice: 0,
            expirationTime: currentTime + liveness,
            reward: 0,
            finalFee: finalFee,
            bond: bondAmount - finalFee,
            customLiveness: liveness
        });

        // Finally, delete the state pertaining to the active proposal so that another proposer can submit a new bundle.
        delete rootBundleProposal;

        bondToken.safeTransferFrom(msg.sender, address(this), bondAmount);
        bondToken.safeIncreaseAllowance(address(optimisticOracle), bondAmount);
        optimisticOracle.disputePriceFor(identifier, currentTime, "", ooPriceRequest, msg.sender, address(this));

        emit RootBundleDisputed(msg.sender, currentTime);
    }

    /**
     * @notice Send unclaimed accumulated protocol fees to fee capture address.
     * @param l1Token Token whose protocol fees the caller wants to disburse.
     */
    function claimProtocolFeesCaptured(address l1Token) public override nonReentrant {
        uint256 _unclaimedAccumulatedProtocolFees = unclaimedAccumulatedProtocolFees[l1Token];
        unclaimedAccumulatedProtocolFees[l1Token] = 0;
        IERC20(l1Token).safeTransfer(protocolFeeCaptureAddress, _unclaimedAccumulatedProtocolFees);
        emit ProtocolFeesCapturedClaimed(l1Token, _unclaimedAccumulatedProtocolFees);
    }

    /**
     * @notice Conveniently queries which destination token is mapped to the hash of an l1 token + destination chain ID.
     * @dev Admin must be considerate to the compatibility of originToken and destinationToken within the protocol. Some
     * token implementations will not function correctly within the Across v2 system. For example ERC20s that charge
     * fees will break internal accounting, ERC777 can cause some functions to revert and upgradable tokens can pose
     * risks if the implementation is shifted between whitelisting and usage.
     * @dev If the pool rebalance route is not whitelisted then this will return address(0).
     * @param destinationChainId Where destination token is deployed.
     * @param l1Token Ethereum version token.
     * @return destinationToken address The destination token that is sent to spoke pools after this contract bridges
     * the l1Token to the destination chain.
     */
    function poolRebalanceRoute(uint256 destinationChainId, address l1Token)
        external
        view
        override
        returns (address destinationToken)
    {
        return poolRebalanceRoutes[_poolRebalanceRouteKey(l1Token, destinationChainId)];
    }

    /**
     * @notice This function allows a caller to load the contract with raw ETH to perform L2 calls. This is needed for
     * Arbitrum calls, but may also be needed for others.
     * @dev This function cannot be included in a multicall transaction call because it is payable. A realistic
     * situation where this might be an issue is if the caller is executing a PoolRebalanceLeaf that needs to relay
     * messages to Arbitrum. Relaying messages to Arbitrum requires that this contract has an ETH balance, so in this
     * case the caller would need to pre-load this contract with ETH before multicall-executing the leaf.
     */
    function loadEthForL2Calls() public payable override {
        /* solhint-disable-line no-empty-blocks */
    }

    /*************************************************
     *              INTERNAL FUNCTIONS               *
     *************************************************/

    // Called when a dispute fails due to parameter changes. This effectively resets the state and cancels the request
    // with no loss of funds, thereby enabling a new bundle to be added.
    function _cancelBundle() internal {
        bondToken.transfer(rootBundleProposal.proposer, bondAmount);
        delete rootBundleProposal;
        emit RootBundleCanceled(msg.sender, getCurrentTime());
    }

    function _getOptimisticOracle() internal view returns (SkinnyOptimisticOracleInterface) {
        return
            SkinnyOptimisticOracleInterface(finder.getImplementationAddress(OracleInterfaces.SkinnyOptimisticOracle));
    }

    function _getBondTokenFinalFee() internal view returns (uint256) {
        return
            StoreInterface(finder.getImplementationAddress(OracleInterfaces.Store))
                .computeFinalFee(address(bondToken))
                .rawValue;
    }

    // Note this method does a lot and wraps together the sending of tokens and updating the pooled token trackers. This
    // is done as a gas saving so we don't need to iterate over the l1Tokens multiple times.
    function _sendTokensToChainAndUpdatePooledTokenTrackers(
        address adapter,
        address spokePool,
        uint256 chainId,
        address[] memory l1Tokens,
        int256[] memory netSendAmounts,
        uint256[] memory bundleLpFees
    ) internal {
        uint256 length = l1Tokens.length;
        for (uint256 i = 0; i < length; ) {
            address l1Token = l1Tokens[i];
            // Validate the L1 -> L2 token route is stored. If it is not then the output of the bridging action
            // could send tokens to the 0x0 address on the L2.
            address l2Token = poolRebalanceRoutes[_poolRebalanceRouteKey(l1Token, chainId)];
            require(l2Token != address(0), "Route not whitelisted");

            // If the net send amount for this token is positive then: 1) send tokens from L1->L2 to facilitate the L2
            // relayer refund, 2) Update the liquidity trackers for the associated pooled tokens.
            if (netSendAmounts[i] > 0) {
                // Perform delegatecall to use the adapter's code with this contract's context. Opt for delegatecall's
                // complexity in exchange for lower gas costs.

                // We are ok with this low-level call since the adapter address is set by the admin and we've
                // already checked that its not the zero address.
                // solhint-disable-next-line avoid-low-level-calls
                (bool success, ) = adapter.delegatecall(
                    abi.encodeWithSignature(
                        "relayTokens(address,address,uint256,address)",
                        l1Token, // l1Token.
                        l2Token, // l2Token.
                        uint256(netSendAmounts[i]), // amount.
                        spokePool // to. This should be the spokePool.
                    )
                );
                require(success, "delegatecall failed");

                // Liquid reserves is decreased by the amount sent. utilizedReserves is increased by the amount sent.
                pooledTokens[l1Token].utilizedReserves += netSendAmounts[i];
                pooledTokens[l1Token].liquidReserves -= uint256(netSendAmounts[i]);
            }

            // Allocate LP fees and protocol fees from the bundle to the associated pooled token trackers.
            _allocateLpAndProtocolFees(l1Token, bundleLpFees[i]);

            // L1 tokens length won't be > types(uint256).length, so use unchecked block to save gas. Based on the
            // stress test results in /test/gas-analytics/HubPool.RootExecution.ts, the UMIP should limit the L1 token
            // count in valid proposals to be ~100 so any PoolRebalanceLeaves with > 100 l1Tokens should not make it
            // to this stage.
            unchecked {
                ++i;
            }
        }
    }

    function _exchangeRateCurrent(address l1Token) internal returns (uint256) {
        PooledToken storage pooledToken = pooledTokens[l1Token]; // Note this is storage so the state can be modified.
        uint256 lpTokenTotalSupply = IERC20(pooledToken.lpToken).totalSupply();
        if (lpTokenTotalSupply == 0) return 1e18; // initial rate is 1:1 between LP tokens and collateral.

        // First, update fee counters and local accounting of finalized transfers from L2 -> L1.
        _updateAccumulatedLpFees(pooledToken); // Accumulate all allocated fees from the last time this method was called.
        _sync(l1Token); // Fetch any balance changes due to token bridging finalization and factor them in.

        // ExchangeRate := (liquidReserves + utilizedReserves - undistributedLpFees) / lpTokenSupply
        // Both utilizedReserves and undistributedLpFees contain assigned LP fees. UndistributedLpFees is gradually
        // decreased over the smear duration using _updateAccumulatedLpFees. This means that the exchange rate will
        // gradually increase over time as undistributedLpFees goes to zero.
        // utilizedReserves can be negative. If this is the case, then liquidReserves is offset by an equal
        // and opposite size. LiquidReserves + utilizedReserves will always be larger than undistributedLpFees so this
        // int will always be positive so there is no risk in underflow in type casting in the return line.
        int256 numerator = int256(pooledToken.liquidReserves) +
            pooledToken.utilizedReserves -
            int256(pooledToken.undistributedLpFees);
        return (uint256(numerator) * 1e18) / lpTokenTotalSupply;
    }

    // Update internal fee counters by adding in any accumulated fees from the last time this logic was called.
    function _updateAccumulatedLpFees(PooledToken storage pooledToken) internal {
        uint256 accumulatedFees = _getAccumulatedFees(pooledToken.undistributedLpFees, pooledToken.lastLpFeeUpdate);
        pooledToken.undistributedLpFees -= accumulatedFees;
        pooledToken.lastLpFeeUpdate = uint32(getCurrentTime());
    }

    // Calculate the unallocated accumulatedFees from the last time the contract was called.
    function _getAccumulatedFees(uint256 undistributedLpFees, uint256 lastLpFeeUpdate) internal view returns (uint256) {
        // accumulatedFees := min(undistributedLpFees * lpFeeRatePerSecond * timeFromLastInteraction, undistributedLpFees)
        // The min acts to pay out all fees in the case the equation returns more than the remaining fees.
        uint256 timeFromLastInteraction = getCurrentTime() - lastLpFeeUpdate;
        uint256 maxUndistributedLpFees = (undistributedLpFees * lpFeeRatePerSecond * timeFromLastInteraction) / (1e18);
        return maxUndistributedLpFees < undistributedLpFees ? maxUndistributedLpFees : undistributedLpFees;
    }

    function _sync(address l1Token) internal {
        // Check if the l1Token balance of the contract is greater than the liquidReserves. If it is then the bridging
        // action from L2 -> L1 has concluded and the local accounting can be updated.
        // Note: this calculation must take into account the bond when it's acting on the bond token and there's an
        // active request.
        uint256 balance = IERC20(l1Token).balanceOf(address(this));
        uint256 balanceSansBond = l1Token == address(bondToken) && _activeRequest() ? balance - bondAmount : balance;
        if (balanceSansBond > pooledTokens[l1Token].liquidReserves) {
            // Note the numerical operation below can send utilizedReserves to negative. This can occur when tokens are
            // dropped onto the contract, exceeding the liquidReserves.
            pooledTokens[l1Token].utilizedReserves -= int256(balanceSansBond - pooledTokens[l1Token].liquidReserves);
            pooledTokens[l1Token].liquidReserves = balanceSansBond;
        }
    }

    function _liquidityUtilizationPostRelay(address l1Token, uint256 relayedAmount) internal returns (uint256) {
        _sync(l1Token); // Fetch any balance changes due to token bridging finalization and factor them in.

        // liquidityUtilizationRatio := (relayedAmount + max(utilizedReserves,0)) / (liquidReserves + max(utilizedReserves,0))
        // UtilizedReserves has a dual meaning: if it's greater than zero then it represents funds pending in the bridge
        // that will flow from L2 to L1. In this case, we can use it normally in the equation. However, if it is
        // negative, then it is already counted in liquidReserves. This occurs if tokens are transferred directly to the
        // contract. In this case, ignore it as it is captured in liquid reserves and has no meaning in the numerator.
        PooledToken memory pooledL1Token = pooledTokens[l1Token];
        uint256 flooredUtilizedReserves = pooledL1Token.utilizedReserves > 0
            ? uint256(pooledL1Token.utilizedReserves) // If positive: take the uint256 cast utilizedReserves.
            : 0; // Else, if negative, it is already captured in liquidReserves and should be ignored.
        uint256 numerator = relayedAmount + flooredUtilizedReserves;
        uint256 denominator = pooledL1Token.liquidReserves + flooredUtilizedReserves;

        // If the denominator equals zero, return 1e18 (max utilization).
        if (denominator == 0) return 1e18;

        // In all other cases, return the utilization ratio.
        return (numerator * 1e18) / denominator;
    }

    function _allocateLpAndProtocolFees(address l1Token, uint256 bundleLpFees) internal {
        // Calculate the fraction of bundledLpFees that are allocated to the protocol and to the LPs.
        uint256 protocolFeesCaptured = (bundleLpFees * protocolFeeCapturePct) / 1e18;
        uint256 lpFeesCaptured = bundleLpFees - protocolFeesCaptured;

        // Assign any LP fees included into the bundle to the pooled token. These LP fees are tracked in the
        // undistributedLpFees and within the utilizedReserves. undistributedLpFees is gradually decreased
        // over the smear duration to give the LPs their rewards over a period of time. Adding to utilizedReserves
        // acts to track these rewards after the smear duration. See _exchangeRateCurrent for more details.
        if (lpFeesCaptured > 0) {
            pooledTokens[l1Token].undistributedLpFees += lpFeesCaptured;
            pooledTokens[l1Token].utilizedReserves += int256(lpFeesCaptured);
        }

        // If there are any protocol fees, allocate them to the unclaimed protocol tracker amount.
        if (protocolFeesCaptured > 0) unclaimedAccumulatedProtocolFees[l1Token] += protocolFeesCaptured;
    }

    function _relaySpokePoolAdminFunction(uint256 chainId, bytes memory functionData) internal {
        (address adapter, address spokePool) = _getInitializedCrossChainContracts(chainId);

        // Perform delegatecall to use the adapter's code with this contract's context.

        // We are ok with this low-level call since the adapter address is set by the admin and we've
        // already checked that its not the zero address.
        // solhint-disable-next-line avoid-low-level-calls
        (bool success, ) = adapter.delegatecall(
            abi.encodeWithSignature(
                "relayMessage(address,bytes)",
                spokePool, // target. This should be the spokePool on the L2.
                functionData
            )
        );
        require(success, "delegatecall failed");
        emit SpokePoolAdminFunctionTriggered(chainId, functionData);
    }

    function _poolRebalanceRouteKey(address l1Token, uint256 destinationChainId) internal pure returns (bytes32) {
        return keccak256(abi.encode(l1Token, destinationChainId));
    }

    function _getInitializedCrossChainContracts(uint256 chainId)
        internal
        view
        returns (address adapter, address spokePool)
    {
        adapter = crossChainContracts[chainId].adapter;
        spokePool = crossChainContracts[chainId].spokePool;
        require(spokePool != address(0), "SpokePool not initialized");
        require(adapter.isContract(), "Adapter not initialized");
    }

    function _activeRequest() internal view returns (bool) {
        return rootBundleProposal.unclaimedPoolRebalanceLeafCount != 0;
    }

    // If functionCallStackOriginatesFromOutsideThisContract is true then this was called by the callback function
    // by dropping ETH onto the contract. In this case, deposit the ETH into WETH. This would happen if ETH was sent
    // over the optimism bridge, for example. If false then this was set as a result of unwinding LP tokens, with the
    // intention of sending ETH to the LP. In this case, do nothing as we intend on sending the ETH to the LP.
    function _depositEthToWeth() internal {
        if (functionCallStackOriginatesFromOutsideThisContract()) weth.deposit{ value: msg.value }();
    }

    // Added to enable the HubPool to receive ETH. This will occur both when the HubPool unwraps WETH to send to LPs and
    // when ETH is sent over the canonical Optimism bridge, which sends ETH.
    fallback() external payable {
        _depositEthToWeth();
    }

    receive() external payable {
        _depositEthToWeth();
    }
}


Vulnerability type: CWE-703|CWE-755|CWE-248