Moonwell - Nyx's results

An open lending and borrowing DeFi protocol.

General Information

Platform: Code4rena

Start Date: 24/07/2023

Pot Size: $100,000 USDC

Total HM: 18

Participants: 73

Period: 7 days

Judge: alcueca

Total Solo HM: 8

Id: 267

League: ETH

Moonwell

Findings Distribution

Researcher Performance

Rank: 23/73

Findings: 2

Award: $437.82

QA:
grade-a

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: 0xWaitress

Also found by: 0xWaitress, Nyx, ast3ros, catellatech, kodyvim, nadin

Labels

bug
2 (Med Risk)
satisfactory
sponsor disputed
duplicate-40

Awards

392.9367 USDC - $392.94

External Links

Lines of code

https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/IRModels/JumpRateModel.sol#L61-L75

Vulnerability details

Impact

The utilizationRate() function can return a value above 1 and not between [0, BASE].

Proof of Concept

function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) {
        // Utilization rate is 0 when there are no borrows
        if (borrows == 0) {
            return 0;
        }

        return borrows.mul(1e18).div(cash.add(borrows).sub(reserves)); 
    }

In the utilizationRate() function, cash and borrows and reserves values gets used to calculate utilization rate between between [0, 1e18].

If Borrow value is 0, then function will return 0. but in this function the scenario where the value of reserves exceeds cash is not handled. the system does not guarantee that reserves never exceeds cash. the reserves grow automatically over time, so it might be difficult to avoid this entirely.

If reserves > cash (and borrows + cash - reserves > 0), the formula for utilizationRate above gives a utilization rate above 1.

Tools Used

Manual Review

Make the utilization rate computation return 1 if reserves > cash.

Assessed type

Math

#0 - c4-pre-sort

2023-08-03T14:01:28Z

0xSorryNotSorry marked the issue as primary issue

#1 - ElliotFriedman

2023-08-03T21:41:17Z

reserves can be pulled back to admin address by admin, so this is a non issue

#2 - c4-sponsor

2023-08-03T21:41:18Z

ElliotFriedman marked the issue as sponsor disputed

#3 - alcueca

2023-08-13T13:18:14Z

@ElliotFriedman, how more exactly are the reserves pulled back to the admin address by the admin? There are a number of scenarios in which that might be difficult or impossible (i.e. high gas, governance attack, dealing with another crisis, etc...)

#4 - alcueca

2023-08-13T13:42:28Z

I found another duplicate (#111 ) with a good explanation of the issue by the sponsor:

reserves can be reduced by calling _reduceReserves on the mToken. This is a non issue as the cost of this attack would be greater than the TVL of the protocol per MToken. Say an MToken has a TVL of 10m in supplied assets, to perform this attack, an attacker would need to donate 10m to DoS the protocol, which could be unfrozen in 5 days by calling _reduceReserves

@ElliotFriedman, this is a textbook definition of a griefing attack. An attacker might be glad to spend money so that you are forced to deny service to your users for 5 days. They might find cheaper ways to do it, as well, since the reserves grow with time and have to be pulled back by governance. This vulnerability also looks like it could be combined with others to put you in a very bad position.

#5 - c4-judge

2023-08-13T13:42:53Z

alcueca marked issue #111 as primary and marked this issue as a duplicate of 111

#6 - c4-judge

2023-08-19T21:09:53Z

alcueca marked the issue as satisfactory

Findings Information

🌟 Selected for report: 0xWaitress

Also found by: 0xWaitress, Nyx, ast3ros, catellatech, kodyvim, nadin

Labels

bug
2 (Med Risk)
satisfactory
duplicate-40

Awards

392.9367 USDC - $392.94

External Links

Lines of code

https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MToken.sol#L380-L463 https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MToken.sol#L1385-L1399

Vulnerability details

Impact

Borrow rates are calculated dynamically and accrueInterest() reverts if the calculated rate is greater than a hard-coded maximum. As accrueInterest() is called by most functions, the entire contract may halt and be unrecoverable.

Proof of Concept

The hard-codes the maximum borrow rate and accrueInterest() reverts if the dynamically calculated rate is greater than the hard-coded value.

/* Calculate the current borrow interest rate */
        uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior);
        require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high"); 

