MEDIUM severity: Unsafe ABI Encoding. The codebase uses `abi.encodeWithSelector` in multiple locations, which is not type-safe and can lead to incorrect calldata encoding if the function signature does not match the intended target. This is used in functions such as `_buildMulticallInstructions`, `receiveMessage`, `lzCompose`, and other...

Vulnerable code:
// 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 MulticallHandler
import { MulticallHandler } from "../../handlers/MulticallHandler.sol";
import { EVMFlowParams, CommonFlowParams } from "./Structs.sol";

/**
 * @title ArbitraryEVMFlowExecutor
 * @notice Base contract for executing arbitrary action sequences using MulticallHandler
 * @dev This contract provides shared functionality for both OFT and CCTP handlers to execute
 * arbitrary actions on HyperEVM via MulticallHandler, returning information about the resulting token amount
 * @custom:security-contact bugs@across.to
 */
abstract contract ArbitraryEVMFlowExecutor {
    using SafeERC20 for IERC20;

    /// @notice Compressed call struct (no value field to save gas)
    struct CompressedCall {
        address target;
        bytes callData;
    }

    /// @notice MulticallHandler contract instance
    address public immutable multicallHandler;

    /// @notice Emitted when arbitrary actions are executed successfully
    event ArbitraryActionsExecuted(
        bytes32 indexed quoteNonce,
        address indexed initialToken,
        uint256 initialAmount,
        address indexed finalToken,
        uint256 finalAmount
    );

    /// @notice Error thrown when final balance is insufficient
    error InsufficientFinalBalance(address token, uint256 expected, uint256 actual);

    uint256 private constant BPS_TOTAL_PRECISION = 18;
    uint256 private constant BPS_DECIMALS = 4;
    uint256 private constant BPS_PRECISION_SCALAR = 10 ** BPS_TOTAL_PRECISION;

    constructor(address _multicallHandler) {
        multicallHandler = _multicallHandler;
    }

    /**
     * @notice Executes arbitrary actions by transferring tokens to MulticallHandler
     * @dev Decompresses CompressedCall[] to MulticallHandler.Call[] format (adds value: 0)
     * @param params Parameters of HyperEVM execution
     * @return commonParams Parameters to continue sponsored execution to transfer funds to final recipient at correct destination
     */
    function _executeFlow(EVMFlowParams memory params) internal returns (CommonFlowParams memory commonParams) {
        // Decode the compressed action data
        CompressedCall[] memory compressedCalls = abi.decode(params.actionData, (CompressedCall[]));

        // Snapshot balances
        uint256 initialAmountSnapshot = IERC20(params.initialToken).balanceOf(address(this));
        uint256 finalAmountSnapshot = IERC20(params.commonParams.finalToken).balanceOf(address(this));

        // Transfer tokens to MulticallHandler
        IERC20(params.initialToken).safeTransfer(multicallHandler, params.commonParams.amountInEVM);

        // Build instructions for MulticallHandler
        bytes memory instructions = _buildMulticallInstructions(
            compressedCalls,
            params.commonParams.finalToken,
            address(this) // Send leftover tokens back to this contract
        );

        // Execute via MulticallHandler
        MulticallHandler(payable(multicallHandler)).handleV3AcrossMessage(
            params.initialToken,
            params.commonParams.amountInEVM,
            address(this),
            instructions
        );

        uint256 finalAmount;
        // This means the swap (if one was intended) didn't happen (action failed), so we use the initial token as the final token.
        if (initialAmountSnapshot == IERC20(params.initialToken).balanceOf(address(this))) {
            params.commonParams.finalToken = params.initialToken;
            finalAmount = params.commonParams.amountInEVM;
        } else {
            uint256 finalBalance = IERC20(params.commonParams.finalToken).balanceOf(address(this));
            if (finalBalance >= finalAmountSnapshot) {
                // This means the swap did happen, so we check the balance of the output token and send it.
                finalAmount = finalBalance - finalAmountSnapshot;
            } else {
                // If we somehow lost final tokens, just set the finalAmount to 0.
                finalAmount = 0;
            }
        }

        params.commonParams.extraFeesIncurred = _calcExtraFeesFinal(
            params.commonParams.amountInEVM,
            params.commonParams.extraFeesIncurred,
            finalAmount
        );
        params.commonParams.amountInEVM = finalAmount;

        emit ArbitraryActionsExecuted(
            params.commonParams.quoteNonce,
            params.initialToken,
            params.commonParams.amountInEVM,
            params.commonParams.finalToken,
            finalAmount
        );

        return params.commonParams;
    }

    /**
     * @notice Builds MulticallHandler Instructions from compressed calls
     * @dev Decompresses calls by adding value: 0, and adds drainLeftoverTokens call at the end
     */
    function _buildMulticallInstructions(
        CompressedCall[] memory compressedCalls,
        address finalToken,
        address fallbackRecipient
    ) internal view returns (bytes memory) {
        uint256 callCount = compressedCalls.length;

        // Create Call[] array with value: 0 for each call, plus one for drainLeftoverTokens
        MulticallHandler.Call[] memory calls = new MulticallHandler.Call[](callCount + 1);

        // Decompress: add value: 0 to each call
        for (uint256 i = 0; i < callCount; ++i) {
            calls[i] = MulticallHandler.Call({
                target: compressedCalls[i].target,
                callData: compressedCalls[i].callData,
                value: 0
            });
        }

        // Add final call to drain leftover tokens back to this contract
        calls[callCount] = MulticallHandler.Call({
            target: multicallHandler,
            callData: abi.encodeWithSelector(
                MulticallHandler.drainLeftoverTokens.selector,
                finalToken,
                fallbackRecipient
            ),
            value: 0
        });

        // Build Instructions struct
        MulticallHandler.Instructions memory instructions = MulticallHandler.Instructions({
            calls: calls,
            fallbackRecipient: fallbackRecipient
        });

        return abi.encode(instructions);
    }

    /// @notice Calcualtes proportional fees to sponsor in finalToken, given the fees to sponsor in initial token and initial amount
    function _calcExtraFeesFinal(
        uint256 amount,
        uint256 extraFeesToSponsorTokenIn,
        uint256 finalAmount
    ) internal pure returns (uint256 extraFeesToSponsorFinalToken) {
        // Total amount to sponsor is the extra fees to sponsor, ceiling division.
        uint256 bpsToSponsor;
        {
            uint256 totalAmount = amount + extraFeesToSponsorTokenIn;
            bpsToSponsor = ((extraFeesToSponsorTokenIn * BPS_PRECISION_SCALAR) + totalAmount - 1) / totalAmount;
        }

        // Apply the bps to sponsor to the final amount to get the amount to sponsor, ceiling division.
        uint256 bpsToSponsorAdjusted = BPS_PRECISION_SCALAR - bpsToSponsor;
        extraFeesToSponsorFinalToken =
            (((finalAmount * BPS_PRECISION_SCALAR) + bpsToSponsorAdjusted - 1) / bpsToSponsorAdjusted) -
            finalAmount;
    }

    /// @notice Allow contract to receive native tokens for arbitrary action execution
    receive() external payable virtual {}
}

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IMessageTransmitterV2 } from "../../../external/interfaces/CCTPInterfaces.sol";
import { SponsoredCCTPQuoteLib } from "../../../libraries/SponsoredCCTPQuoteLib.sol";
import { SponsoredCCTPInterface } from "../../../interfaces/SponsoredCCTPInterface.sol";
import { Bytes32ToAddress } from "../../../libraries/AddressConverters.sol";
import { HyperCoreFlowExecutor } from "../HyperCoreFlowExecutor.sol";
import { ArbitraryEVMFlowExecutor } from "../ArbitraryEVMFlowExecutor.sol";
import { CommonFlowParams, EVMFlowParams } from "../Structs.sol";

