HIGH severity: Forwarder and Withdrawal Helper Contracts Do Not Handle ETH Transfers Correctly. The `ForwarderBase` and `WithdrawalHelperBase` contracts, designed to operate on L2s and facilitate communication with L3s, lack a `receive` function, making them unable to accept direct ETH transfers. 
The root cause is the omission of the `receive` function, which is required for a contract to rec...

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

import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { ForwarderInterface } from "./interfaces/ForwarderInterface.sol";
import { AdapterInterface } from "./interfaces/AdapterInterface.sol";

/**
 * @title ForwarderBase
 * @notice This contract expects to receive messages and tokens from an authorized sender on L1 and forwards messages and tokens to spoke pool contracts on
 * L3. Messages are intended to originate from the hub pool. The motivating use case for this contract is to aid with sending messages from L1 to an L3, which
 * by definition is a network which does not have a direct connection with L1 but instead must communicate with that L1 via an L2. Each contract that extends
 * the ForwarderBase maintains a mapping of chain IDs to a bridge adapter addresses. For example, if this contract is deployed on Arbitrum, then this mapping
 * would send L3 chain IDs which roll up to Arbitrum to an adapter contract address deployed on Arbitrum which directly interfaces with the L3 token/message
 * bridge. In other words, this contract maintains a mapping of important contracts which helps transmit messages to the "next layer".
 * @custom:security-contact bugs@across.to
 */
abstract contract ForwarderBase is UUPSUpgradeable, ForwarderInterface {
    // Address that can relay messages using this contract and also upgrade this contract.
    address public crossDomainAdmin;

    // Map from a destination chain ID to the address of an adapter contract which interfaces with the L2-L3 bridge. The destination chain ID corresponds to
    // the network ID of an L3. These chain IDs are used as the key in this mapping because network IDs are enforced to be unique. Since we require the chain
    // ID to be sent along with a message or token relay, ForwarderInterface's relay functions include an extra field, `destinationChainId`, when compared to the
    // relay functions of `AdapterInterface`.
    mapping(uint256 => address) chainAdapters;

    event ChainAdaptersUpdated(uint256 indexed destinationChainId, address l2Adapter);
    event SetXDomainAdmin(address indexed crossDomainAdmin);

    error InvalidCrossDomainAdmin();
    error InvalidChainAdapter();
    error RelayMessageFailed();
    error RelayTokensFailed(address baseToken);
    // Error which is triggered when there is no adapter set in the `chainAdapters` mapping.
    error UninitializedChainAdapter();

    /*
     * @dev Cross domain admin permissioning is implemented specifically for each L2 that this contract is deployed on, so this base contract
     * simply prescribes this modifier to protect external functions using that L2's specific admin permissioning logic.
     */
    modifier onlyAdmin() {
        _requireAdminSender();
        _;
    }

    /**
     * @notice Constructs the Forwarder contract.
     * @dev _disableInitializers() restricts anybody from initializing the implementation contract, which if not done,
     * may disrupt the proxy if another EOA were to initialize it.
     */
    constructor() {
        _disableInitializers();
    }

    /**
     * @notice Initializes the forwarder contract.
     * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract.
     */
    function __Forwarder_init(address _crossDomainAdmin) public onlyInitializing {
        __UUPSUpgradeable_init();
        _setCrossDomainAdmin(_crossDomainAdmin);
    }

    /**
     * @notice Sets a new cross domain admin for this contract.
     * @param _newCrossDomainAdmin L1 address of the new cross domain admin.
     */
    function setCrossDomainAdmin(address _newCrossDomainAdmin) external onlyAdmin {
        if (_newCrossDomainAdmin == address(0)) revert InvalidCrossDomainAdmin();
        _setCrossDomainAdmin(_newCrossDomainAdmin);
        emit SetXDomainAdmin(_newCrossDomainAdmin);
    }

    /**
     * @notice Maps a new destination chain ID to an adapter contract which facilitates bridging to that chain.
     * @param _destinationChainId The chain ID of the target network.
     * @param _l2Adapter Contract address of the adapter which interfaces with the L2-L3 bridge.
     * @dev Actual bridging logic is delegated to the adapter contract so that the forwarder can function irrespective of the "flavor" of
     * L3 (e.g. ArbitrumOrbit, OpStack, etc.).
     */
    function updateAdapter(uint256 _destinationChainId, address _l2Adapter) external onlyAdmin {
        if (_l2Adapter == address(0)) revert InvalidChainAdapter();
        chainAdapters[_destinationChainId] = _l2Adapter;
        emit ChainAdaptersUpdated(_destinationChainId, _l2Adapter);
    }

    /**
     * @notice Relays a specified message to a contract on L3. This contract assumes that `target` exists on the L3 and can properly
     * receive the function being called.
     * @param target The address of the spoke pool contract that will receive the input message.
     * @param destinationChainId The chain ID of the network which contains `target`.
     * @param message The data to execute on the target contract.
     */
    function relayMessage(
        address target,
        uint256 destinationChainId,
        bytes memory message
    ) external payable override onlyAdmin {
        address adapter = chainAdapters[destinationChainId];
        if (adapter == address(0)) revert UninitializedChainAdapter();

        // The forwarder assumes that `target` exists on the following network.
        (bool success, ) = adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (target, message)));
        if (!success) revert RelayMessageFailed();
        emit MessageForwarded(target, destinationChainId, message);
    }

    /**
     * @notice Relays `amount` of a token to a contract on L3. Importantly, this contract assumes that `target` exists on L3.
     * @param baseToken This layer's address of the token to send.
     * @param destinationChainToken The next layer's address of the token to send.
     * @param amount The amount of the token to send.
     * @param destinationChainId The chain ID of the network which contains `target`.
     * @param target The address of the contract that which will *ultimately* receive the tokens. For most cases, this is the spoke pool contract on L3.
     * @dev While `relayMessage` also assumes that `target` is correct, this function has the potential of deleting funds if `target` is incorrectly set.
     * This should be guarded by the logic of the Hub Pool on L1, since the Hub Pool will always set `target` to the L3 spoke pool per UMIP-157.
     */
    function relayTokens(
        address baseToken,
        address destinationChainToken,
        uint256 amount,
        uint256 destinationChainId,
        address target
    ) external payable override onlyAdmin {
        address adapter = chainAdapters[destinationChainId];
        if (adapter == address(0)) revert UninitializedChainAdapter();

        (bool success, ) = adapter.delegatecall(
            abi.encodeCall(AdapterInterface.relayTokens, (baseToken, destinationChainToken, amount, target))
        );
        if (!success) revert RelayTokensFailed(baseToken);
        emit TokensForwarded(baseToken, destinationChainToken, amount, destinationChainId, target);
    }

    // Function to be overridden in order to authenticate that messages sent to this contract originated
    // from the expected account.
    function _requireAdminSender() internal virtual;

    // We also want to restrict who can upgrade this contract. The same admin that can relay messages through this
    // contract can upgrade this contract.
    function _authorizeUpgrade(address) internal virtual override onlyAdmin {}

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

    // 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[1000] private __gap;
}

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

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { MultiCaller } from "@uma/core/contracts/common/implementation/MultiCaller.sol";
import { CircleCCTPAdapter, ITokenMessenger, CircleDomainIds } from "../../libraries/CircleCCTPAdapter.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