The actual calculation is dynamic and takes no notice of the hard-coded cap, so it is possible that this state will manifest, causing a major DoS due to most functions calling accrueInterest() and accrueInterest() reverting.

The admin role may update the interest rate calculation contract via the setInterestRateModel() function.

function _setInterestRateModel(InterestRateModel newInterestRateModel) override public returns (uint) {
        uint error = accrueInterest(); //@audit here !
        if (error != uint(Error.NO_ERROR)) {
            // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted change of interest rate model failed
            return fail(Error(error), FailureInfo.SET_INTEREST_RATE_MODEL_ACCRUE_INTEREST_FAILED);
        }
        // _setInterestRateModelFresh emits interest-rate-model-update-specific logs on errors, so we don't need to.
        return _setInterestRateModelFresh(newInterestRateModel);
    }

However, this method also calls accrueInterest before completing the upgrade, so it would fail as well.

Tools Used

Manual Review

Prevent utilization rate calculations from returning anything above borrowRateMaxMantissa or Remove the accrueInterest from setInterestRateModel() function.

Assessed type

DoS

#0 - 0xSorryNotSorry

2023-08-02T15:53:01Z

Duping under the primary issue since the referred root cause is the same and having partials.

#1 - c4-pre-sort

2023-08-03T14:05:40Z

0xSorryNotSorry marked the issue as duplicate of #4

#2 - c4-judge

2023-08-19T20:44:39Z

alcueca marked the issue as satisfactory

#3 - c4-judge

2023-08-21T21:47:20Z

alcueca marked the issue as duplicate of #40

Lines of code

https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Comptroller.sol#L563-L619 https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MToken.sol#L283-L313

Vulnerability details

Impact

Interest are not accrued for each asset inside getHypotheticalAccountLiquidityInternal. This can cause wrong calculations of borrowed amount and can allow user to redeem and leave his account in bad state.

Proof of Concept

The getHypotheticalAccountLiquidityInternal function is called to calculate if this user has shortfall or no when he will do specific action like redeem or borrow.

