Moonwell - 0xWaitress'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: 5/73

Findings: 3

Award: $5,746.15

QA:
grade-a

🌟 Selected for report: 2

πŸš€ Solo Findings: 1

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

510.8177 USDC - $510.82

External Links

Lines of code

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

Vulnerability details

Impact

utilization can be above 100% when reserve is more than cash

utilization is calculated as borrow / (cash + borrow - reserve). and the utilization would determine the interest rate in this classic jump/klink model.

Consider:

  1. both depositor and borrowers come in. Reserves accumulates as interest revenue paid by borrowers accrues.
  2. depositor withdraws. The protocol has only borrowers and the reserve.

When reserves becomes more than cash, the formula for utilizationRate would give a utilization rate above 1. This is different from the code comment which said to @return The utilization rate as a mantissa between [0, 1e18]

     * @return The utilization rate as a mantissa between [0, 1e18]
    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));
    }

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

Proof of Concept

Tools Used

Consider put a cap of 1e18 to the final output.

Assessed type

Math

#0 - c4-pre-sort

2023-08-03T14:02:25Z

0xSorryNotSorry marked the issue as duplicate of #135

#1 - ElliotFriedman

2023-08-03T21:42:35Z

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

#2 - c4-sponsor

2023-08-03T21:42:38Z

ElliotFriedman marked the issue as sponsor disputed

#3 - c4-judge

2023-08-13T13:43:58Z

alcueca marked the issue as satisfactory

#4 - alcueca

2023-08-13T13:45:27Z

@code423n4, please merge this issue into #40 by the same warden.

#5 - c4-judge

2023-08-13T13:45:39Z

alcueca marked the issue as nullified

#6 - c4-judge

2023-08-21T21:27:58Z

alcueca marked the issue as not nullified

#7 - c4-judge

2023-08-21T21:28:07Z

alcueca marked the issue as satisfactory

#8 - c4-judge

2023-08-21T21:28:13Z

alcueca marked the issue as nullified

#9 - c4-judge

2023-08-21T21:28:30Z

alcueca marked the issue as not nullified

#10 - c4-judge

2023-08-21T21:28:42Z

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)
primary issue
satisfactory
selected for report
sponsor disputed
edited-by-warden
M-16

Awards

510.8177 USDC - $510.82

External Links

Lines of code

https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MToken.sol#L403

Vulnerability details

Impact

accrueInterest is expected to revert when the rate is higher than the maximum allowed rate, which is possible since the utilization can be more than 1

accrueInterest is an essential function to keep updated of the global mToken interest payment from the borrower to the depositor. The function is called anytime there is a deposit/liquidate/borrow/repay/withdraw.

The function is set to revert when the borrowRateMantissa is bigger than the borrowRateMaxMantissa. require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high");.