/**
 * @title WithdrawalHelperBase
 * @notice This contract contains general configurations for bridging tokens from an L2 to a single recipient on L1.
 * @dev This contract should be deployed on L2. It provides an interface to withdraw tokens to some address on L1. The only
 * function which must be implemented in contracts which inherit this contract is `withdrawToken`. It is up to that function
 * to determine which bridges to use for an input L2 token. Importantly, that function must also verify that the l2 to l1
 * token mapping is correct so that the bridge call itself can succeed.
 */
abstract contract WithdrawalHelperBase is CircleCCTPAdapter, MultiCaller, UUPSUpgradeable {
    using SafeERC20 for IERC20;

    // The L1 address which will unconditionally receive all withdrawals from this contract.
    address public immutable TOKEN_RECIPIENT;
    // The address of the primary or default token gateway/canonical bridge contract on L2.
    address public immutable L2_TOKEN_GATEWAY;
    // The address of the admin contract on L1,which will likely be the hub pool. As a last resort, this admin can rescue stuck tokens
    // on this withdrawal helper contract, similar to how it may send admin functions to spoke pools.
    address public crossDomainAdmin;

    event SetXDomainAdmin(address _crossDomainAdmin);

    // Error which triggers when the cross domain admin was attempted to be set to the zero address.
    error InvalidCrossDomainAdmin();
    // Error which triggers when the caller of a protected function is not the cross domain admin.
    error NotCrossDomainAdmin();

    // Functions which contain this modifier should only be callable via a cross-chain call where the L1 msg.sender is the hub pool.
    modifier onlyAdmin() {
        _requireAdminSender();
        _;
    }

    /*
     * @notice Constructs a new withdrawal helper.
     * @param _l2Usdc Address of native USDC on the L2.
     * @param _cctpTokenMessenger Address of the CCTP token messenger contract on L2.
     * @param _destinationCircleDomainId Circle's assigned CCTP domain ID for the destination network.
     * @param _l2TokenGateway Address of the network's l2 token gateway/bridge contract.
     * @param _tokenRecipient L1 address which will unconditionally receive all withdrawals originating from this contract.
     * @param _crossDomainAdmin Address of the admin on L1. This address is the only one which may tell this contract to send tokens to an
     * L2 address.
     * @dev _disableInitializers() restricts anybody from initializing the implementation contract, which if not done,
     * may disrupt the proxy if another EOA were to initialize it.
     */
    constructor(
        IERC20 _l2Usdc,
        ITokenMessenger _cctpTokenMessenger,
        uint32 _destinationCircleDomainId,
        address _l2TokenGateway,
        address _tokenRecipient
    ) CircleCCTPAdapter(_l2Usdc, _cctpTokenMessenger, _destinationCircleDomainId) {
        L2_TOKEN_GATEWAY = _l2TokenGateway;
        TOKEN_RECIPIENT = _tokenRecipient;
        _disableInitializers();
    }

    /**
     * @notice Initializes the withdrawal helper contract.
     * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract.
     */
    function __WithdrawalHelper_init(address _crossDomainAdmin) public onlyInitializing {
        __UUPSUpgradeable_init();
        _setCrossDomainAdmin(_crossDomainAdmin);
    }

    /*
     * @notice Sets a new cross domain admin. The admin cannot be the zero address. The cross domain admin is the only address which may call
     * upgrade this contract.
     * @param _newCrossDomainAdmin L1 address of the new cross domain admin.
     */
    function _setCrossDomainAdmin(address _newCrossDomainAdmin) internal {
        if (_newCrossDomainAdmin == address(0)) revert InvalidCrossDomainAdmin();
        crossDomainAdmin = _newCrossDomainAdmin;
        emit SetXDomainAdmin(_newCrossDomainAdmin);
    }

    /*
     * @notice Withdraws a specified token to L1. This may be implemented uniquely for each L2, since each L2 has various
     * dependencies to withdraw a token, such as the token bridge to use, mappings for L1 and L2 tokens, and gas configurations.
     * Notably, withdrawals should always send token back to `TOKEN_RECIPIENT`.
     * @param l1Token Address of the l1Token to receive.
     * @param l2Token Address of the l2Token to send back.
     * @param amountToReturn Amount of l2Token to send back.
     * @dev Some networks do not require the L1/L2 token argument to withdraw tokens, while others enable contracts to derive the
     * L1/L2 given knowledge of only one of the addresses. Both arguments are provided to enable a flexible interface; however, due
     * to this, `withdrawToken` MUST account for situations where the L1/L2 token mapping is incorrect.
     */
    function withdrawToken(
        address l1Token,
        address l2Token,
        uint256 amountToReturn
    ) public virtual;

    /*
     * @notice Checks that the L1 msg.sender is the `crossDomainAdmin` address.
     * @dev This implementation must change on a per-chain basis, since each L2 network has their own method of deriving the L1 msg.sender.
     */
    function _requireAdminSender() internal virtual;

    /*
     * @notice Access control check for upgrading this proxy contract
     * @dev This requires that _requireAdminSender() is properly implemented on all contracts which inherit WithdrawalHelperBase.
     */
    function _authorizeUpgrade(address) internal override onlyAdmin {}

    // 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[1000] private __gap;
}

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

