MEDIUM severity: WrapAndSupply prevents supplying on host when gasFee is set. The wrapAndSupplyOnExtensionMarket function in WrapAndSupply.sol uses all of the msg.value to wrap native tokens, leaving no value to cover the gasFee required by mTokenGateway. The root cause is the failure to account for the gasFee when calculating the amount to wrap. A user calling this function...

Vulnerable code:
// Copyright (c) 2025 Merge Layers Inc.
//
// This source code is licensed under the Business Source License 1.1
// (the "License"); you may not use this file except in compliance with the
// License. You may obtain a copy of the License at
//
//     https://github.com/malda-protocol/malda-lending/blob/main/LICENSE-BSL
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This file contains code derived from or inspired by Compound V2,
// originally licensed under the BSD 3-Clause License. See LICENSE-COMPOUND-V2
// for original license terms and attributions.

// SPDX-License-Identifier: AGPL-3.0
pragma solidity =0.8.28;

/*
 _____ _____ __    ____  _____ 
|     |  _  |  |  |    \|  _  |
| | | |     |  |__|  |  |     |
|_|_|_|__|__|_____|____/|__|__|   
*/

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

import {ImErc20} from "src/interfaces/ImErc20.sol";
import {ImTokenMinimal} from "src/interfaces/ImToken.sol";
import {ImTokenGateway} from "src/interfaces/ImTokenGateway.sol";

interface IWrappedNative {
    function deposit() external payable;
    function transfer(address to, uint256 value) external returns (bool);
    function withdraw(uint256) external;
}

contract WrapAndSupply {
    IWrappedNative public immutable wrappedNative;

    // ----------- ERRORS ------------
    error WrapAndSupply_AddressNotValid();
    error WrapAndSupply_AmountNotValid();

    // ----------- EVENTS ------------
    event WrappedAndSupplied(address indexed sender, address indexed receiver, address indexed market, uint256 amount);

    constructor(address _wrappedNative) {
        require(_wrappedNative != address(0), WrapAndSupply_AddressNotValid());
        wrappedNative = IWrappedNative(_wrappedNative);
    }

    // ----------- PUBLIC ------------
    /**
     * @notice Wraps a native coin into its wrapped version and supplies on a host market
     * @param mToken The market address
     * @param receiver The mToken receiver
     */
    function wrapAndSupplyOnHostMarket(address mToken, address receiver, uint256 minAmount) external payable {
        address underlying = ImTokenMinimal(mToken).underlying();
        require(underlying == address(wrappedNative), WrapAndSupply_AddressNotValid());

        uint256 amount = _wrap();

        IERC20(underlying).approve(mToken, 0);
        IERC20(underlying).approve(mToken, amount);
        ImErc20(mToken).mint(amount, receiver, minAmount);

        emit WrappedAndSupplied(msg.sender, receiver, mToken, amount);
    }

    /**
     * @notice Wraps a native coin into its wrapped version and supplies on an extension market
     * @param mTokenGateway The market address
     * @param receiver The receiver
     * @param selector The host chain function selector
     */
    function wrapAndSupplyOnExtensionMarket(address mTokenGateway, address receiver, bytes4 selector)
        external
        payable
    {
        address underlying = ImTokenGateway(mTokenGateway).underlying();
        require(underlying == address(wrappedNative), WrapAndSupply_AddressNotValid());

        uint256 amount = _wrap();

        IERC20(underlying).approve(mTokenGateway, 0);
        IERC20(underlying).approve(mTokenGateway, amount);
        ImTokenGateway(mTokenGateway).supplyOnHost(amount, receiver, selector);
    }

    // ----------- PRIVATE ------------
    function _wrap() private returns (uint256) {
        uint256 amount = msg.value;
        require(amount > 0, WrapAndSupply_AmountNotValid());

        wrappedNative.deposit{value: amount}();
        return amount;
    }
}