With proper configuration, this check should be fine since the real borrowRate should be calibrated to be within the maxRate. However, since the utilization could actually be bigger than 1 (when reserve is bigger than cash [issue-37] , the actual rate could be bigger than the expected unreachable max rate:

Example: base: 1% multiplierPerTimestamp: 5% APR jumpMultiplier: 50% APR klink: 50% We could reasonably assume the max APR is 1% + 5% * 0.5 + 50% * (1-0.5) = 28.5%

However when reserve is more than cash it would cause the utilization be bigger than 1, let'say utilization is 101%: such that the actual rate would be:

1% + 5% * 0.5 + 50% * (1.01 - 0.5) = 29%

This would cause the accurueInterest to revert.

Impact: all deposit/withdraw/borrow/repay function would fail and severe brick the operation of the protocol and lock user funds. Interest accrual cannot work which impact both borrower for timely exit as well as deposit to collect their interest.

    function accrueInterest() virtual override public returns (uint) {
        /* Remember the initial block timestamp */
        uint currentBlockTimestamp = getBlockTimestamp();
        uint accrualBlockTimestampPrior = accrualBlockTimestamp;

        /* Short-circuit accumulating 0 interest */
        if (accrualBlockTimestampPrior == currentBlockTimestamp) {
            return uint(Error.NO_ERROR);
        }

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

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

https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MToken.sol#L403

Proof of Concept

Tools Used

Consider to cap the real-time borrowRate as the borrowRateMaxMantissa instead of reverting.

Assessed type

DoS

#0 - c4-pre-sort

2023-08-03T14:02:23Z

0xSorryNotSorry marked the issue as duplicate of #135

#1 - c4-sponsor

2023-08-03T21:42:17Z

ElliotFriedman marked the issue as sponsor disputed

#2 - ElliotFriedman

2023-08-03T21:42:28Z

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

#3 - alcueca

2023-08-13T13:20:46Z

@code423n4, if valid, this should be merged with #37 by the same warden, and then the combination would be the one selected for report of this group of duplicates.

#4 - c4-judge

2023-08-19T21:23:27Z

alcueca marked the issue as satisfactory

#5 - c4-judge

2023-08-19T21:24:13Z

alcueca marked the issue as selected for report

Findings Information

🌟 Selected for report: 0xWaitress

Labels

bug
2 (Med Risk)
disagree with severity
primary issue
satisfactory
selected for report
sponsor confirmed
M-17

Awards

5190.4455 USDC - $5,190.45

External Links

Lines of code

https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L65-L68

Vulnerability details

Impact

a single emissionCap is not suitable for different tokens reward if they have different underlying decimals

At MultiRewardDistributor, different rewards token can be set up for each mToken, which is the assetToken or debtToken of the lending protocol. When a reward token is set up, it's emission speed cannot be bigger than the emissionCap (100 * 1e18 as suggested in the code comment). However since multiple reward tokens may be set up, and they may have different decimal places, for example USDT has a decimal of 6 and GUSD even has a decimal of 2 they are virtually not bounded by such emissionCap.

On the contract, tokens with more than 18 decimal would not be practically emitted. This one-size-fit-all emissionCap might create obstacles for the protocol to effectively manage each rewardTokens.

    /// @notice The emission cap dictates an upper limit for reward speed emission speed configs
    /// @dev By default, is set to 100 1e18 token emissions / sec to avoid unbounded
    ///  computation/multiplication overflows
    uint256 public emissionCap;
    function _updateBorrowSpeed(
        MToken _mToken,
        address _emissionToken,
        uint256 _newBorrowSpeed
    ) external onlyEmissionConfigOwnerOrAdmin(_mToken, _emissionToken) {
        MarketEmissionConfig
            storage emissionConfig = fetchConfigByEmissionToken(
                _mToken,
                _emissionToken
            );

        uint256 currentBorrowSpeed = emissionConfig
            .config
            .borrowEmissionsPerSec;

        require(
            _newBorrowSpeed != currentBorrowSpeed,
            "Can't set new borrow emissions to be equal to current!"
        );
        require(
            _newBorrowSpeed < emissionCap,
            "Cannot set a borrow reward speed higher than the emission cap!"
        );

https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L703-L725

Proof of Concept

Tools Used

Consider creating emissionCap for each rewardTokens so they can be adapted based on their own decimals

Assessed type

Context

#0 - c4-pre-sort

2023-08-03T14:11:06Z

0xSorryNotSorry marked the issue as primary issue

#1 - c4-sponsor

2023-08-03T19:23:56Z

ElliotFriedman marked the issue as disagree with severity

#2 - ElliotFriedman

2023-08-03T19:24:55Z

this is a known issue, the amount of tokens created is bounded 100e18 per second. then, reward speeds will be integration tested to ensure they are properly configured.

#3 - c4-sponsor

2023-08-03T19:25:07Z

ElliotFriedman marked the issue as sponsor confirmed

#4 - alcueca

2023-08-12T21:06:47Z

@ElliotFriedman, I'm not sure if you understood the finding. How would you set up the emissions cap if emissions are in USDT (6 decimals) and WETH (18 decimals).

#5 - c4-judge

2023-08-12T21:07:04Z

alcueca marked the issue as satisfactory

#6 - ElliotFriedman

2023-08-21T20:35:48Z

Valid issue, just a question of is it medium severity. I'll leave that to judges to decide.

#7 - alcueca

2023-08-21T20:51:23Z

Function incorrect as to spec would be QA - This means that if the way the code works is wrong but has no real impact in the business it is QA. Function of the protocol or its availability possibly impacted would be Medium - In certain cases, the emissionsCap will either not work or stop emissions, the protocol is impacted in a non-lethal manner, and the finding is medium.

#8 - c4-judge

2023-08-21T21:41:40Z

alcueca marked the issue as selected for report

#9 - lyoungblood

2023-08-21T23:17:44Z

Good finding - disagree with the severity as the cap is just a safety feature that didn't exist at all in the original Compound.

[L-1] anyone can claimRewards for others which is undesirable for some users causing tax issue.

claimReward can be called by anyone to trigger reward collection for any particular user. This is fine with security but may cause taxation issue or make some user/contract to collect tokens at the time when the owner does not deem appropriate to call.

    function claimReward(address[] memory holders, MToken[] memory mTokens, bool borrowers, bool suppliers) public {

Recommendation: Consider only allow the msg.sender to claimReward, or add a reward allowance delegation function.

[N-1] remove liquidateBorrowVerify in ComtrollerInterface if this is no use

    function liquidateBorrowVerify(
        address mTokenBorrowed,
        address mTokenCollateral,
        address liquidator,
        address borrower,
        uint repayAmount,
        uint seizeTokens) virtual external;

#0 - c4-judge

2023-08-12T18:19:31Z

alcueca marked the issue as grade-a

#1 - ElliotFriedman

2023-08-15T17:40:06Z

this is expected behavior, when claiming rewards on others behalf, rewards get sent to the person who accrued them.

#2 - c4-sponsor

2023-08-15T17:40:11Z

ElliotFriedman marked the issue as sponsor disputed

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