import "./interfaces/AdapterInterface.sol";
import "../external/interfaces/WETH9Interface.sol";
import "../libraries/CircleCCTPAdapter.sol";
import "../external/interfaces/CCTPInterfaces.sol";

// @dev Use local modified CrossDomainEnabled contract instead of one exported by eth-optimism because we need
// this contract's state variables to be `immutable` because of the delegateCall call.
import "./CrossDomainEnabled.sol";
import "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol";

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

/**
 * @notice Interface for Synthetix custom bridge to Optimism.
 */
interface SynthetixBridgeToOptimism is IL1StandardBridge {
    /**
     * @notice Send tokens to Optimism.
     * @param to Address to send tokens to on L2.
     * @param amount Amount of tokens to send.
     */
    function depositTo(address to, uint256 amount) external;
}

/**
 * @notice Contract containing logic to send messages from L1 to Optimism.
 * @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 it's only necessary that the HubPool's methods
 * that call this contract's logic guard against reentrancy.
 * @custom:security-contact bugs@across.to
 */

// solhint-disable-next-line contract-name-camelcase
contract Optimism_Adapter is CrossDomainEnabled, AdapterInterface, CircleCCTPAdapter {
    using SafeERC20 for IERC20;
    uint32 public constant L2_GAS_LIMIT = 200_000;

    WETH9Interface public immutable L1_WETH;

    IL1StandardBridge public immutable L1_STANDARD_BRIDGE;

    // Optimism has the ability to support "custom" bridges. These bridges are not supported by the canonical bridge
    // and so we need to store the address of the custom token and the associated bridge. In the event we want to
    // support a new token that is not supported by Optimism, we can add a new custom bridge for it and re-deploy the
    // adapter. A full list of custom optimism tokens and their associated bridges can be found here:
    // https://github.com/ethereum-optimism/ethereum-optimism.github.io/blob/master/optimism.tokenlist.json
    address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address public constant DAI_OPTIMISM_BRIDGE = 0x10E6593CDda8c58a1d0f14C5164B376352a55f2F;
    address public constant SNX = 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F;
    address public constant SNX_OPTIMISM_BRIDGE = 0x39Ea01a0298C315d149a490E34B59Dbf2EC7e48F;

    /**
     * @notice Constructs new Adapter.
     * @param _l1Weth WETH address on L1.
     * @param _crossDomainMessenger XDomainMessenger Optimism system contract.
     * @param _l1StandardBridge Standard bridge contract.
     * @param _l1Usdc USDC address on L1.
     * @param _cctpTokenMessenger TokenMessenger contract to bridge via CCTP.
     */
    constructor(
        WETH9Interface _l1Weth,
        address _crossDomainMessenger,
        IL1StandardBridge _l1StandardBridge,
        IERC20 _l1Usdc,
        ITokenMessenger _cctpTokenMessenger
    )
        CrossDomainEnabled(_crossDomainMessenger)
        CircleCCTPAdapter(_l1Usdc, _cctpTokenMessenger, CircleDomainIds.Optimism)
    {
        L1_WETH = _l1Weth;
        L1_STANDARD_BRIDGE = _l1StandardBridge;
    }

    /**
     * @notice Send cross-chain message to target on Optimism.
     * @param target Contract on Optimism that will receive message.
     * @param message Data to send to target.
     */
    function relayMessage(address target, bytes calldata message) external payable override {
        sendCrossDomainMessage(target, L2_GAS_LIMIT, message);
        emit MessageRelayed(target, message);
    }

    /**
     * @notice Bridge tokens to Optimism.
     * @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,
        uint256 amount,
        address to
    ) external payable override {
        // If the l1Token is weth then unwrap it to ETH then send the ETH to the standard bridge.
        if (l1Token == address(L1_WETH)) {
            L1_WETH.withdraw(amount);
            L1_STANDARD_BRIDGE.depositETHTo{ value: amount }(to, L2_GAS_LIMIT, "");
        }
        // If the l1Token is USDC, then we send it to the CCTP bridge
        else if (_isCCTPEnabled() && l1Token == address(usdcToken)) {
            _transferUsdc(to, amount);
        } else {
            address bridgeToUse = address(L1_STANDARD_BRIDGE);

            // Check if the L1 token requires a custom bridge. If so, use that bridge over the standard bridge.
            if (l1Token == DAI) bridgeToUse = DAI_OPTIMISM_BRIDGE; // 1. DAI
            if (l1Token == SNX) bridgeToUse = SNX_OPTIMISM_BRIDGE; // 2. SNX

            IERC20(l1Token).safeIncreaseAllowance(bridgeToUse, amount);
            if (l1Token == SNX) SynthetixBridgeToOptimism(bridgeToUse).depositTo(to, amount);
            else IL1StandardBridge(bridgeToUse).depositERC20To(l1Token, l2Token, to, amount, L2_GAS_LIMIT, "");
        }
        emit TokensRelayed(l1Token, l2Token, amount, to);
    }
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity ^0.8.0;

import { WithdrawalHelperBase } from "./WithdrawalHelperBase.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { WETH9Interface } from "../../external/interfaces/WETH9Interface.sol";
import { ITokenMessenger } from "../../external/interfaces/CCTPInterfaces.sol";
import { Lib_PredeployAddresses } from "@eth-optimism/contracts/libraries/constants/Lib_PredeployAddresses.sol";
import { LibOptimismUpgradeable } from "@openzeppelin/contracts-upgradeable/crosschain/optimism/LibOptimismUpgradeable.sol";
import { IL2ERC20Bridge } from "../../Ovm_SpokePool.sol";

/**
 * @notice Minimal interface for the Ovm_SpokePool contract. This interface is called to pull state from the network's
 * spoke pool contract to be used by this withdrawal adapter.
 */
interface IOvm_SpokePool {
    // Returns the address of the token bridge for the input l2 token.
    function tokenBridges(address token) external view returns (address);

    // Returns the address of the l1 token set in the spoke pool for the input l2 token.
    function remoteL1Tokens(address token) external view returns (address);

    // Returns the address for the representation of ETH on the l2.
    function l2Eth() external view returns (address);

    // Returns the address of the wrapped native token for the L2.
    function wrappedNativeToken() external view returns (WETH9Interface);

    // Returns the amount of gas the contract allocates for a token withdrawal.
    function l1Gas() external view returns (uint32);
}

/**
 * @title Ovm_WithdrawalAdapter
 * @notice This contract interfaces with L2-L1 token bridges and withdraws tokens to a single address on L1.
 * @dev This contract should be deployed on OpStack L2s which both have a Ovm_SpokePool contract deployed to the L2
 * network AND only use token bridges defined in the Ovm_SpokePool. A notable exception to this requirement is Optimism,
 * which has a special SNX bridge (and thus this adapter will NOT work for Optimism).
 * @custom:security-contact bugs@across.to
 */
contract Ovm_WithdrawalHelper is WithdrawalHelperBase {
    using SafeERC20 for IERC20;

    // Address for the wrapped native token on this chain. For Ovm standard bridges, we need to unwrap
    // this token before initiating the withdrawal. Normally, it is 0x42..006, but there are instances
    // where this address is different.
    WETH9Interface public immutable wrappedNativeToken;
    // Address of the corresponding spoke pool on L2. This is to piggyback off of the spoke pool's supported
    // token routes/defined token bridges.
    IOvm_SpokePool public immutable spokePool;
    // Address of native ETH on the l2. For OpStack chains, this address is used to indicate a native ETH withdrawal.
    // In general, this address is 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000.
    address public immutable l2Eth;
    // Address of the messenger contract on L2. This is by default defined in Lib_PredeployAddresses.
    address public constant MESSENGER = Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER;

    /*
     * @notice Constructs the Ovm_WithdrawalAdapter.
     * @param _l2Usdc Address of native USDC on the L2.
     * @param _cctpTokenMessenger Address of the CCTP token messenger contract on L2.
     * @param _destinationCircleDomainId Circle's assigned CCTP domain ID for the destination network. For Ethereum, this
     * is 0.
     * @param _l2Gateway Address of the Optimism ERC20 L2 standard bridge contract.
     * @param _tokenRecipient The L1 address which will unconditionally receive tokens from withdrawals by this contract.
     * @param _crossDomainAdmin Address of the admin on L1. This address is the only one which may tell this contract to send tokens to an
     * L2 address.
     * @param _spokePool The contract address of the Ovm_SpokePool which is deployed on this L2 network.
     */
    constructor(
        IERC20 _l2Usdc,
        ITokenMessenger _cctpTokenMessenger,
        uint32 _destinationCircleDomainId,
        address _l2Gateway,
        address _tokenRecipient,
        IOvm_SpokePool _spokePool
    ) WithdrawalHelperBase(_l2Usdc, _cctpTokenMessenger, _destinationCircleDomainId, _l2Gateway, _tokenRecipient) {
        spokePool = _spokePool;

        // These addresses should only change network-by-network, or after a bridge upgrade, so we define them once in the constructor.
        wrappedNativeToken = spokePool.wrappedNativeToken();
        l2Eth = spokePool.l2Eth();
    }

    /**
     * @notice Initializes the withdrawal helper contract.
     * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract.
     */
    function initialize(address _crossDomainAdmin) public initializer {
        __WithdrawalHelper_init(_crossDomainAdmin);
    }

    /*
     * @notice Calls CCTP or the Optimism token gateway to withdraw tokens back to the recipient.
     * @param l2Token address of the l2Token to send back.
     * @param amountToReturn amount of l2Token to send back.
     * @dev The l1Token parameter is unused since we obtain the l1Token to receive by querying the state of the Ovm_SpokePool deployed
     * to this network.
     * @dev This function is a copy of the `_bridgeTokensToHubPool` function found on the Ovm_SpokePool contract here:
     * https://github.com/across-protocol/contracts/blob/65191dbcded95c8fe050e0f95eb7848e3784e61f/contracts/Ovm_SpokePool.sol#L148.
     * New lines of code correspond to instances where this contract queries state from the spoke pool, such as determining
     * the appropriate token bridge for the withdrawal or finding the remoteL1Token to withdraw.
     */
    function withdrawToken(
        address,
        address l2Token,
        uint256 amountToReturn
    ) public override {
        // Fetch the current l1Gas defined in the Ovm_SpokePool.
        uint32 l1Gas = spokePool.l1Gas();
        // If the token being bridged is WETH then we need to first unwrap it to ETH and then send ETH over the
        // canonical bridge. On Optimism, this is address 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000.
        if (l2Token == address(wrappedNativeToken)) {
            WETH9Interface(l2Token).withdraw(amountToReturn); // Unwrap into ETH.
            l2Token = l2Eth; // Set the l2Token to ETH.
            IL2ERC20Bridge(Lib_PredeployAddresses.L2_STANDARD_BRIDGE).withdrawTo{ value: amountToReturn }(
                l2Token, // _l2Token. Address of the L2 token to bridge over.
                TOKEN_RECIPIENT, // _to. Withdraw, over the bridge, to the l1 pool contract.
                amountToReturn, // _amount.
                l1Gas, // _l1Gas. Unused, but included for potential forward compatibility considerations
                "" // _data. We don't need to send any data for the bridging action.
            );
        }
        // If the token is USDC && CCTP bridge is enabled, then bridge USDC via CCTP.
        else if (l2Token == address(usdcToken) && _isCCTPEnabled()) {
            _transferUsdc(TOKEN_RECIPIENT, amountToReturn);
        }
        // Note we'll default to withdrawTo instead of bridgeERC20To unless the remoteL1Tokens mapping is set for
        // the l2Token. withdrawTo should be used to bridge back non-native L2 tokens
        // (i.e. non-native L2 tokens have a canonical L1 token). If we should bridge "native L2" tokens then
        // we'd need to call bridgeERC20To and give allowance to the tokenBridge to spend l2Token from this contract.
        // Therefore for native tokens we should set ensure that remoteL1Tokens is set for the l2Token.
        else {
            IL2ERC20Bridge tokenBridge = IL2ERC20Bridge(
                spokePool.tokenBridges(l2Token) == address(0)
                    ? Lib_PredeployAddresses.L2_STANDARD_BRIDGE
                    : spokePool.tokenBridges(l2Token)
            );
            address remoteL1Token = spokePool.remoteL1Tokens(l2Token);
            if (remoteL1Token != address(0)) {
                // If there is a mapping for this L2 token to an L1 token, then use the L1 token address and
                // call bridgeERC20To.
                IERC20(l2Token).safeIncreaseAllowance(address(tokenBridge), amountToReturn);
                tokenBridge.bridgeERC20To(
                    l2Token, // _l2Token. Address of the L2 token to bridge over.
                    remoteL1Token, // Remote token to be received on L1 side. If the
                    // remoteL1Token on the other chain does not recognize the local token as the correct
                    // pair token, the ERC20 bridge will fail and the tokens will be returned to sender on
                    // this chain.
                    TOKEN_RECIPIENT, // _to
                    amountToReturn, // _amount
                    l1Gas, // _l1Gas
                    "" // _data
                );
            } else {
                tokenBridge.withdrawTo(
                    l2Token, // _l2Token. Address of the L2 token to bridge over.
                    TOKEN_RECIPIENT, // _to. Withdraw, over the bridge, to the l1 pool contract.
                    amountToReturn, // _amount.
                    l1Gas, // _l1Gas. Unused, but included for potential forward compatibility considerations
                    "" // _data. We don't need to send any data for the bridging action.
                );
            }
        }
    }

    function _requireAdminSender() internal view override {
        if (LibOptimismUpgradeable.crossChainSender(MESSENGER) != crossDomainAdmin) revert NotCrossDomainAdmin();
    }
}


Vulnerability type: CWE-710|CWE-1076