MEDIUM severity: First depositor can brick market by forcing very large borrow rate. The JumpRateModelV4 calculates the borrow rate based on utilization, which is defined as borrows / (cash + borrows - reserves). However, this formula can result in utilization exceeding 1e18 when cash + borrows - reserves is less than borrows. A malicious first depositor can manipulate the market st...

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: BSL-1.1
pragma solidity =0.8.28;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IInterestRateModel} from "src/interfaces/IInterestRateModel.sol";

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

/**
 * @title JumpRateModelV4
 * @notice Implementation of the IInterestRateModel interface for calculating interest rates
 */
contract JumpRateModelV4 is IInterestRateModel, Ownable {
    // ----------- STORAGE ------------

    error JumpRateModelV4_MultiplierNotValid();
    error JumpRateModelV4_InputNotValid();

    /**
     * @inheritdoc IInterestRateModel
     */
    uint256 public override blocksPerYear;

    /**
     * @inheritdoc IInterestRateModel
     */
    uint256 public override multiplierPerBlock;

    /**
     * @inheritdoc IInterestRateModel
     */
    uint256 public override baseRatePerBlock;

    /**
     * @inheritdoc IInterestRateModel
     */
    uint256 public override jumpMultiplierPerBlock;

    /**
     * @inheritdoc IInterestRateModel
     */
    uint256 public override kink;

    /**
     * @inheritdoc IInterestRateModel
     */
    string public override name;

    /**
     * @notice Construct an interest rate model
     * @param blocksPerYear_ The estimated number of blocks per year
     * @param baseRatePerYear The base APR, scaled by 1e18
     * @param multiplierPerYear The rate increase in interest wrt utilization, scaled by 1e18
     * @param jumpMultiplierPerYear The multiplier per block after utilization point
     * @param kink_ The utilization point where the jump multiplier applies
     * @param owner_ The owner of the contract
     * @param name_ A user-friendly name for the contract
     */
    constructor(
        uint256 blocksPerYear_,
        uint256 baseRatePerYear,
        uint256 multiplierPerYear,
        uint256 jumpMultiplierPerYear,
        uint256 kink_,
        address owner_,
        string memory name_
    ) Ownable(owner_) {
        blocksPerYear = blocksPerYear_;
        name = name_;
        _updateJumpRateModel(baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_);
    }

    // ----------- OWNER ------------

    /**
     * @notice Update the parameters of the interest rate model (only callable by owner, i.e. Timelock)
     * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by 1e18)
     * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by 1e18)
     * @param jumpMultiplierPerYear The multiplierPerBlock after hitting a specified utilization point
     * @param kink_ The utilization point at which the jump multiplier is applied
     */
    function updateJumpRateModel(
        uint256 baseRatePerYear,
        uint256 multiplierPerYear,
        uint256 jumpMultiplierPerYear,
        uint256 kink_
    ) external onlyOwner {
        _updateJumpRateModel(baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_);
    }

    /**
     * @notice Updates the blocksPerYear in order to make interest calculations simpler
     * @param blocksPerYear_ The new estimated eth blocks per year.
     */
    function updateBlocksPerYear(uint256 blocksPerYear_) external onlyOwner {
        blocksPerYear = blocksPerYear_;
    }

    // ----------- PUBLIC ------------
    /**
     * @inheritdoc IInterestRateModel
     */
    function isInterestRateModel() external pure override returns (bool) {
        return true;
    }

    /**
     * @inheritdoc IInterestRateModel
     */
    function utilizationRate(uint256 cash, uint256 borrows, uint256 reserves) public pure override returns (uint256) {
        if (borrows == 0) {
            return 0;
        }
        return borrows * 1e18 / (cash + borrows - reserves);
    }

    /**
     * @inheritdoc IInterestRateModel
     */
    function getBorrowRate(uint256 cash, uint256 borrows, uint256 reserves) public view override returns (uint256) {
        uint256 util = utilizationRate(cash, borrows, reserves);

        if (util <= kink) {
            return util * multiplierPerBlock / 1e18 + baseRatePerBlock;
        } else {
            uint256 normalRate = kink * multiplierPerBlock / 1e18 + baseRatePerBlock;
            uint256 excessUtil = util - kink;
            return excessUtil * jumpMultiplierPerBlock / 1e18 + normalRate;
        }
    }

    /**
     * @inheritdoc IInterestRateModel
     */
    function getSupplyRate(uint256 cash, uint256 borrows, uint256 reserves, uint256 reserveFactorMantissa)
        external
        view
        override
        returns (uint256)
    {
        uint256 oneMinusReserveFactor = 1e18 - reserveFactorMantissa;
        uint256 borrowRate = getBorrowRate(cash, borrows, reserves);
        uint256 rateToPool = borrowRate * oneMinusReserveFactor / 1e18;
        return utilizationRate(cash, borrows, reserves) * rateToPool / 1e18;
    }

    // ----------- PRIVATE ------------
    /**
     * @notice Internal function to update the parameters of the interest rate model
     * @param baseRatePerYear The base APR, scaled by 1e18
     * @param multiplierPerYear The rate increase wrt utilization, scaled by 1e18
     * @param jumpMultiplierPerYear The multiplier per block after utilization point
     * @param kink_ The utilization point where the jump multiplier applies
     */
    function _updateJumpRateModel(
        uint256 baseRatePerYear,
        uint256 multiplierPerYear,
        uint256 jumpMultiplierPerYear,
        uint256 kink_
    ) private {
        baseRatePerBlock = baseRatePerYear / blocksPerYear;
        multiplierPerBlock = multiplierPerYear * 1e18 / (blocksPerYear * kink_);
        jumpMultiplierPerBlock = jumpMultiplierPerYear / blocksPerYear;
        kink = kink_;

        emit NewInterestParams(baseRatePerBlock, multiplierPerBlock, jumpMultiplierPerBlock, kink);
    }
}

