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: 17/73
Findings: 5
Award: $655.37
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xkazim
Also found by: Auditwolf, BRONZEDISC, Hama, MohammedRizwan, R-Nemes, dacian, kodyvim, markus_ether, nadin, niki, okolicodes
104.4113 USDC - $104.41
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Oracles/ChainlinkCompositeOracle.sol#L180 https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Oracles/ChainlinkOracle.sol#L97
wrong price could be returned in the event of a market crash.
From chainlink:
The data feed aggregator includes both minAnswer and maxAnswer values. These variables prevent the aggregator from updating the latestAnswer outside the agreed range of acceptable values, but they do not stop your application from reading the most recent answer.
Configure your application to detect when the reported answer is close to reaching minAnswer or maxAnswer and issue an alert so you can respond to a potential market event. Separately, configure your application to detect and respond to extreme price volatility or prices that are outside of your acceptable limits.
Chainlink oracles have a min and max price that they return. If the price goes below the minimum price the oracle will not return the correct price but only the min price. Same goes for the other extremity.
Both ChainlinkCompositeOracle.getPriceAndDecimals
and ChainlinkOracle.getChainlinkPrice
does not check if price within the correct range:
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Oracles/ChainlinkOracle.sol#L97
function getChainlinkPrice( AggregatorV3Interface feed ) internal view returns (uint256) { (, int256 answer, , uint256 updatedAt, ) = AggregatorV3Interface(feed) .latestRoundData(); require(answer > 0, "Chainlink price cannot be lower than 0"); require(updatedAt != 0, "Round is in incompleted state"); ...SNIP }
wrong price may be returned in the event of a market crash.
Manual Review
Check the latest answer against reasonable limits and/or revert in case you get a bad price
require(answer >= minAnswer && answer <= maxAnswer, "invalid price");
Other
#0 - 0xSorryNotSorry
2023-08-01T11:17:32Z
The implementation does not set a min/max value by design. Also Chainlink does not return min/max price as per the AggregatorV3 docs HERE contrary to the reported.
Further proof required as per the context.
#1 - c4-pre-sort
2023-08-01T11:17:38Z
0xSorryNotSorry marked the issue as low quality report
#2 - c4-judge
2023-08-14T21:27:39Z
alcueca marked the issue as duplicate of #340
#3 - c4-judge
2023-08-14T21:28:06Z
alcueca marked the issue as satisfactory
43.3709 USDC - $43.37
Calls to target
with non-zero value would always revert.
Some external functions within TemporalGovernor
uses _executeProposal
to execute proposals but these external functions are not marked payable
and contract does not implement recieve payable
fallback.
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L400
For instance:
function executeProposal(bytes memory VAA) public whenNotPaused { _executeProposal(VAA, false); }
In _executeProposal
:
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L400
function _executeProposal(bytes memory VAA, bool overrideDelay) private { ...SNIP _sanityCheckPayload(targets, values, calldatas); for (uint256 i = 0; i < targets.length; i++) { address target = targets[i]; uint256 value = values[i]; bytes memory data = calldatas[i]; // Go make our call, and if it is not successful revert with the error bubbling up (bool success, bytes memory returnData) = target.call{value: value}(//@audit if value is non zero this call would revert. data ); /// revert on failure with error message if any require(success, string(returnData)); emit ExecutedTransaction(target, value, data); } }
You can see that _executeProposal
makes low level call to the target with value. if value
is non-zero the call would revert since the contract have no ETH, since the external function is not marked payable and the contract does not implement receive payable
.
Another Instance:
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L266
Manual Review
Add payable
to the external functions calling _executeProposal
or implement receive payable external
so contract can recieve ETH.
Payable
#0 - c4-pre-sort
2023-08-03T13:21:52Z
0xSorryNotSorry marked the issue as duplicate of #268
#1 - c4-judge
2023-08-12T20:38:11Z
alcueca marked the issue as satisfactory
#2 - c4-judge
2023-08-12T20:38:14Z
alcueca marked the issue as partial-50
#3 - c4-judge
2023-08-21T21:35:22Z
alcueca changed the severity to 2 (Med Risk)
#4 - emmydev9
2023-08-23T06:35:42Z
I respectfully bring up this point in regards to partial credit.
The intention was to illustrate that the proposal execution functionality might not function as intended in all anticipated scenarios. Due to the absence of a method in the contract to receive ETH following the _executeProposal
call.
#5 - alcueca
2023-08-23T20:47:32Z
Partial credit is awarded because the primary included an actual PoC, as a foundry test, and this submission didn't.
🌟 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/main/src/core/IRModels/WhitePaperInterestRateModel.sol#L51 https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/IRModels/JumpRateModel.sol#L68
The WhitePaperInterestRateModel.utilizationRate
and JumpRateModel.utilizationRate
function can return value above 1 and not between 0, 1e18.
In WhitePaperInterestRateModel.utilizationRate
and JumpRateModel.utilizationRate
function, cash and borrows and reserves values gets used to calculate utilization rate between between [0, 1e18]. reserves is currently unused but it will be used in the future.
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/IRModels/WhitePaperInterestRateModel.sol#L51
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/IRModels/JumpRateModel.sol#L68
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)); }
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 cash + borrows - 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 - ElliotFriedman
2023-08-03T19:07:00Z
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
#1 - c4-sponsor
2023-08-03T19:07:04Z
ElliotFriedman marked the issue as sponsor disputed
#2 - c4-judge
2023-08-13T13:34:37Z
alcueca marked the issue as duplicate of #135
#3 - c4-judge
2023-08-13T13:42:56Z
alcueca marked the issue as selected for report
#4 - c4-judge
2023-08-13T13:43:30Z
alcueca marked the issue as satisfactory
#5 - alcueca
2023-08-19T21:10:24Z
As mentioned on #135, 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.
#6 - c4-judge
2023-08-19T21:24:11Z
alcueca marked issue #40 as primary and marked this issue as a 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
borrowRatePerTimestamp
is not fresh and does not include interests accruedhttps://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MToken.sol#L236
borrowRatePerTimestamp
returns the borrowing rate, but the interests accrued are not included.
function borrowRatePerTimestamp() override external view returns (uint) { return interestRateModel.getBorrowRate(getCashPrior(), totalBorrows, totalReserves); }
Impact Any smart contract relying on the value of borrowRatePerTimestamp() to perform some logic - such as decide whether to borrow from VToken based on the returned value - will be impacted.
For instance, imagine a protocol XYZ having a function in their contract that calls borrowRatePerTimestamp() and have a conditional call to mToken.borrow() if the borrowing rate is less than N. The call to borrowRatePerTimestamp() may return a value M < N, but the actual borrowing rate processed in borrow() can be P > N due to interest accrual.
Include some interest accrual logic in borrowRatePerTimestamp() - note that it cannot be a simple call to accrueInterest as borrowRatePerBlock() is a view function.
chainlink states that:
During periods of low volatility, the heartbeat triggers updates to the latest answer. Some heartbeats are configured to last several hours, so your application should check the timestamp and verify that the latest answer is recent enough for your application.
Both ChainlinkCompositeOracle.getPriceAndDecimals
and ChainlinkOracle.getChainlinkPrice
should check that the reported answer is updated within the heartbeat or within time limits deemed acceptable:
The following check could be added:
require(block.timestamp - updatedAt < PRICE_ORACLE_STALE_THRESHOLD, "STALE_THRESHOLD: Exceeded");
#0 - c4-judge
2023-08-12T17:47:00Z
alcueca marked the issue as grade-a
#1 - c4-sponsor
2023-08-15T18:30:35Z
ElliotFriedman marked the issue as sponsor confirmed
🌟 Selected for report: Sathish9098
Also found by: 0xSmartContract, K42, Udsen, berlin-101, catellatech, cryptonue, hals, jaraxxus, kodyvim, kutugu, solsaver
Moonwell is an open lending and borrowing DeFi protocol built on Base, Moonbeam, and Moonriver. It allows users to leverage their assets through the lending and borrowing process, providing them with an intuitive and seamless experience. The protocol operates on Moonbeam & Moonriver and utilizes mToken, a compound fork, in comparison to cToken. Users can earn rewards by participating in the protocol passively, either by lending mTokens or borrowing them.
2.1 Token Rescue Limitation: To ensure protocol solvency, Moonwell should restrict the withdrawal of tokens that are whitelisted as collateral. Allowing this could lead to potential insolvency risks.
2.2 Accurate Reward Calculation: External View functions should accurately return rewards to facilitate third-party integration. This involves accruing interest before returning data to enhance transparency and usability.
2.3 Chainlink Best Practices: Moonwell must strictly adhere to all Chainlink best practices to avoid using stale prices. Given the highly volatile nature of the crypto market, relying on outdated prices can be dangerous.
3.1 Admin and Governance Control: The presence of an admin controlled by governance raises concerns. This entity can access the withdrawal and rescue of stranded tokens, potentially affecting the protocol's decentralization.
3.2 Asset Price Setting: The owner's ability to set underlying asset prices as a fallback for Chainlink unavailability poses a centralization risk. Transparent price feeds should be prioritized.
3.3 Protocol Pausing: While the ability to pause the protocol serves as a safety measure, it can impact users' ability to borrow, lend, and initiate liquidations. Striking a balance between safety and protocol availability is essential for maintaining a permissionless nature.
The audit process for Moonwell took a total of 3 days, with the following breakdown:
1st Day: Understanding protocol documentation. 2nd Day: Focusing on linking documentation to logic and typing reports for identified vulnerabilities, including a review of various Chainlink contracts. 3rd Day: Summarizing the audit by completing a QA report and final analysis.
60 hours
#0 - c4-judge
2023-08-11T21:01:00Z
alcueca marked the issue as grade-b