/**
 * @title SponsoredCCTPDstPeriphery
 * @notice Destination chain periphery contract that supports sponsored/non-sponsored CCTP deposits.
 * @dev This contract is used to receive tokens via CCTP and execute the flow accordingly.
 */
contract SponsoredCCTPDstPeriphery is SponsoredCCTPInterface, HyperCoreFlowExecutor, ArbitraryEVMFlowExecutor {
    using SafeERC20 for IERC20Metadata;
    using Bytes32ToAddress for bytes32;

    /// @notice The CCTP message transmitter contract.
    IMessageTransmitterV2 public immutable cctpMessageTransmitter;

    /// @notice The public key of the signer that was used to sign the quotes.
    address public signer;

    /// @notice Allow a buffer for quote deadline validation. CCTP transfer might have taken a while to finalize
    uint256 public quoteDeadlineBuffer = 30 minutes;

    /// @notice A mapping of used nonces to prevent replay attacks.
    mapping(bytes32 => bool) public usedNonces;

    /**
     * @notice Constructor for the SponsoredCCTPDstPeriphery contract.
     * @param _cctpMessageTransmitter The address of the CCTP message transmitter contract.
     * @param _signer The address of the signer that was used to sign the quotes.
     * @param _donationBox The address of the donation box contract. This is used to store funds that are used for sponsored flows.
     * @param _baseToken The address of the base token which would be the USDC on HyperEVM.
     * @param _multicallHandler The address of the multicall handler contract.
     */
    constructor(
        address _cctpMessageTransmitter,
        address _signer,
        address _donationBox,
        address _baseToken,
        address _multicallHandler
    ) HyperCoreFlowExecutor(_donationBox, _baseToken) ArbitraryEVMFlowExecutor(_multicallHandler) {
        cctpMessageTransmitter = IMessageTransmitterV2(_cctpMessageTransmitter);
        signer = _signer;
    }

    /**
     * @notice Sets the signer address that is used to validate the signatures of the quotes.
     * @param _signer The new signer address.
     */
    function setSigner(address _signer) external nonReentrant onlyDefaultAdmin {
        signer = _signer;
    }

    /**
     * @notice Sets the quote deadline buffer. This is used to prevent the quote from being used after it has expired.
     * @param _quoteDeadlineBuffer The new quote deadline buffer.
     */
    function setQuoteDeadlineBuffer(uint256 _quoteDeadlineBuffer) external nonReentrant onlyDefaultAdmin {
        quoteDeadlineBuffer = _quoteDeadlineBuffer;
    }

    /**
     * @notice Receives a message from CCTP and executes the flow accordingly. This function first calls the
     * CCTP message transmitter to receive the funds before validating the quote and executing the flow.
     * @param message The message that is received from CCTP.
     * @param attestation The attestation that is received from CCTP.
     * @param signature The signature of the quote.
     */
    function receiveMessage(
        bytes memory message,
        bytes memory attestation,
        bytes memory signature
    ) external nonReentrant {
        cctpMessageTransmitter.receiveMessage(message, attestation);

        // If the hook data is invalid or the mint recipient is not this contract we cannot process the message
        // and therefore we exit. In this case the funds will be kept in this contract.
        if (!SponsoredCCTPQuoteLib.validateMessage(message)) {
            return;
        }

        // Extract the quote and the fee that was executed from the message.
        (SponsoredCCTPInterface.SponsoredCCTPQuote memory quote, uint256 feeExecuted) = SponsoredCCTPQuoteLib
            .getSponsoredCCTPQuoteData(message);

        // Validate the quote and the signature.
        bool isQuoteValid = _isQuoteValid(quote, signature);
        if (isQuoteValid) {
            usedNonces[quote.nonce] = true;
        }

        uint256 amountAfterFees = quote.amount - feeExecuted;

        CommonFlowParams memory commonParams = CommonFlowParams({
            amountInEVM: amountAfterFees,
            quoteNonce: quote.nonce,
            finalRecipient: quote.finalRecipient.toAddress(),
            // If the quote is invalid we don't want to swap, so we use the base token as the final token
            finalToken: isQuoteValid ? quote.finalToken.toAddress() : baseToken,
            // If the quote is invalid we don't sponsor the flow or the extra fees
            maxBpsToSponsor: isQuoteValid ? quote.maxBpsToSponsor : 0,
            extraFeesIncurred: feeExecuted
        });

        // Route to appropriate execution based on executionMode
        if (
            isQuoteValid &&
            (quote.executionMode == uint8(ExecutionMode.ArbitraryActionsToCore) ||
                quote.executionMode == uint8(ExecutionMode.ArbitraryActionsToEVM))
        ) {
            // Execute flow with arbitrary evm actions
            _executeWithEVMFlow(
                EVMFlowParams({
                    commonParams: commonParams,
                    initialToken: baseToken,
                    actionData: quote.actionData,
                    transferToCore: quote.executionMode == uint8(ExecutionMode.ArbitraryActionsToCore)
                })
            );
        } else {
            // Execute standard HyperCore flow (default)
            HyperCoreFlowExecutor._executeFlow(commonParams, quote.maxUserSlippageBps);
        }
    }

    function _isQuoteValid(
        SponsoredCCTPInterface.SponsoredCCTPQuote memory quote,
        bytes memory signature
    ) internal view returns (bool) {
        return
            SponsoredCCTPQuoteLib.validateSignature(signer, quote, signature) &&
            !usedNonces[quote.nonce] &&
            quote.deadline + quoteDeadlineBuffer >= block.timestamp;
    }

    function _executeWithEVMFlow(EVMFlowParams memory params) internal {
        params.commonParams = ArbitraryEVMFlowExecutor._executeFlow(params);

        // Route to appropriate destination based on transferToCore flag
        (params.transferToCore ? _executeSimpleTransferFlow : _fallbackHyperEVMFlow)(params.commonParams);
    }
}

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