// 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;

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

// interfaces
import {ImToken, ImTokenMinimal} from "src/interfaces/ImToken.sol";
import {IInterestRateModel} from "src/interfaces/IInterestRateModel.sol";
import {IOperator, IOperatorDefender} from "src/interfaces/IOperator.sol";

// contracts
import {mTokenConfiguration} from "./mTokenConfiguration.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

abstract contract mToken is mTokenConfiguration, ReentrancyGuard {
    constructor() {
        borrowRateMaxMantissa = 0.0005e16;
    }
    /**
     * @notice Initialize the money market
     * @param operator_ The address of the Operator
     * @param interestRateModel_ The address of the interest rate model
     * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18
     * @param name_ EIP-20 name of this token
     * @param symbol_ EIP-20 symbol of this token
     * @param decimals_ EIP-20 decimal precision of this token
     */

    function _initializeMToken(
        address operator_,
        address interestRateModel_,
        uint256 initialExchangeRateMantissa_,
        string memory name_,
        string memory symbol_,
        uint8 decimals_
    ) internal {
        require(accrualBlockTimestamp == 0 && borrowIndex == 0, mt_AlreadyInitialized());
        require(initialExchangeRateMantissa_ > 0, mt_ExchangeRateNotValid());
        // Set initial exchange rate
        initialExchangeRateMantissa = initialExchangeRateMantissa_;

        _setOperator(operator_);

        accrualBlockTimestamp = _getBlockTimestamp();
        borrowIndex = mantissaOne;

        _setInterestRateModel(interestRateModel_);

        name = name_;
        symbol = symbol_;
        decimals = decimals_;
    }

    // ----------- TOKENS VIEW ------------
    /**
     * @inheritdoc ImToken
     */
    function allowance(address owner, address spender) external view override returns (uint256) {
        return transferAllowances[owner][spender];
    }

    /**
     * @inheritdoc ImTokenMinimal
     */
    function balanceOf(address owner) external view override returns (uint256) {
        return accountTokens[owner];
    }

    /**
     * @inheritdoc ImToken
     */
    function balanceOfUnderlying(address owner) external override returns (uint256) {
        Exp memory exchangeRate = Exp({mantissa: exchangeRateCurrent()});
        return mul_ScalarTruncate(exchangeRate, accountTokens[owner]);
    }

    // ----------- MARKETS VIEW ------------
    /**
     * @inheritdoc ImToken
     */
    function getAccountSnapshot(address account) external view override returns (uint256, uint256, uint256) {
        return (accountTokens[account], _borrowBalanceStored(account), _exchangeRateStored());
    }

    /**
     * @inheritdoc ImToken
     */
    function borrowRatePerBlock() external view override returns (uint256) {
        return IInterestRateModel(interestRateModel).getBorrowRate(_getCashPrior(), totalBorrows, totalReserves);
    }

    /**
     * @inheritdoc ImToken
     */
    function supplyRatePerBlock() external view override returns (uint256) {
        return IInterestRateModel(interestRateModel).getSupplyRate(
            _getCashPrior(), totalBorrows, totalReserves, reserveFactorMantissa
        );
    }

    /**
     * @inheritdoc ImToken
     */
    function borrowBalanceStored(address account) external view override returns (uint256) {
        return _borrowBalanceStored(account);
    }

    /**
     * @inheritdoc ImToken
     */
    function getCash() external view override returns (uint256) {
        return _getCashPrior();
    }

    /**
     * @inheritdoc ImToken
     */
    function exchangeRateStored() external view override returns (uint256) {
        return _exchangeRateStored();
    }

    // ----------- TOKENS PUBLIC ------------
    /**
     * @inheritdoc ImToken
     */
    function transfer(address dst, uint256 amount) external override nonReentrant returns (bool) {
        _transferTokens(msg.sender, msg.sender, dst, amount);

        return true;
    }

    /**
     * @inheritdoc ImToken
     */
    function transferFrom(address src, address dst, uint256 amount) external override nonReentrant returns (bool) {
        _transferTokens(msg.sender, src, dst, amount);

        return true;
    }

    /**
     * @inheritdoc ImToken
     */
    function approve(address spender, uint256 amount) external override returns (bool) {
        transferAllowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    // ----------- MARKETS PUBLIC ------------
    /**
     * @inheritdoc ImToken
     */
    function totalBorrowsCurrent() external override nonReentrant returns (uint256) {
        _accrueInterest();
        return totalBorrows;
    }

    /**
     * @inheritdoc ImToken
     */
    function borrowBalanceCurrent(address account) external override nonReentrant returns (uint256) {
        _accrueInterest();
        return _borrowBalanceStored(account);
    }

    /**
     * @inheritdoc ImToken
     */
    function exchangeRateCurrent() public override nonReentrant returns (uint256) {
        _accrueInterest();
        return _exchangeRateStored();
    }

    /**
     * @inheritdoc ImToken
     */
    function seize(address liquidator, address borrower, uint256 seizeTokens) external override nonReentrant {
        _seize(msg.sender, liquidator, borrower, seizeTokens);
    }

    /**
     * @inheritdoc ImToken
     */
    function reduceReserves(uint256 reduceAmount) external override nonReentrant {
        require(
            msg.sender == admin || rolesOperator.isAllowedFor(msg.sender, rolesOperator.GUARDIAN_RESERVE()),
            mt_OnlyAdminOrRole()
        );

        _accrueInterest();

        require(_getCashPrior() >= reduceAmount, mt_ReserveCashNotAvailable());
        require(reduceAmount <= totalReserves, mt_ReserveCashNotAvailable());

        /////////////////////////
        // EFFECTS & INTERACTIONS
        // (No safe failures beyond this point)
        // totalReserves - reduceAmount
        uint256 totalReservesNew = totalReserves - reduceAmount;

        // Store reserves[n+1] = reserves[n] - reduceAmount
        totalReserves = totalReservesNew;

        // doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred.
        _doTransferOut(payable(msg.sender), reduceAmount);
        totalUnderlying -= reduceAmount;

        emit ReservesReduced(admin, reduceAmount, totalReservesNew);
    }

    // ----------- INTERNAL VIEW ------------
    /**
     * @notice Return the borrow balance of account based on stored data
     * @param account The address whose balance should be calculated
     * @return (error code, the calculated balance or 0 if error code is non-zero)
     */
    function _borrowBalanceStored(address account) internal view returns (uint256) {
        /* Get borrowBalance and borrowIndex */
        BorrowSnapshot storage borrowSnapshot = accountBorrows[account];

        /* If borrowBalance = 0 then borrowIndex is likely also 0.
         * Rather than failing the calculation with a division by 0, we immediately return 0 in this case.
         */
        if (borrowSnapshot.principal == 0) {
            return 0;
        }

        /* Calculate new borrow balance using the interest index:
         *  recentBorrowBalance = borrower.borrowBalance * market.borrowIndex / borrower.borrowIndex
         */
        uint256 principalTimesIndex = borrowSnapshot.principal * borrowIndex;
        return principalTimesIndex / borrowSnapshot.interestIndex;
    }

    // ----------- INTERNAL ------------
    /**
     * @notice Sender supplies assets into the market and receives mTokens in exchange
     * @dev Accrues interest whether or not the operation succeeds, unless reverted
     * @param user The user address
     * @param user The receiver address
     * @param mintAmount The amount of the underlying asset to supply
     * @param minAmountOut The minimum amount to be received
     * @param doTransfer If an actual transfer should be performed
     */
    function _mint(address user, address receiver, uint256 mintAmount, uint256 minAmountOut, bool doTransfer)
        internal
        nonReentrant
    {
        _accrueInterest();
        // emits the actual Mint event if successful and logs on errors, so we don't need to
        __mint(user, receiver, mintAmount, minAmountOut, doTransfer);
    }

    /**
     * @notice Sender redeems mTokens in exchange for the underlying asset
     * @dev Accrues interest whether or not the operation succeeds, unless reverted
     * @param user The user address
     * @param redeemTokens The number of mTokens to redeem into underlying
     * @param doTransfer If an actual transfer should be performed
     */
    function _redeem(address user, uint256 redeemTokens, bool doTransfer)
        internal
        nonReentrant
        returns (uint256 underlyingAmount)
    {
        _accrueInterest();
        // emits redeem-specific logs on errors, so we don't need to
        underlyingAmount = __redeem(payable(user), redeemTokens, 0, doTransfer);
    }

    /**
     * @notice Sender redeems mTokens in exchange for a specified amount of underlying asset
     * @dev Accrues interest whether or not the operation succeeds, unless reverted
     * @param user The user address
     * @param redeemAmount The amount of underlying to receive from redeeming mTokens
     * @param doTransfer If an actual transfer should be performed
     */
    function _redeemUnderlying(address user, uint256 redeemAmount, bool doTransfer) internal nonReentrant {
        _accrueInterest();
        // emits redeem-specific logs on errors, so we don't need to
        __redeem(payable(user), 0, redeemAmount, doTransfer);
    }

    /**
     * @notice Sender borrows assets from the protocol to their own address
     * @param user The user address
     * @param borrowAmount The amount of the underlying asset to borrow
     * @param doTransfer If an actual transfer should be performed
     */
    function _borrow(address user, uint256 borrowAmount, bool doTransfer) internal nonReentrant {
        _accrueInterest();
        // emits borrow-specific logs on errors, so we don't need to
        __borrow(payable(user), payable(user), borrowAmount, doTransfer);
    }

    /**
     * @notice Sender borrows assets from the protocol to their own address
     * @param user The user address
     * @param receiver The underlying receiver address
     * @param borrowAmount The amount of the underlying asset to borrow
     */
    function _borrowWithReceiver(address user, address receiver, uint256 borrowAmount) internal nonReentrant {
        _accrueInterest();
        __borrow(payable(user), payable(receiver), borrowAmount, true);
    }

    /**
     * @notice Sender repays their own borrow
     * @param repayAmount The amount to repay, or `type(uint256).max` for the full outstanding amount
     * @param doTransfer If an actual transfer should be performed
     */
    function _repay(uint256 repayAmount, bool doTransfer) internal nonReentrant returns (uint256) {
        _accrueInterest();
        // emits repay-borrow-specific logs on errors, so we don't need to
        return __repay(msg.sender, msg.sender, repayAmount, doTransfer);
    }

    /**
     * @notice Sender repays a borrow belonging to borrower
     * @param borrower the account with the debt being payed off
     * @param repayAmount The amount to repay, or `type(uint256).max` for the full outstanding amount
     * @param doTransfer If an actual transfer should be performed
     */
    function _repayBehalf(address borrower, uint256 repayAmount, bool doTransfer)
        internal
        nonReentrant
        returns (uint256)
    {
        _accrueInterest();
        // emits repay-borrow-specific logs on errors, so we don't need to
        return __repay(msg.sender, borrower, repayAmount, doTransfer);
    }

    /**
     * @notice The sender liquidates the borrowers collateral.
     *  The collateral seized is transferred to the liquidator.
     * @param liquidator The liquidator address
     * @param borrower The borrower of this mToken to be liquidated
     * @param mTokenCollateral The market in which to seize collateral from the borrower
     * @param repayAmount The amount of the underlying borrowed asset to repay
     * @param doTransfer If an actual transfer should be performed
     */
    function _liquidate(
        address liquidator,
        address borrower,
        uint256 repayAmount,
        address mTokenCollateral,
        bool doTransfer
    ) internal nonReentrant {
        _accrueInterest();

        ImToken(mTokenCollateral).accrueInterest();

        // emits borrow-specific logs on errors, so we don't need to
        __liquidate(liquidator, borrower, repayAmount, mTokenCollateral, doTransfer);
    }

    /**
     * @notice Transfers collateral tokens (this market) to the liquidator.
     * @dev Called only during an in-kind liquidation, or by liquidateBorrow during the liquidation of another mToken.
     *  Its absolutely critical to use msg.sender as the seizer mToken and not a parameter.
     * @param seizerToken The contract seizing the collateral (i.e. borrowed mToken)
     * @param liquidator The account receiving seized collateral
     * @param borrower The account having collateral seized
     * @param seizeTokens The number of mTokens to seize
     */
    function _seize(address seizerToken, address liquidator, address borrower, uint256 seizeTokens) internal {
        IOperatorDefender(operator).beforeMTokenSeize(address(this), seizerToken, liquidator, borrower);

        require(borrower != liquidator, mt_InvalidInput());

        /*
         * We calculate the new borrower and liquidator token balances, failing on underflow/overflow:
         *  borrowerTokensNew = accountTokens[borrower] - seizeTokens
         *  liquidatorTokensNew = accountTokens[liquidator] + seizeTokens
         */
        uint256 protocolSeizeTokens = mul_(seizeTokens, Exp({mantissa: PROTOCOL_SEIZE_SHARE_MANTISSA}));
        uint256 liquidatorSeizeTokens = seizeTokens - protocolSeizeTokens;
        Exp memory exchangeRate = Exp({mantissa: _exchangeRateStored()});
        uint256 protocolSeizeAmount = mul_ScalarTruncate(exchangeRate, protocolSeizeTokens);
        uint256 totalReservesNew = totalReserves + protocolSeizeAmount;

        /////////////////////////
        // EFFECTS & INTERACTIONS
        // (No safe failures beyond this point)

        /* We write the calculated values into storage */
        totalReserves = totalReservesNew;
        totalSupply = totalSupply - protocolSeizeTokens;
        accountTokens[borrower] = accountTokens[borrower] - seizeTokens;
        accountTokens[liquidator] = accountTokens[liquidator] + liquidatorSeizeTokens;

        /* Emit a Transfer event */
        emit Transfer(borrower, liquidator, liquidatorSeizeTokens);
        emit Transfer(borrower, address(this), protocolSeizeTokens);
        emit ReservesAdded(address(this), protocolSeizeAmount, totalReservesNew);
    }

    /**
     * @notice Accrues interest and reduces reserves by transferring from msg.sender
     * @param addAmount Amount of addition to reserves
     */
    function _addReserves(uint256 addAmount) internal nonReentrant {
        _accrueInterest();

        // totalReserves + actualAddAmount
        uint256 totalReservesNew;
        uint256 actualAddAmount;

        /////////////////////////
        // EFFECTS & INTERACTIONS
        // (No safe failures beyond this point)

        /*
         * We call doTransferIn for the caller and the addAmount
         *  Note: The mToken must handle variations between ERC-20 and ETH underlying.
         *  On success, the mToken holds an additional addAmount of cash.
         *  doTransferIn reverts if anything goes wrong, since we can't be sure if side effects occurred.
         *  it returns the amount actually transferred, in case of a fee.
         */

        actualAddAmount = _doTransferIn(msg.sender, addAmount);
        totalUnderlying += actualAddAmount;

        totalReservesNew = totalReserves + actualAddAmount;

        // Store reserves[n+1] = reserves[n] + actualAddAmount
        totalReserves = totalReservesNew;

        /* Emit NewReserves(admin, actualAddAmount, reserves[n+1]) */
        emit ReservesAdded(msg.sender, actualAddAmount, totalReservesNew);
    }

    // ----------- PRIVATE ------------
    /**
     * @notice The liquidator liquidates the borrowers collateral.
     *  The collateral seized is transferred to the liquidator.
     * @param liquidator The address repaying the borrow and seizing collateral
     * @param borrower The borrower of this mToken to be liquidated
     * @param mTokenCollateral The market in which to seize collateral from the borrower
     * @param repayAmount The amount of the underlying borrowed asset to repay
     * @param doTransfer If an actual transfer should be performed
     */
    function __liquidate(
        address liquidator,
        address borrower,
        uint256 repayAmount,
        address mTokenCollateral,
        bool doTransfer
    ) internal {
        require(borrower != liquidator, mt_InvalidInput());
        require(repayAmount > 0 && repayAmount != type(uint256).max, mt_InvalidInput());

        IOperatorDefender(operator).beforeMTokenLiquidate(address(this), mTokenCollateral, borrower, repayAmount);

        require(
            ImToken(mTokenCollateral).accrualBlockTimestamp() == _getBlockTimestamp(),
            mt_CollateralBlockTimestampNotValid()
        );

        /* Fail if repayBorrow fails */
        uint256 actualRepayAmount = __repay(liquidator, borrower, repayAmount, doTransfer);

        /////////////////////////
        // EFFECTS & INTERACTIONS
        // (No safe failures beyond this point)

        /* We calculate the number of collateral tokens that will be seized */
        uint256 seizeTokens =
            IOperator(operator).liquidateCalculateSeizeTokens(address(this), mTokenCollateral, actualRepayAmount);

        /* Revert if borrower collateral token balance < seizeTokens */
        require(ImToken(mTokenCollateral).balanceOf(borrower) >= seizeTokens, mt_LiquidateSeizeTooMuch());

        // If this is also the collateral, run _seize to avoid re-entrancy, otherwise make an external call
        if (address(mTokenCollateral) == address(this)) {
            _seize(address(this), liquidator, borrower, seizeTokens);
        } else {
            ImToken(mTokenCollateral).seize(liquidator, borrower, seizeTokens);
        }

        /* We emit a LiquidateBorrow event */
        emit LiquidateBorrow(liquidator, borrower, actualRepayAmount, address(mTokenCollateral), seizeTokens);
    }
    /**
     * @notice Borrows are repaid by another user (possibly the borrower).
     * @param payer the account paying off the borrow
     * @param borrower the account with the debt being payed off
     * @param repayAmount the amount of underlying tokens being returned, or `type(uint256).max` for the full outstanding amount
     * @param doTransfer If an actual transfer should be performed
     */

    function __repay(address payer, address borrower, uint256 repayAmount, bool doTransfer) private returns (uint256) {
        IOperatorDefender(operator).beforeMTokenRepay(address(this), borrower);

        /* We fetch the amount the borrower owes, with accumulated interest */
        uint256 accountBorrowsPrev = _borrowBalanceStored(borrower);

        /* If repayAmount == type(uint256).max , repayAmount = accountBorrows */
        uint256 repayAmountFinal = repayAmount == type(uint256).max ? accountBorrowsPrev : repayAmount;

        /////////////////////////
        // EFFECTS & INTERACTIONS
        // (No safe failures beyond this point)

        /*
         * We call _doTransferIn for the payer and the repayAmount
         *  Note: The mToken must handle variations between ERC-20 and ETH underlying.
         *  On success, the mToken holds an additional repayAmount of cash.
         *  _doTransferIn reverts if anything goes wrong, since we can't be sure if side effects occurred.
         *   it returns the amount actually transferred, in case of a fee.
         */
        uint256 actualRepayAmount = doTransfer ? _doTransferIn(payer, repayAmountFinal) : repayAmountFinal;
        totalUnderlying += actualRepayAmount;

        /*
         * We calculate the new borrower and total borrow balances, failing on underflow:
         *  accountBorrowsNew = accountBorrows - actualRepayAmount
         *  totalBorrowsNew = totalBorrows - actualRepayAmount
         */
        uint256 accountBorrowsNew = accountBorrowsPrev - actualRepayAmount;
        uint256 totalBorrowsNew = totalBorrows - actualRepayAmount;

        /* We write the previously calculated values into storage */
        accountBorrows[borrower].principal = accountBorrowsNew;
        accountBorrows[borrower].interestIndex = borrowIndex;
        totalBorrows = totalBorrowsNew;

        /* We emit a RepayBorrow event */
        emit RepayBorrow(payer, borrower, actualRepayAmount, accountBorrowsNew, totalBorrowsNew);

        return actualRepayAmount;
    }

    /**
     * @notice Users borrow assets from the protocol to their own address
     * @param borrowAmount The amount of the underlying asset to borrow
     */
    function __borrow(address payable borrower, address payable receiver, uint256 borrowAmount, bool doTransfer)
        private
    {
        IOperatorDefender(operator).beforeMTokenBorrow(address(this), borrower, borrowAmount);

        require(_getCashPrior() >= borrowAmount, mt_BorrowCashNotAvailable());

        /*
         * We calculate the new borrower and total borrow balances, failing on overflow:
         *  accountBorrowNew = accountBorrow + borrowAmount
         *  totalBorrowsNew = totalBorrows + borrowAmount
         */
        uint256 accountBorrowsPrev = _borrowBalanceStored(borrower);
        uint256 accountBorrowsNew = accountBorrowsPrev + borrowAmount;
        uint256 totalBorrowsNew = totalBorrows + borrowAmount;

        /////////////////////////
        // EFFECTS & INTERACTIONS
        // (No safe failures beyond this point)

        /*
         * We write the previously calculated values into storage.
         *  Note: Avoid token reentrancy attacks by writing increased borrow before external transfer.
        `*/
        accountBorrows[borrower].principal = accountBorrowsNew;
        accountBorrows[borrower].interestIndex = borrowIndex;
        totalBorrows = totalBorrowsNew;

        if (doTransfer) {
            /*
            * We invoke _doTransferOut for the borrower and the borrowAmount.
            *  Note: The mToken must handle variations between ERC-20 and ETH underlying.
            *  On success, the mToken borrowAmount less of cash.
            *  _doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred.
            */
            _doTransferOut(receiver, borrowAmount);
        }
        totalUnderlying -= borrowAmount;

        /* We emit a Borrow event */
        emit Borrow(borrower, borrowAmount, accountBorrowsNew, totalBorrowsNew);
    }

    function __redeem(address payable redeemer, uint256 redeemTokensIn, uint256 redeemAmountIn, bool doTransfer)
        private
        returns (uint256 redeemAmount)
    {
        require(redeemTokensIn == 0 || redeemAmountIn == 0, mt_InvalidInput());

        /* exchangeRate = invoke Exchange Rate Stored() */
        Exp memory exchangeRate = Exp({mantissa: _exchangeRateStored()});

        uint256 redeemTokens;
        /* If redeemTokensIn > 0: */
        if (redeemTokensIn > 0) {
            /*
             * We calculate the exchange rate and the amount of underlying to be redeemed:
             *  redeemTokens = redeemTokensIn
             *  redeemAmount = redeemTokensIn x exchangeRateCurrent
             */
            redeemTokens = redeemTokensIn;
            redeemAmount = mul_ScalarTruncate(exchangeRate, redeemTokensIn);
        } else {
            /*
             * We get the current exchange rate and calculate the amount to be redeemed:
             *  redeemTokens = redeemAmountIn / exchangeRate
             *  redeemAmount = redeemAmountIn
             */
            redeemTokens = div_(redeemAmountIn, exchangeRate);
            redeemAmount = redeemAmountIn;
        }
        if (redeemTokens == 0 && redeemAmount == 0) revert mt_RedeemEmpty();

        /* Fail if redeem not allowed */
        IOperatorDefender(operator).beforeMTokenRedeem(address(this), redeemer, redeemTokens);

        require(_getCashPrior() >= redeemAmount, mt_RedeemCashNotAvailable());

        /////////////////////////
        // EFFECTS & INTERACTIONS
        // (No safe failures beyond this point)

        /*
         * We write the previously calculated values into storage.
         *  Note: Avoid token reentrancy attacks by writing reduced supply before external transfer.
         */
        totalSupply = totalSupply - redeemTokens;
        accountTokens[redeemer] = accountTokens[redeemer] - redeemTokens;

        /*
         * We invoke _doTransferOut for the redeemer and the redeemAmount.
         *  Note: The mToken must handle variations between ERC-20 and ETH underlying.
         *  On success, the mToken has redeemAmount less of cash.
         *  _doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred.
         */
        if (doTransfer) _doTransferOut(redeemer, redeemAmount);
        totalUnderlying -= redeemAmount;

        /* We emit a Transfer event, and a Redeem event */
        emit Transfer(redeemer, address(this), redeemTokens);
        emit Redeem(redeemer, redeemAmount, redeemTokens);
    }
    /**
     * @notice User supplies assets into the market and receives mTokens in exchange
     * @dev Assumes interest has already been accrued up to the current block
     * @param minter The address of the account which is supplying the assets
     * @param receiver The address of the account which is receiving the assets
     * @param mintAmount The amount of the underlying asset to supply
     * @param minAmountOut The min amount to be received
     * @param doTransfer If an actual transfer should be performed
     */

    function __mint(address minter, address receiver, uint256 mintAmount, uint256 minAmountOut, bool doTransfer)
        private
    {
        IOperatorDefender(operator).beforeMTokenMint(address(this), minter);

        Exp memory exchangeRate = Exp({mantissa: _exchangeRateStored()});

        /////////////////////////
        // EFFECTS & INTERACTIONS
        // (No safe failures beyond this point)

        /*
         *  We call `_doTransferIn` for the minter and the mintAmount.
         *  Note: The mToken must handle variations between ERC-20 and ETH underlying.
         *  `_doTransferIn` reverts if anything goes wrong, since we can't be sure if
         *  side-effects occurred. The function returns the amount actually transferred,
         *  in case of a fee. On success, the mToken holds an additional `actualMintAmount`
         *  of cash.
         */
        uint256 actualMintAmount = doTransfer ? _doTransferIn(minter, mintAmount) : mintAmount;
        totalUnderlying += actualMintAmount;

        /*
         * We get the current exchange rate and calculate the number of mTokens to be minted:
         *  mintTokens = actualMintAmount / exchangeRate
         */

        uint256 mintTokens = div_(actualMintAmount, exchangeRate);
        require(mintTokens >= minAmountOut, mt_MinAmountNotValid());

        // avoid exchangeRate manipulation
        if (totalSupply == 0) {
            totalSupply = 1000;
            accountTokens[address(0)] = 1000;
            mintTokens -= 1000;
        }

        /*
         * We calculate the new total supply of mTokens and minter token balance, checking for overflow:
         *  totalSupplyNew = totalSupply + mintTokens
         *  accountTokensNew = accountTokens[minter] + mintTokens
         * And write them into storage
         */
        totalSupply = totalSupply + mintTokens;
        accountTokens[receiver] = accountTokens[receiver] + mintTokens;

        /* We emit a Mint event, and a Transfer event */
        emit Mint(minter, receiver, actualMintAmount, mintTokens);
        emit Transfer(address(this), receiver, mintTokens);

        /* We call the defense hook */
        IOperatorDefender(operator).afterMTokenMint(address(this));

        // Activate market by default if not entered already
        bool isEntered = IOperator(operator).checkMembership(minter, address(this));
        if (!isEntered) {
            IOperator(operator).enterMarketsWithSender(minter);
        }
    }

    /**
     * @notice Transfer `tokens` tokens from `src` to `dst` by `spender`
     * @dev Called by both `transfer` and `transferFrom` internally
     * @param spender The address of the account performing the transfer
     * @param src The address of the source account
     * @param dst The address of the destination account
     * @param tokens The number of tokens to transfer
     */
    function _transferTokens(address spender, address src, address dst, uint256 tokens) private {
        IOperatorDefender(operator).beforeMTokenTransfer(address(this), src, dst, tokens);

        require(src != dst, mt_TransferNotValid());

        /* Get the allowance, infinite for the account owner */
        uint256 startingAllowance = 0;
        if (spender == src) {
            startingAllowance = type(uint256).max;
        } else {
            startingAllowance = transferAllowances[src][spender];
        }

        /* Do the calculations, checking for {under,over}flow */
        uint256 allowanceNew = startingAllowance - tokens;
        uint256 srcTokensNew = accountTokens[src] - tokens;
        uint256 dstTokensNew = accountTokens[dst] + tokens;

        /////////////////////////
        // EFFECTS & INTERACTIONS
        // (No safe failures beyond this point)

        accountTokens[src] = srcTokensNew;
        accountTokens[dst] = dstTokensNew;

        /* Eat some of the allowance (if necessary) */
        if (startingAllowance != type(uint256).max) {
            transferAllowances[src][spender] = allowanceNew;
        }

        /* We emit a Transfer event */
        emit Transfer(src, dst, tokens);
    }
}

// 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;

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

// interfaces
import {IRoles} from "src/interfaces/IRoles.sol";
import {ImToken, ImTokenMinimal} from "src/interfaces/ImToken.sol";
import {IInterestRateModel} from "src/interfaces/IInterestRateModel.sol";

// contracts
import {ExponentialNoError} from "src/utils/ExponentialNoError.sol";

abstract contract mTokenStorage is ImToken, ExponentialNoError {
    // ----------- ACCESS STORAGE ------------
    /**
     * @inheritdoc ImToken
     */
    address payable public admin;

    /**
     * @inheritdoc ImToken
     */
    address payable public pendingAdmin;

    /**
     * @inheritdoc ImToken
     */
    address public operator;

    /**
     * @inheritdoc ImToken
     */
    IRoles public rolesOperator;

    // ----------- TOKENS STORAGE ------------
    /**
     * @inheritdoc ImTokenMinimal
     */
    string public name;

    /**
     * @inheritdoc ImTokenMinimal
     */
    string public symbol;

    /**
     * @inheritdoc ImTokenMinimal
     */
    uint8 public decimals;

    // ----------- MARKET STORAGE ------------
    /**
     * @inheritdoc ImToken
     */
    address public interestRateModel;

    /**
     * @inheritdoc ImToken
     */
    uint256 public reserveFactorMantissa;

    /**
     * @inheritdoc ImToken
     */
    uint256 public accrualBlockTimestamp;

    /**
     * @inheritdoc ImToken
     */
    uint256 public borrowIndex;

    /**
     * @inheritdoc ImToken
     */
    uint256 public totalBorrows;

    /**
     * @inheritdoc ImToken
     */
    uint256 public totalReserves;

    /**
     * @inheritdoc ImTokenMinimal
     */
    uint256 public totalSupply;

    /**
     * @inheritdoc ImTokenMinimal
     */
    uint256 public totalUnderlying;

    /**
     * @notice Maximum borrow rate that can ever be applied
     */
    uint256 public borrowRateMaxMantissa = 0.0005e16;

    /**
     * @notice Container for borrow balance information
     * @member principal Total balance (with accrued interest), after applying the most recent balance-changing action
     * @member interestIndex Global borrowIndex as of the most recent balance-changing action
     */
    struct BorrowSnapshot {
        uint256 principal;
        uint256 interestIndex;
    }

    // Mapping of account addresses to outstanding borrow balances
    mapping(address => BorrowSnapshot) internal accountBorrows;

    // Official record of token balances for each account
    mapping(address => uint256) internal accountTokens;

    // Approved token transfer amounts on behalf of others
    mapping(address => mapping(address => uint256)) internal transferAllowances;

    /**
     * @notice Initial exchange rate used when minting the first mTokens (used when totalSupply = 0)
     */
    uint256 internal initialExchangeRateMantissa;

    /**
     * @notice Maximum fraction of interest that can be set aside for reserves
     */
    uint256 internal constant RESERVE_FACTOR_MAX_MANTISSA = 1e18;

    /**
     * @notice Share of seized collateral that is added to reserves
     */
    uint256 internal constant PROTOCOL_SEIZE_SHARE_MANTISSA = 2.8e16; //2.8%

    // ----------- ERRORS ------------
    error mt_OnlyAdmin();
    error mt_RedeemEmpty();
    error mt_InvalidInput();
    error mt_OnlyAdminOrRole();
    error mt_TransferNotValid();
    error mt_MinAmountNotValid();
    error mt_BorrowRateTooHigh();
    error mt_AlreadyInitialized();
    error mt_ReserveFactorTooHigh();
    error mt_ExchangeRateNotValid();
    error mt_MarketMethodNotValid();
    error mt_LiquidateSeizeTooMuch();
    error mt_RedeemCashNotAvailable();
    error mt_BorrowCashNotAvailable();
    error mt_ReserveCashNotAvailable();
    error mt_RedeemTransferOutNotPossible();
    error mt_SameChainOperationsAreDisabled();
    error mt_CollateralBlockTimestampNotValid();

    // ----------- ACCESS EVENTS ------------
    /**
     * @notice Event emitted when rolesOperator is changed
     */
    event NewRolesOperator(address indexed oldRoles, address indexed newRoles);

    /**
     * @notice Event emitted when Operator is changed
     */
    event NewOperator(address indexed oldOperator, address indexed newOperator);

    // ----------- TOKENS EVENTS ------------
    /**
     * @notice EIP20 Transfer event
     */
    event Transfer(address indexed from, address indexed to, uint256 amount);

    /**
     * @notice EIP20 Approval event
     */
    event Approval(address indexed owner, address indexed spender, uint256 amount);

    // ----------- MARKETS EVENTS ------------
    /**
     * @notice Event emitted when interest is accrued
     */
    event AccrueInterest(uint256 cashPrior, uint256 interestAccumulated, uint256 borrowIndex, uint256 totalBorrows);

    /**
     * @notice Event emitted when tokens are minted
     */
    event Mint(address indexed minter, address indexed receiver, uint256 mintAmount, uint256 mintTokens);

    /**
     * @notice Event emitted when tokens are redeemed
     */
    event Redeem(address indexed redeemer, uint256 redeemAmount, uint256 redeemTokens);

    /**
     * @notice Event emitted when underlying is borrowed
     */
    event Borrow(address indexed borrower, uint256 borrowAmount, uint256 accountBorrows, uint256 totalBorrows);

    /**
     * @notice Event emitted when a borrow is repaid
     */
    event RepayBorrow(
        address indexed payer,
        address indexed borrower,
        uint256 repayAmount,
        uint256 accountBorrows,
        uint256 totalBorrows
    );

    /**
     * @notice Event emitted when a borrow is liquidated
     */
    event LiquidateBorrow(
        address indexed liquidator,
        address indexed borrower,
        uint256 repayAmount,
        address indexed mTokenCollateral,
        uint256 seizeTokens
    );

    /**
     * @notice Event emitted when interestRateModel is changed
     */
    event NewMarketInterestRateModel(address indexed oldInterestRateModel, address indexed newInterestRateModel);

    /**
     * @notice Event emitted when the reserve factor is changed
     */
    event NewReserveFactor(uint256 oldReserveFactorMantissa, uint256 newReserveFactorMantissa);

    /**
     * @notice Event emitted when the reserves are added
     */
    event ReservesAdded(address indexed benefactor, uint256 addAmount, uint256 newTotalReserves);

    /**
     * @notice Event emitted when the reserves are reduced
     */
    event ReservesReduced(address indexed admin, uint256 reduceAmount, uint256 newTotalReserves);

    /**
     * @notice Event emitted when the borrow max mantissa is updated
     */
    event NewBorrowRateMaxMantissa(uint256 oldVal, uint256 maxMantissa);

    /**
     * @notice Event emitted when same chain flow state is enabled or disabled
     */
    event SameChainFlowStateUpdated(address indexed sender, bool _oldState, bool _newState);

    /**
     * @notice Event emitted when same chain flow state is enabled or disabled
     */
    event ZkVerifierUpdated(address indexed oldVerifier, address indexed newVerifier);

    // ----------- VIRTUAL ------------
    /**
     * @inheritdoc ImToken
     */
    function accrueInterest() external virtual {
        _accrueInterest();
    }

    /**
     * @dev Function to simply retrieve block timestamp
     *  This exists mainly for inheriting test contracts to stub this result.
     */
    function _getBlockTimestamp() internal view virtual returns (uint256) {
        return block.timestamp;
    }

    /**
     * @notice Calculates the exchange rate from the underlying to the MToken
     * @dev This function does not accrue interest before calculating the exchange rate
     *      Can generate issues if inflated by an attacker when market is created
     *      Solution: use 0 collateral factor initially
     * @return calculated exchange rate scaled by 1e18
     */
    function _exchangeRateStored() internal view virtual returns (uint256) {
        uint256 _totalSupply = totalSupply;
        if (_totalSupply == 0) {
            /*
             * If there are no tokens minted:
             *  exchangeRate = initialExchangeRate
             */
            return initialExchangeRateMantissa;
        } else {
            /*
             * Otherwise:
             *  exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply
             */
            uint256 totalCash = _getCashPrior();
            uint256 cashPlusBorrowsMinusReserves = totalCash + totalBorrows - totalReserves;
            uint256 exchangeRate = (cashPlusBorrowsMinusReserves * expScale) / _totalSupply;

            return exchangeRate;
        }
    }

    /**
     * @notice Gets balance of this contract in terms of the underlying
     * @dev This excludes the value of the current message, if any
     * @return The quantity of underlying owned by this contract
     */
    function _getCashPrior() internal view virtual returns (uint256);

    /**
     * @dev Performs a transfer in, reverting upon failure. Returns the amount actually transferred to the protocol, in case of a fee.
     *  This may revert due to insufficient balance or insufficient allowance.
     */
    function _doTransferIn(address from, uint256 amount) internal virtual returns (uint256);

    /**
     * @dev Performs a transfer out, ideally returning an explanatory error code upon failure rather than reverting.
     *  If caller has not called checked protocol's balance, may revert due to insufficient cash held in the contract.
     *  If caller has checked protocol's balance, and verified it is >= amount, this should not revert in normal conditions.
     */
    function _doTransferOut(address payable to, uint256 amount) internal virtual;

    // ----------- NON-VIRTUAL ------------
    function _accrueInterest() internal {
        /* Remember the initial block timestamp */
        uint256 currentBlockTimestamp = _getBlockTimestamp();
        uint256 accrualBlockTimestampPrior = accrualBlockTimestamp;

        /* Short-circuit accumulating 0 interest */
        if (accrualBlockTimestampPrior == currentBlockTimestamp) return;

        /* Read the previous values out of storage */
        uint256 cashPrior = _getCashPrior();
        uint256 borrowsPrior = totalBorrows;
        uint256 reservesPrior = totalReserves;
        uint256 borrowIndexPrior = borrowIndex;

        /* Calculate the current borrow interest rate */
        uint256 borrowRateMantissa =
            IInterestRateModel(interestRateModel).getBorrowRate(cashPrior, borrowsPrior, reservesPrior);
        if (borrowRateMaxMantissa > 0) {
            require(borrowRateMantissa <= borrowRateMaxMantissa, mt_BorrowRateTooHigh());
        }

        /* Calculate the number of blocks elapsed since the last accrual */
        uint256 blockDelta = currentBlockTimestamp - accrualBlockTimestampPrior;

        /*
         * Calculate the interest accumulated into borrows and reserves and the new index:
         *  simpleInterestFactor = borrowRate * blockDelta
         *  interestAccumulated = simpleInterestFactor * totalBorrows
         *  totalBorrowsNew = interestAccumulated + totalBorrows
         *  totalReservesNew = interestAccumulated * reserveFactor + totalReserves
         *  borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex
         */

        Exp memory simpleInterestFactor = mul_(Exp({mantissa: borrowRateMantissa}), blockDelta);
        uint256 interestAccumulated = mul_ScalarTruncate(simpleInterestFactor, borrowsPrior);
        uint256 totalBorrowsNew = interestAccumulated + borrowsPrior;
        uint256 totalReservesNew =
            mul_ScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior);
        uint256 borrowIndexNew = mul_ScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior);

        /////////////////////////
        // EFFECTS & INTERACTIONS
        // (No safe failures beyond this point)

        /* We write the previously calculated values into storage */
        accrualBlockTimestamp = currentBlockTimestamp;
        borrowIndex = borrowIndexNew;
        totalBorrows = totalBorrowsNew;
        totalReserves = totalReservesNew;

        /* We emit an AccrueInterest event */
        emit AccrueInterest(cashPrior, interestAccumulated, borrowIndexNew, totalBorrowsNew);
    }
}


Vulnerability type: CWE-682