Moonwell

Moonwell
DeFiFoundry
15,000 USDC
View results
Submission Details
Severity: high
Invalid

Liquidated accounts do not receive supplier and borrower rewards

Summary

Though users should receive rewards in all markets even in case of liquidation, they do not receive any rewards in case of manual liquidation via the MErc20DelegateFixer.fixUser function.

Vulnerability Details

The MErc20DelegateFixer.fixUser function bypasses several calls to the Comptroller which are obligatory for all transfers and liquidations. Besides the account healthiness and liquidation availability checks the Comptroller contract calculates and distributes borrower and supplier rewards. So the expected behavior of the protocol is to reward all users. In case the user has not claimed rewards for a long time the losses can be sufficient.

function repayBorrowAllowed(
address mToken,
address payer,
address borrower,
uint repayAmount) external returns (uint) {
// Shh - currently unused
payer;
borrower;
repayAmount;
if (!markets[mToken].isListed) {
return uint(Error.MARKET_NOT_LISTED);
}
// Keep the flywheel moving
Exp memory borrowIndex = Exp({mantissa: MToken(mToken).borrowIndex()});
updateAndDistributeBorrowerRewardsForToken(mToken, borrower, borrowIndex);
return uint(Error.NO_ERROR);
}
function updateAndDistributeBorrowerRewardsForToken(address mToken, address borrower, Exp memory marketBorrowIndex) internal {
for (uint8 rewardType = 0; rewardType <= 1; rewardType++) {
updateRewardBorrowIndex(rewardType, mToken, marketBorrowIndex);
distributeBorrowerReward(rewardType, mToken, borrower, marketBorrowIndex);
}
}
function distributeBorrowerReward(uint8 rewardType, address mToken, address borrower, Exp memory marketBorrowIndex) internal {
require(rewardType <= 1, "rewardType is invalid");
RewardMarketState storage borrowState = rewardBorrowState [rewardType][mToken];
Double memory borrowIndex = Double({mantissa: borrowState.index});
Double memory borrowerIndex = Double({mantissa: rewardBorrowerIndex[rewardType][mToken][borrower]});
rewardBorrowerIndex[rewardType][mToken][borrower] = borrowIndex.mantissa;
if (borrowerIndex.mantissa > 0) {
Double memory deltaIndex = sub_(borrowIndex, borrowerIndex);
uint borrowerAmount = div_(MToken(mToken).borrowBalanceStored(borrower), marketBorrowIndex);
uint borrowerDelta = mul_(borrowerAmount, deltaIndex);
uint borrowerAccrued = add_(rewardAccrued[rewardType][borrower], borrowerDelta);
rewardAccrued[rewardType][borrower] = borrowerAccrued;
emit DistributedBorrowerReward(rewardType, MToken(mToken), borrower, borrowerDelta, borrowIndex.mantissa);
}
}

https://github.com/Cyfrin/2024-03-Moonwell/blob/e57b8551a92824d35d4490f5e7f27c373be172bd/src/Comptroller.sol#L415-L434

function transferAllowed(address mToken, address src, address dst, uint transferTokens) external returns (uint) {
// Pausing is a very serious situation - we revert to sound the alarms
require(!transferGuardianPaused, "transfer is paused");
// Currently the only consideration is whether or not
// the src is allowed to redeem this many tokens
uint allowed = redeemAllowedInternal(mToken, src, transferTokens);
if (allowed != uint(Error.NO_ERROR)) {
return allowed;
}
// Keep the flywheel moving
updateAndDistributeSupplierRewardsForToken(mToken, src);
updateAndDistributeSupplierRewardsForToken(mToken, dst);
return uint(Error.NO_ERROR);
}

https://github.com/Cyfrin/2024-03-Moonwell/blob/e57b8551a92824d35d4490f5e7f27c373be172bd/src/Comptroller.sol#L601-L617

Impact

Asset losses, unexpected behavior.

Tools used

Manual Review

Recommendations

Consider calling the Comptroller.claimReward function for each liquidated user before zero out balances.

Updates

Lead Judging Commences

0xnevi Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Other
pontifex Submitter
about 1 year ago
0xnevi Lead Judge
about 1 year ago
pontifex Submitter
about 1 year ago
0xnevi Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Other

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.