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: 4/73
Findings: 4
Award: $7,101.79
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: Udsen
5190.4455 USDC - $5,190.45
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MErc20.sol#L139-L142 https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MToken.sol#L1002 https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L1235-L1239
If a borrower is undercollateralized
then he can be liquidated by a liquidator by calling the MErc20.liquidateBorrow
function. liquidateBorrow
function calls the MToken.liquidateBorrowFresh
in its execution process. Inside the liquidateBorrowFresh
function the MToken.repayBorrowFresh
is called which verifies whether repayment of borrowed amount is allowed by calling the Comptroller.repayBorrowAllowed
function. The repayBorrowAllowed
function updates the borrower eligible rewards
by calling the MultiRewardDistributor.updateMarketBorrowIndexAndDisburseBorrowerRewards
.
The MultiRewardDistributor.updateMarketBorrowIndexAndDisburseBorrowerRewards
function calls the disburseBorrowerRewardsInternal
to ditribute the multi rewards to the borrower
.
The disburseBorrowerRewardsInternal
calls the sendReward
function if the _sendTokens
flag is set to true
.
sendReward
function is called for each emissionConfig.config.emissionToken
token of the MarketEmissionConfig[]
array of the given _mToken
market.
sendReward
function transafers the rewards to the borrower
using the safeTransfer
function as shown below:
token.safeTransfer(_user, _amount);
Problem here is that emissionToken
can be ERC777
token (which is backward compatible with ERC20) thus allowing a malicious borrower
(the recipient contract
of rewards in this case) to implement tokensReceived
hook in its contract and revert the transaction inside the hook. This will revert the entire liquidation
transaction. Hence the undercollateralized borrower
can avoid the liquidation thus putting both depositors and protocol in danger.
function liquidateBorrow(address borrower, uint repayAmount, MTokenInterface mTokenCollateral) override external returns (uint) { (uint err,) = liquidateBorrowInternal(borrower, repayAmount, mTokenCollateral); return err; }
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MErc20.sol#L139-L142
(uint repayBorrowError, uint actualRepayAmount) = repayBorrowFresh(liquidator, borrower, repayAmount);
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MToken.sol#L1002
if (_amount > 0 && _amount <= currentTokenHoldings) { // Ensure we use SafeERC20 to revert even if the reward token isn't ERC20 compliant token.safeTransfer(_user, _amount); return 0; } else {
Manual Review and VSCode
Hence it is recommended to disallow any ERC777 tokens being configured as emissionToken
in any MToken
market and only allow ERC20 tokens as emissionTokens
.
Other
#0 - 0xSorryNotSorry
2023-08-03T07:57:40Z
This logic is a bit off as the malicious user will not be having the ERC777 MToken rewards anyway, so the value extracted from this scenario doesn't match.
only allow ERC20 tokens as emissionTokens
Above mitigation recommendation is also matching the start of the submission. Erc777's are Erc20 tokens as well.
#1 - c4-pre-sort
2023-08-03T07:57:59Z
0xSorryNotSorry marked the issue as low quality report
#2 - alcueca
2023-08-15T14:18:01Z
The finding shows that rewards are distributed as part of a liquidation.
If an ERC777 token is set as an emissionsToken, which has transfer hooks, a malicious borrower can revert on receiving rewards, and avoid liquidation. Avoiding liquidation in certain situations accrues bad debt for the protocol, or can be compounded with other vulnerabilities. This would be a valid Medium.
@ElliotFriedman, do you have in your documentation anything mentioning that ERC777 tokens should not be used as emission tokens?
#3 - c4-judge
2023-08-15T14:59:29Z
alcueca changed the severity to 2 (Med Risk)
#4 - ElliotFriedman
2023-08-15T17:31:23Z
this is a valid finding
#5 - c4-sponsor
2023-08-15T17:31:28Z
ElliotFriedman marked the issue as sponsor confirmed
#6 - c4-judge
2023-08-19T20:48:43Z
alcueca marked the issue as satisfactory
1796.6927 USDC - $1,796.69
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Comptroller.sol#L394-L424
The Mtoken
markets configured for the respective collateral asset types can get deprecated due to various reasons associated with those assets. There should be functionality in the Comptroller.liquidateBorrowAllowed
function to liquidate all the borrows in the accounts for deprecated markets.
This functionality is present in the Comptroller.liquidateBorrowAllowed
of the compound
protocol, but is missing in the Comptroller.liquidateBorrowAllowed
of the moonwell protocol.
Since this isDeprecated
functionality is missing the borrowed assets of the deprecated MToken
will not able to be repaid after the deprecation.
Following code snippet shows how the isDeprecated
functionality is implemented in Compound.Comptroller.sol
.
function isDeprecated(CToken cToken) public view returns (bool) { return markets[address(cToken)].collateralFactorMantissa == 0 && borrowGuardianPaused[address(cToken)] == true && cToken.reserveFactorMantissa() == 1e18 ; }
function liquidateBorrowAllowed( address mTokenBorrowed, address mTokenCollateral, address liquidator, address borrower, uint repayAmount) override external view returns (uint) { // Shh - currently unused liquidator; if (!markets[mTokenBorrowed].isListed || !markets[mTokenCollateral].isListed) { return uint(Error.MARKET_NOT_LISTED); } /* The borrower must have shortfall in order to be liquidatable */ (Error err, , uint shortfall) = getAccountLiquidityInternal(borrower); if (err != Error.NO_ERROR) { return uint(err); } if (shortfall == 0) { return uint(Error.INSUFFICIENT_SHORTFALL); } /* The liquidator may not repay more than what is allowed by the closeFactor */ uint borrowBalance = MToken(mTokenBorrowed).borrowBalanceStored(borrower); uint maxClose = mul_ScalarTruncate(Exp({mantissa: closeFactorMantissa}), borrowBalance); if (repayAmount > maxClose) { return uint(Error.TOO_MUCH_REPAY); } return uint(Error.NO_ERROR); }
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Comptroller.sol#L394-L424
Manual Review and VSCode
Hence it is recommended to add this functionality and set the deprecated
flag to true
for the tokens which are going to be deprecated thus allowing the borrowed positions of the deprecated token to be immediately liquidated thus keeping the protocol stable and safe. The isDeprecated
should be called inside the Comptroller.liquidateBorrowAllowed
function to enable liquidation.
Other
#0 - c4-pre-sort
2023-08-03T13:32:15Z
0xSorryNotSorry marked the issue as primary issue
#1 - c4-sponsor
2023-08-03T22:06:13Z
ElliotFriedman marked the issue as sponsor acknowledged
#2 - c4-judge
2023-08-12T20:10:13Z
alcueca marked the issue as satisfactory
#3 - c4-judge
2023-08-21T21:31:02Z
alcueca marked the issue as selected for report
#4 - c4-judge
2023-08-21T21:31:52Z
alcueca marked issue #67 as primary and marked this issue as a duplicate of 67
🌟 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/main/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L1214-L1218 https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L1237 https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L1214-L1247
The MultiRewardDistributor.sendReward
has declared the _user
as a payable
address as shown below:
function sendReward( address payable _user, uint256 _amount, address _rewardToken ) internal nonReentrant returns (uint256) {
This denotes the _user
can be paid in native ETH
if the emission token
is native token
(denoted in address(0) or otherwise). But in the sendReward
function there is no functionlity handle ETH
transfer via send
, transfer
or low level call
functions.
function sendReward( address payable _user, uint256 _amount, address _rewardToken ) internal nonReentrant returns (uint256) {
token.safeTransfer(_user, _amount);
function sendReward( address payable _user, uint256 _amount, address _rewardToken ) internal nonReentrant returns (uint256) { // Short circuit if we don't have anything to send out if (_amount == 0) { return _amount; } // If pause guardian is active, bypass all token transfers, but still accrue to local tally if (paused()) { return _amount; } IERC20 token = IERC20(_rewardToken); // Get the distributor's current balance uint256 currentTokenHoldings = token.balanceOf(address(this)); // Only transfer out if we have enough of a balance to cover it (otherwise just accrue without sending) if (_amount > 0 && _amount <= currentTokenHoldings) { // Ensure we use SafeERC20 to revert even if the reward token isn't ERC20 compliant token.safeTransfer(_user, _amount); return 0; } else { // If we've hit here it means we weren't able to emit the reward and we should emit an event // instead of failing. emit InsufficientTokensToEmit(_user, _rewardToken, _amount); // By default, return the same amount as what's left over to send, we accrue reward but don't send them out return _amount; } }
Manual Review and VSCode
If the ETH
is not used as a reward payment currency then there is no usecase of declaring _user
as payable
. If the ETH
is used to pay the rewards to _user
, it is recommended to add the functionality in the sendReward
function to handle ETH
balance calculation of the contract and transfer of the specified _amount
.
Payable
#0 - 0xSorryNotSorry
2023-08-01T10:52:05Z
As can be seen in the function body, sendReward is used to send ERC20 only and it's also noted on the NATSPEC;
// SendRewards will attempt to send only if it has enough emission tokens to do so,
The submission could be QA.
#1 - c4-pre-sort
2023-08-01T10:52:10Z
0xSorryNotSorry marked the issue as low quality report
#2 - alcueca
2023-08-13T14:37:07Z
Valid QA
#3 - c4-judge
2023-08-13T14:37:12Z
alcueca changed the severity to QA (Quality Assurance)
#4 - c4-judge
2023-08-13T14:37:21Z
alcueca marked the issue as grade-a
🌟 Selected for report: Sathish9098
Also found by: 0xSmartContract, K42, Udsen, berlin-101, catellatech, cryptonue, hals, jaraxxus, kodyvim, kutugu, solsaver
My findings are based on following areas:
How the supplyCaps
can be used by malicious users to prevent borrower adding liquidity to thier existing position to avoid liquidations
How a malicioius borrower can avoid liquidations by using the tokenRecieved
hook calls in the ERC777 tokens
How certain critical functinalities are missing in the Moonwell.Comptroller.sol
compared to the Compound.Comptroller.sol
contract which it is forked from.
How block.timestamp
overflow can break critical functionality of the protocol.
How functions containing payable address
do not have functionality to handle native ETH
transfers.
How the latestRoundData()
function of chainlink oracles can provide stale prices due to not using the updatedAt
timestamp
I mainly focussed on the MultiRewardDistributor
, ChainlinkCompositeOracle
and TemporalGovernor
contracts since the sponsor had mentioned they were the contracts with specific areas of concern.
Further I analyzed the Comptroller.sol
to find any discrepencies between the Compound.Comptroller.sol
that could lead to vulnerabilities.
Further I audited the MToken
contract and MErc20
contracts since they had main functinality related to asset management and liquidation process.
The MultiRewardDistributor
contract was implementing the novel idea of multiple emission tokens which behave as multiple reward tokens that should be provided to users of the protocol.
More attention should be given to how the rewards are distributed to users during critical functions such as liquidations
and Borrow Repayements
.
Since the rewards are distributed by iterating through each of the emission tokens for a particular market, a single revert in an iteration could completely revert the entire transaction.
Futher How passing the control to an external contract during reward distribution could impact the transaction execution should also considered.
For example if ERC777
which is backward compatible with ERC20
is used as emissionToken
then a malicious user can use the tokenRecieved
hook call of the ERC777 token to revert the entire liquidation transaction.
These vulnearbilities could be extremely dangerous for proper execution of the protocol.
Codebase quality analysis
Codebase was nicely coded and well structured. Most of the functionalities were imported from the Compound
protocol. Few customization were added to core function like Comptroller.mintAllowed
by introducing supplyCap
mapping.
The Comptroller
, MToken
and MErc20
showed close resemblance to core contracts of the Compound
protocol. Few typos in the Natspec
comments were observed and it is recommended to pay more attention on clarity of the Natspec
comments.
For example some of the functions did not have the Natspec
comments on the return
value.
It will make it easier for the audtiors and developers to better understand the codebase.
Centralization risks
Centrelization risk was observed in the MultiRewardDistributor
contract where the admin
was give all the authority within the contract to control key functions such as _addEmissionConfig
etc ...
Further admin acts like an owner of the config to set parameters, and act like the comptroller to kick the reward index updates, and act like a pause guardian to pause things.
Hence in this contract admin plays few major roles.
More focus should be paid on how the multiple token reward distribution is executed. Since this could open more avenues for various vulnerabilities which are common among ERC20
tokens.
These vulnerabilities occur since multiple ERC20 tokens can be configured as emissionTokens
.
15 hours
#0 - c4-judge
2023-08-11T20:42:35Z
alcueca marked the issue as grade-b