// Copyright (c) 2025 Merge Layers Inc.
//
// This source code is licensed under the Business Source License 1.1
// (the "License"); you may not use this file except in compliance with the
// License. You may obtain a copy of the License at
//
//     https://github.com/malda-protocol/malda-lending/blob/main/LICENSE-BSL
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This file contains code derived from or inspired by Compound V2,
// originally licensed under the BSD 3-Clause License. See LICENSE-COMPOUND-V2
// for original license terms and attributions.

// SPDX-License-Identifier: BSL-1.1
pragma solidity =0.8.28;

/*
 _____ _____ __    ____  _____ 
|     |  _  |  |  |    \|  _  |
| | | |     |  |__|  |  |     |
|_|_|_|__|__|_____|____/|__|__|   
*/

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

// contracts
import {IRoles} from "src/interfaces/IRoles.sol";
import {IBlacklister} from "src/interfaces/IBlacklister.sol";
import {ImTokenGateway} from "src/interfaces/ImTokenGateway.sol";
import {ImTokenOperationTypes} from "src/interfaces/ImToken.sol";

import {mTokenProofDecoderLib} from "src/libraries/mTokenProofDecoderLib.sol";

import {IZkVerifier} from "src/verifier/ZkVerifier.sol";

contract mTokenGateway is OwnableUpgradeable, ImTokenGateway, ImTokenOperationTypes {
    using SafeERC20 for IERC20;

    // ----------- STORAGE -----------
    /**
     * @inheritdoc ImTokenGateway
     */
    IRoles public rolesOperator;

    /**
     * @inheritdoc ImTokenGateway
     */
    IBlacklister public blacklistOperator;

    IZkVerifier public verifier;

    mapping(OperationType => bool) public paused;

    /**
     * @inheritdoc ImTokenGateway
     */
    address public underlying;

    mapping(address => uint256) public accAmountIn;
    mapping(address => uint256) public accAmountOut;
    mapping(address => mapping(address => bool)) public allowedCallers;
    mapping(address => bool) public userWhitelisted;
    bool public whitelistEnabled;

    uint32 private constant LINEA_CHAIN_ID = 59144;

    ///@dev gas fee for `supplyOnHost`
    uint256 public gasFee;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(address payable _owner, address _underlying, address _roles, address _blacklister, address zkVerifier_)
        external
        initializer
    {
        __Ownable_init(_owner);
        require(_roles != address(0), mTokenGateway_AddressNotValid());
        require(zkVerifier_ != address(0), mTokenGateway_AddressNotValid());
        require(_blacklister != address(0), mTokenGateway_AddressNotValid());
        require(_underlying != address(0), mTokenGateway_AddressNotValid());
        require(_roles != address(0), mTokenGateway_AddressNotValid());

        underlying = _underlying;
        rolesOperator = IRoles(_roles);
        blacklistOperator = IBlacklister(_blacklister);

        verifier = IZkVerifier(zkVerifier_);
    }

    modifier notPaused(OperationType _type) {
        require(!paused[_type], mTokenGateway_Paused(_type));
        _;
    }

    modifier onlyAllowedUser(address user) {
        if (whitelistEnabled) {
            require(userWhitelisted[user], mTokenGateway_UserNotWhitelisted());
        }
        _;
    }

    modifier ifNotBlacklisted(address user) {
        require (!blacklistOperator.isBlacklisted(user), mTokenGateway_UserBlacklisted());
        _;
    }

    // ----------- VIEW ------------
    /**
     * @inheritdoc ImTokenGateway
     */
    function isPaused(OperationType _type) external view returns (bool) {
        return paused[_type];
    }

    /**
     * @inheritdoc ImTokenGateway
     */
    function getProofData(address user, uint32) external view returns (uint256, uint256) {
        return (accAmountIn[user], accAmountOut[user]);
    }

    // ----------- OWNER ------------
    /**
     * @notice Sets user whitelist status
     * @param user The user address
     * @param state The new staate
     */
    function setWhitelistedUser(address user, bool state) external onlyOwner {
        userWhitelisted[user] = state;
        emit mTokenGateway_UserWhitelisted(user, state);
    }

    /**
     * @notice Enable user whitelist
     */
    function enableWhitelist() external onlyOwner {
        whitelistEnabled = true;
        emit mTokenGateway_WhitelistEnabled();
    }

    /**
     * @notice Disable user whitelist
     */
    function disableWhitelist() external onlyOwner {
        whitelistEnabled = false;
        emit mTokenGateway_WhitelistDisabled();
    }

    /**
     * @inheritdoc ImTokenGateway
     */
    function setPaused(OperationType _type, bool state) external override {
        if (state) {
            require(
                msg.sender == owner() || rolesOperator.isAllowedFor(msg.sender, rolesOperator.GUARDIAN_PAUSE()),
                mTokenGateway_CallerNotAllowed()
            );
        } else {
            require(msg.sender == owner(), mTokenGateway_CallerNotAllowed());
        }

        emit mTokenGateway_PausedState(_type, state);
        paused[_type] = state;
    }

    /**
     * @inheritdoc ImTokenGateway
     */
    function extractForRebalancing(uint256 amount) external notPaused(OperationType.Rebalancing) {
        if (!rolesOperator.isAllowedFor(msg.sender, rolesOperator.REBALANCER())) revert mTokenGateway_NotRebalancer();
        IERC20(underlying).safeTransfer(msg.sender, amount);
    }

    /**
     * @notice Sets the gas fee
     * @param amount the new gas fee
     */
    function setGasFee(uint256 amount) external onlyOwner {
        gasFee = amount;
        emit mTokenGateway_GasFeeUpdated(amount);
    }

    /**
     * @notice Withdraw gas received so far
     * @param receiver the receiver address
     */
    function withdrawGasFees(address payable receiver) external {
        if (msg.sender != owner() && !_isAllowedFor(msg.sender, _getSequencerRole())) {
            revert mTokenGateway_CallerNotAllowed();
        }
        uint256 balance = address(this).balance;
        receiver.transfer(balance);
    }

    /**
     * @notice Updates IZkVerifier address
     * @param _zkVerifier the verifier address
     */
    function updateZkVerifier(address _zkVerifier) external onlyOwner {
        require(_zkVerifier != address(0), mTokenGateway_AddressNotValid());
        emit ZkVerifierUpdated(address(verifier), _zkVerifier);
        verifier = IZkVerifier(_zkVerifier);
    }

    // ----------- PUBLIC ------------
    /**
     * @inheritdoc ImTokenGateway
     */
    function updateAllowedCallerStatus(address caller, bool status) external override {
        allowedCallers[msg.sender][caller] = status;
        emit AllowedCallerUpdated(msg.sender, caller, status);
    }

    /**
     * @inheritdoc ImTokenGateway
     */
    function supplyOnHost(uint256 amount, address receiver, bytes4 lineaSelector)
        external
        payable
        override
        notPaused(OperationType.AmountIn)
        onlyAllowedUser(msg.sender)
        ifNotBlacklisted(msg.sender)
        ifNotBlacklisted(receiver)
    {
        // checks
        require(amount > 0, mTokenGateway_AmountNotValid());
        require(msg.value >= gasFee, mTokenGateway_NotEnoughGasFee());

        IERC20(underlying).safeTransferFrom(msg.sender, address(this), amount);

        // effects
        accAmountIn[receiver] += amount;

        emit mTokenGateway_Supplied(
            msg.sender,
            receiver,
            accAmountIn[receiver],
            accAmountOut[receiver],
            amount,
            uint32(block.chainid),
            LINEA_CHAIN_ID,
            lineaSelector
        );
    }

    /**
     * @inheritdoc ImTokenGateway
     */
    function outHere(bytes calldata journalData, bytes calldata seal, uint256[] calldata amounts, address receiver)
        external
        notPaused(OperationType.AmountOutHere)
        ifNotBlacklisted(msg.sender)
        ifNotBlacklisted(receiver)
    {
        // verify received data
        if (!rolesOperator.isAllowedFor(msg.sender, rolesOperator.PROOF_BATCH_FORWARDER())) {
            _verifyProof(journalData, seal);
        }

        bytes[] memory journals = abi.decode(journalData, (bytes[]));
        uint256 length = journals.length;
        require(length == amounts.length, mTokenGateway_LengthNotValid());

        for (uint256 i; i < journals.length;) {
            _outHere(journals[i], amounts[i], receiver);

            unchecked {
                ++i;
            }
        }
    }

    function _outHere(bytes memory journalData, uint256 amount, address receiver) internal {
        (address _sender, address _market,, uint256 _accAmountOut, uint32 _chainId, uint32 _dstChainId,) =
            mTokenProofDecoderLib.decodeJournal(journalData);

        // temporary overwrite; will be removed in future implementations
        receiver = _sender;

        // checks
        _checkSender(msg.sender, _sender);
        require(_market == address(this), mTokenGateway_AddressNotValid());
        require(_chainId == LINEA_CHAIN_ID, mTokenGateway_ChainNotValid()); // allow only Host
        require(_dstChainId == uint32(block.chainid), mTokenGateway_ChainNotValid());
        require(amount > 0, mTokenGateway_AmountNotValid());
        require(_accAmountOut - accAmountOut[_sender] >= amount, mTokenGateway_AmountTooBig());
        require(IERC20(underlying).balanceOf(address(this)) >= amount, mTokenGateway_ReleaseCashNotAvailable());

        // effects
        accAmountOut[_sender] += amount;

        // interactions
        IERC20(underlying).safeTransfer(_sender, amount);

        emit mTokenGateway_Extracted(
            msg.sender,
            _sender,
            receiver,
            accAmountIn[_sender],
            accAmountOut[_sender],
            amount,
            uint32(_chainId),
            uint32(block.chainid)
        );
    }

    // ----------- PRIVATE ------------
    function _verifyProof(bytes calldata journalData, bytes calldata seal) private view {
        require(journalData.length > 0, mTokenGateway_JournalNotValid());

        // Decode the dynamic array of journals.
        bytes[] memory journals = abi.decode(journalData, (bytes[]));

        // Check the L1Inclusion flag for each journal.
        bool isSequencer = _isAllowedFor(msg.sender, _getProofForwarderRole())
            || _isAllowedFor(msg.sender, _getBatchProofForwarderRole());

        if (!isSequencer) {
            for (uint256 i = 0; i < journals.length; i++) {
                (,,,,,, bool L1Inclusion) = mTokenProofDecoderLib.decodeJournal(journals[i]);
                if (!L1Inclusion) {
                    revert mTokenGateway_L1InclusionRequired();
                }
            }
        }

        // verify it using the ZkVerifier contract
        verifier.verifyInput(journalData, seal);
    }

    function _checkSender(address msgSender, address srcSender) private view {
        if (msgSender != srcSender) {
            require(
                allowedCallers[srcSender][msgSender] || msgSender == owner()
                    || _isAllowedFor(msgSender, _getProofForwarderRole())
                    || _isAllowedFor(msgSender, _getBatchProofForwarderRole()),
                mTokenGateway_CallerNotAllowed()
            );
        }
    }

    function _getSequencerRole() private view returns (bytes32) {
        return rolesOperator.SEQUENCER();
    }

    function _getBatchProofForwarderRole() private view returns (bytes32) {
        return rolesOperator.PROOF_BATCH_FORWARDER();
    }

    function _getProofForwarderRole() private view returns (bytes32) {
        return rolesOperator.PROOF_FORWARDER();
    }

    function _isAllowedFor(address _sender, bytes32 role) private view returns (bool) {
        return rolesOperator.isAllowedFor(_sender, role);
    }
}


Vulnerability type: CWE-682