import { ILayerZeroComposer } from "../../../external/interfaces/ILayerZeroComposer.sol";
import { OFTComposeMsgCodec } from "../../../external/libraries/OFTComposeMsgCodec.sol";
import { ComposeMsgCodec } from "./ComposeMsgCodec.sol";
import { ExecutionMode } from "./Structs.sol";
import { AddressToBytes32, Bytes32ToAddress } from "../../../libraries/AddressConverters.sol";
import { IOFT, IOAppCore } from "../../../interfaces/IOFT.sol";
import { HyperCoreFlowExecutor } from "../HyperCoreFlowExecutor.sol";
import { ArbitraryEVMFlowExecutor } from "../ArbitraryEVMFlowExecutor.sol";
import { CommonFlowParams, EVMFlowParams } from "../Structs.sol";

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

/// @notice Handler that receives funds from LZ system, checks authorizations(both against LZ system and src chain
/// sender), and forwards authorized params to the `_executeFlow` function
contract DstOFTHandler is ILayerZeroComposer, HyperCoreFlowExecutor, ArbitraryEVMFlowExecutor {
    using ComposeMsgCodec for bytes;
    using Bytes32ToAddress for bytes32;
    using AddressToBytes32 for address;
    using SafeERC20 for IERC20;

    /// @notice We expect bridge amount that comes through to this Handler to be 1:1 with the src send amount, and we
    /// require our src handler to ensure that it is. We don't sponsor extra bridge fees in this handler
    uint256 public constant EXTRA_FEES_TO_SPONSOR = 0;

    address public immutable OFT_ENDPOINT_ADDRESS;
    address public immutable IOFT_ADDRESS;

    /// @notice A mapping used to validate an incoming message against a list of authorized src periphery contracts. In
    /// bytes32 to support non-EVM src chains
    mapping(uint64 eid => bytes32 authorizedSrcPeriphery) public authorizedSrcPeripheryContracts;

    /// @notice A mapping used for nonce uniqueness checks. Our src periphery and LZ should have prevented this already,
    /// but I guess better safe than sorry
    mapping(bytes32 quoteNonce => bool used) public usedNonces;

    /// @notice Emitted when a new authorized src periphery is configured
    event SetAuthorizedPeriphery(uint32 srcEid, bytes32 srcPeriphery);

    /// @notice Thrown when trying to call lzCompose from a source periphery that's not been configured in `authorizedSrcPeripheryContracts`
    error AuthorizedPeripheryNotSet(uint32 _srcEid);
    /// @notice Thrown when source chain recipient is not authorized periphery contract
    error UnauthorizedSrcPeriphery(uint32 _srcEid);
    /// @notice Thrown when the supplied token does not match the supplied ioft messenger
    error TokenIOFTMismatch();
    /// @notice Thrown when the supplied ioft address does not match the supplied endpoint address
    error IOFTEndpointMismatch();
    /// @notice Thrown if Quote nonce was already used
    error NonceAlreadyUsed();
    /// @notice Thrown if supplied OApp is not configured ioft
    error InvalidOApp();
    /// @notice Thrown if called by an unauthorized endpoint
    error UnauthorizedEndpoint();
    /// @notice Thrown when supplied _composeMsg format is unexpected
    error InvalidComposeMsgFormat();

    constructor(
        address _oftEndpoint,
        address _ioft,
        address _donationBox,
        address _baseToken,
        address _multicallHandler
    ) HyperCoreFlowExecutor(_donationBox, _baseToken) ArbitraryEVMFlowExecutor(_multicallHandler) {
        // baseToken is assigned on `HyperCoreFlowExecutor` creation
        if (baseToken != IOFT(_ioft).token()) {
            revert TokenIOFTMismatch();
        }

        OFT_ENDPOINT_ADDRESS = _oftEndpoint;
        IOFT_ADDRESS = _ioft;
        if (address(IOAppCore(IOFT_ADDRESS).endpoint()) != address(OFT_ENDPOINT_ADDRESS)) {
            revert IOFTEndpointMismatch();
        }
    }

    function setAuthorizedPeriphery(uint32 srcEid, bytes32 srcPeriphery) external nonReentrant onlyDefaultAdmin {
        authorizedSrcPeripheryContracts[srcEid] = srcPeriphery;
        emit SetAuthorizedPeriphery(srcEid, srcPeriphery);
    }

    /**
     * @notice Handles incoming composed messages from LayerZero.
     * @dev Ensures the message comes from the correct OApp and is sent through the authorized endpoint.
     *
     * @param _oApp The address of the OApp that is sending the composed message.
     */
    function lzCompose(
        address _oApp,
        bytes32 /* _guid */,
        bytes calldata _message,
        address /* _executor */,
        bytes calldata /* _extraData */
    ) external payable override nonReentrant {
        _requireAuthorizedMessage(_oApp, _message);

        // Decode the actual `composeMsg` payload to extract the recipient address
        bytes memory composeMsg = OFTComposeMsgCodec.composeMsg(_message);

        // This check is a safety mechanism against blackholing funds. The funds were sent by the authorized periphery
        // contract, but if the length is unexpected, we require funds be rescued, this is not a situation we aim to
        // revover from in `lzCompose` call
        if (composeMsg._isValidComposeMsgBytelength() == false) {
            revert InvalidComposeMsgFormat();
        }

        bytes32 quoteNonce = composeMsg._getNonce();
        if (usedNonces[quoteNonce]) {
            revert NonceAlreadyUsed();
        }
        usedNonces[quoteNonce] = true;

        uint256 amountLD = OFTComposeMsgCodec.amountLD(_message);
        uint256 maxBpsToSponsor = composeMsg._getMaxBpsToSponsor();
        uint256 maxUserSlippageBps = composeMsg._getMaxUserSlippageBps();
        address finalRecipient = composeMsg._getFinalRecipient().toAddress();
        address finalToken = composeMsg._getFinalToken().toAddress();
        uint8 executionMode = composeMsg._getExecutionMode();
        bytes memory actionData = composeMsg._getActionData();

        CommonFlowParams memory commonParams = CommonFlowParams({
            amountInEVM: amountLD,
            quoteNonce: quoteNonce,
            finalRecipient: finalRecipient,
            finalToken: finalToken,
            maxBpsToSponsor: maxBpsToSponsor,
            extraFeesIncurred: EXTRA_FEES_TO_SPONSOR
        });

        // Route to appropriate execution based on executionMode
        if (
            executionMode == uint8(ExecutionMode.ArbitraryActionsToCore) ||
            executionMode == uint8(ExecutionMode.ArbitraryActionsToEVM)
        ) {
            // Execute flow with arbitrary evm actions
            _executeWithEVMFlow(
                EVMFlowParams({
                    commonParams: commonParams,
                    initialToken: baseToken,
                    actionData: actionData,
                    transferToCore: executionMode == uint8(ExecutionMode.ArbitraryActionsToCore)
                })
            );
        } else {
            // Execute standard HyperCore flow (default)
            HyperCoreFlowExecutor._executeFlow(commonParams, maxUserSlippageBps);
        }
    }

    function _executeWithEVMFlow(EVMFlowParams memory params) internal {
        params.commonParams = ArbitraryEVMFlowExecutor._executeFlow(params);

        // Route to appropriate destination based on transferToCore flag
        (params.transferToCore ? _executeSimpleTransferFlow : _fallbackHyperEVMFlow)(params.commonParams);
    }

    /// @notice Checks that message was authorized by LayerZero's identity system and that it came from authorized src periphery
    function _requireAuthorizedMessage(address _oApp, bytes calldata _message) internal view {
        if (_oApp != IOFT_ADDRESS) {
            revert InvalidOApp();
        }
        if (msg.sender != OFT_ENDPOINT_ADDRESS) {
            revert UnauthorizedEndpoint();
        }
        _requireAuthorizedPeriphery(_message);
    }

    /// @dev Checks that _message came from the authorized src periphery contract stored in `authorizedSrcPeripheryContracts`
    function _requireAuthorizedPeriphery(bytes calldata _message) internal view {
        uint32 _srcEid = OFTComposeMsgCodec.srcEid(_message);
        bytes32 authorizedPeriphery = authorizedSrcPeripheryContracts[_srcEid];
        if (authorizedPeriphery == bytes32(0)) {
            revert AuthorizedPeripheryNotSet(_srcEid);
        }

        // Decode original sender
        bytes32 _composeFromBytes32 = OFTComposeMsgCodec.composeFrom(_message);

        // We don't allow arbitrary src chain callers. If such a caller does send a message to this handler, the funds
        // will remain in this contract and will have to be rescued by an admin rescue function
        if (authorizedPeriphery != _composeFromBytes32) {
            revert UnauthorizedSrcPeriphery(_srcEid);
        }
    }
}


Vulnerability type: CWE-707|CWE-20|CWE-1287