exitMarket() function in Comptroller.sol function was missing checks to prevent reentrancy exploitation. This allowed an attacker to call exitMarket() multiple times before the newly borrowed amount was recorded. Since the borrowed amount was not actually recorded on the first exitMarket() call, it removed the borrowed coins from the list of borrowed funds. The attacker was then able to request their collateral be returned, even though they still had an outstanding loan.
function exitMarket(address mTokenAddress) override external returns (uint) {
MToken mToken = MToken(mTokenAddress);
/* Get sender tokensHeld and amountOwed underlying from the mToken */
(uint oErr, uint tokensHeld, uint amountOwed, ) = mToken.getAccountSnapshot(msg.sender);
require(oErr == 0, "exitMarket: getAccountSnapshot failed"); // semi-opaque error code
/* Fail if the sender has a borrow balance */
if (amountOwed != 0) {
return fail(Error.NONZERO_BORROW_BALANCE, FailureInfo.EXIT_MARKET_BALANCE_OWED);
}
/* Fail if the sender is not permitted to redeem all of their tokens */
uint allowed = redeemAllowedInternal(mTokenAddress, msg.sender, tokensHeld);
if (allowed != 0) {
return failOpaque(Error.REJECTION, FailureInfo.EXIT_MARKET_REJECTION, allowed);
}
Market storage marketToExit = markets[address(mToken)];
/* Return true if the sender is not already ‘in’ the market */
if (!marketToExit.accountMembership[msg.sender]) {
return uint(Error.NO_ERROR);
}
/* Set mToken account membership to false */
delete marketToExit.accountMembership[msg.sender];
/* Delete mToken from the account’s list of assets */
// load into memory for faster iteration
MToken[] memory userAssetList = accountAssets[msg.sender];
uint len = userAssetList.length;
uint assetIndex = len;
for (uint i = 0; i < len; i++) {
if (userAssetList[i] == mToken) {
assetIndex = i;
break;
}
}
// We *must* have found the asset in the list or our redundant data structure is broken
assert(assetIndex < len);
// copy last item in list to location of item to be removed, reduce length by 1
MToken[] storage storedList = accountAssets[msg.sender];
storedList[assetIndex] = storedList[storedList.length - 1];
storedList.pop();
emit MarketExited(mToken, msg.sender);
return uint(Error.NO_ERROR);
}
Tools Used
VsCode
Recommended Mitigation Steps
Consider implementing the non reentrant modifier for exit market too
Lines of code
https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Comptroller.sol#L154-L205
Vulnerability details
Impact
exitMarket() function in Comptroller.sol function was missing checks to prevent reentrancy exploitation. This allowed an attacker to call exitMarket() multiple times before the newly borrowed amount was recorded. Since the borrowed amount was not actually recorded on the first exitMarket() call, it removed the borrowed coins from the list of borrowed funds. The attacker was then able to request their collateral be returned, even though they still had an outstanding loan.
Proof of Concept
here is instance: https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Comptroller.sol#L154-L205
Tools Used
VsCode
Recommended Mitigation Steps
Assessed type
Reentrancy