function getHypotheticalAccountLiquidityInternal(
        address account,
        MToken mTokenModify,
        uint redeemTokens,
        uint borrowAmount) internal view returns (Error, uint, uint) {

        AccountLiquidityLocalVars memory vars; // Holds all our calculation results
        uint oErr;

        // For each asset the account is in
        MToken[] memory assets = accountAssets[account];
        for (uint i = 0; i < assets.length; i++) {
            MToken asset = assets[i];

This function is going to loop through all assets of user in order to calculate how much collateral he has in that market and how much debt he has.

function borrowBalanceStoredInternal(address account) internal view returns (MathError, uint) {
        /* Note: we do not assert that the market is up to date */
        MathError mathErr;
        uint principalTimesIndex;
        uint result;

        /* 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 (MathError.NO_ERROR, 0);
        }

        /* Calculate new borrow balance using the interest index:
         *  recentBorrowBalance = borrower.borrowBalance * market.borrowIndex / borrower.borrowIndex
         */
        (mathErr, principalTimesIndex) = mulUInt(borrowSnapshot.principal, borrowIndex); //@audit here 
        if (mathErr != MathError.NO_ERROR) {
            return (mathErr, 0);
        }

        (mathErr, result) = divUInt(principalTimesIndex, borrowSnapshot.interestIndex);
        if (mathErr != MathError.NO_ERROR) {
            return (mathErr, 0);
        }

        return (MathError.NO_ERROR, result);
    }

To calculate balance they use borrow index and this index is increasing with time. In order to receive correct borrow amount, accrueInterests function should be called for market, which will update borrowIndex. But protocol doesn't accrue interests for each market of borrower and as result, getHypotheticalAccountLiquidityInternal can allow user to redeem, when he should not be allowed.

Tools Used

Manual Review

The protocol needs to accrue interest for each market of the user.

Assessed type

Other

#0 - 0xSorryNotSorry

2023-08-02T15:35:49Z

This is the intended mechanism after it's name Hypothetical

Invalid assumption.

#1 - c4-pre-sort

2023-08-02T15:35:56Z

0xSorryNotSorry marked the issue as low quality report

#2 - alcueca

2023-08-14T21:23:07Z

getHypotheticalAccountLiquidityInternal gets called inside redeemAllowed which gets called inside exitMarket. If it misrepresents the user position, in particular user debt, it might lead to losses for the protocol.

@ElliotFriedman, do we have anything here?

#3 - c4-judge

2023-08-14T22:04:25Z

alcueca marked the issue as primary issue

#4 - ElliotFriedman

2023-08-21T20:22:21Z

accrue interest will get called pretty regularly in most markets, so technically this is valid, but in practice we won't be worried about it and won't fix.

#5 - alcueca

2023-08-21T21:16:23Z

Sponsor acknowledged the issue.

#6 - c4-judge

2023-08-21T21:16:29Z

alcueca marked the issue as satisfactory

#7 - c4-judge

2023-08-21T21:39:50Z

alcueca marked the issue as selected for report

#8 - lyoungblood

2023-08-21T22:53:05Z

getHypotheticalAccountLiquidityInternal gets called inside redeemAllowed which gets called inside exitMarket. If it misrepresents the user position, in particular user debt, it might lead to losses for the protocol.

This can't result in losses to the protocol because exitMarket doesn't result in any transfer of value, it only makes that market's collateral token not count toward borrowing power. So, in the hypothetical situation where a market is not very active and hasn't had accrueInterest called in a while, the borrower might be able to call exitMarket and go negative (where they have borrowed more than collateral value), but the only outcome would be to make that borrower liquidatable, which would result in borrower loss of funds due to liquidation fees.

This might be a low severity bug, but we'll mark as won't fix and dispute severity, as it can't lead to losses for the protocol.

#9 - alcueca

2023-08-22T06:35:59Z

@lyoungblood, your dispute doesn't go to the heart of the finding. Dismissing findings without fully understanding them is risky for you, and I would discourage that.

However, the trace goes like this:

MERC20.borrow MToken.borrowInternal accrueInterest() MToken.borrowFresh Comptroller.borrowAllowed getHypotheticalAccountLiquidityInternal

The pattern holds for the rest of the codebase. accrueInterest doesn't get called in getHypotheticalAccountLiquidityInternal, but it always gets called beforehand in the same transaction. Therefore the bug doesn't exist as such and the finding is invalid.

#10 - c4-judge

2023-08-22T06:36:49Z

alcueca marked the issue as unsatisfactory: Invalid

#11 - Nyksxx

2023-08-23T09:57:28Z

MERC20.borrow MToken.borrowInternal accrueInterest() MToken.borrowFresh Comptroller.borrowAllowed getHypotheticalAccountLiquidityInternal

The pattern holds for the rest of the codebase. accrueInterest doesn't get called in getHypotheticalAccountLiquidityInternal, but it always gets called beforehand in the same transaction. Therefore the bug doesn't exist as such and the finding is invalid.

Hi , this pattern is correct but misses the point I was trying to show in my report. accrueInterest() is only called for one asset, but it needs to be called for all assets that the user has. Example issue from Venus Contest: https://github.com/code-423n4/2023-05-venus-findings/issues/486 and please look at Recommended Mitigation Steps section.

#12 - alcueca

2023-08-24T05:26:42Z

The warden is right that the issue is not only on the asset that is the target of the operation, but on the other assets held that might be also related to the operation, maybe as collateral.

However, no proof of impact is given. As such, it is only proven that the function is incorrect as to spec in that the position of an account will be not accurate in certain cases. It is not shown how inaccurate the position can be, or if this can be abused to cause losses for the protocol or other users.

The severity of this finding is therefore QA.

#13 - c4-judge

2023-08-24T05:26:53Z

alcueca changed the severity to QA (Quality Assurance)

#14 - c4-judge

2023-08-24T05:26:58Z

alcueca marked the issue as grade-a

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter