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
Rank: 23/73
Findings: 2
Award: $437.82
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xWaitress
Also found by: 0xWaitress, Nyx, ast3ros, catellatech, kodyvim, nadin
392.9367 USDC - $392.94
The utilizationRate() function can return a value above 1 and not between [0, BASE].
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.
Manual Review
Make the utilization rate computation return 1 if reserves > cash.
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
🌟 Selected for report: 0xWaitress
Also found by: 0xWaitress, Nyx, ast3ros, catellatech, kodyvim, nadin
392.9367 USDC - $392.94
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
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.
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.
Manual Review
Prevent utilization rate calculations from returning anything above borrowRateMaxMantissa or Remove the accrueInterest from setInterestRateModel() function.
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
🌟 Selected for report: immeas
Also found by: 0x70C9, 0xAnah, 0xArcturus, 0xComfyCat, 0xWaitress, 0xackermann, 0xkazim, 2997ms, 33audits, Arz, Aymen0909, ChrisTina, JP_Courses, John_Femi, Jorgect, Kaysoft, LosPollosHermanos, MohammedRizwan, Nyx, Rolezn, Sathish9098, Stormreckson, T1MOH, Tendency, Topmark, Udsen, Vagner, albertwh1te, ast3ros, banpaleo5, berlin-101, catellatech, cats, codetilda, cryptonue, eeshenggoh, fatherOfBlocks, hals, jamshed, jaraxxus, josephdara, kankodu, kodyvim, kutugu, lanrebayode77, mert_eren, nadin, naman1778, niki, petrichor, ravikiranweb3, said, solsaver, souilos, twcctop, wahedtalash77
44.8793 USDC - $44.88
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
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.
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.
Manual Review
The protocol needs to accrue interest for each market of the user.
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 insideredeemAllowed
which gets called insideexitMarket
. 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 ingetHypotheticalAccountLiquidityInternal
, 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