Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

liquidateBorrower() fetches data without a call to updateState() first and hence can revert

Description

liquidateBorrower() calls lendingPool.getUserDebt(userAddress) without first calling lendingPool.updateState() and hence works on a figure based on an outdated usageIndex. The call is made only later on in the function:

File: contracts/core/pools/StabilityPool/StabilityPool.sol
449: function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
450: _update();
451: // Get the user's debt from the LendingPool.
452:@---> uint256 userDebt = lendingPool.getUserDebt(userAddress);
453: uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
454:
455: if (userDebt == 0) revert InvalidAmount();
456:
457: uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
458: if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
459:
460: // Approve the LendingPool to transfer the debt amount
461:@---> bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
462: if (!approveSuccess) revert ApprovalFailed();
463: // Update lending pool state before liquidation
464:@---> lendingPool.updateState();
465:
466: // Call finalizeLiquidation on LendingPool
467: lendingPool.finalizeLiquidation(userAddress);
468:
469: emit BorrowerLiquidated(userAddress, scaledUserDebt);
470: }

Impact

The debt could well have increased since the last state update. As a result, on L461 allowance approval could be granted for an outdated lower debt value. When updateState() gets called on L464, the usageIndex is incremented to the actual value and hence call to finalizeLiquidation() on L467 fails due to insufficient approval here:

File: contracts/core/pools/LendingPool/LendingPool.sol
496: function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
497: if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
498:
499: // update state
500: ReserveLibrary.updateReserveState(reserve, rateData);
501:
502: if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
503: revert GracePeriodNotExpired();
504: }
505:
506: UserData storage user = userData[userAddress];
507:
508: uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
509:
510:
511: isUnderLiquidation[userAddress] = false;
512: liquidationStartTime[userAddress] = 0;
513: // Transfer NFTs to Stability Pool
514: for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
515: uint256 tokenId = user.nftTokenIds[i];
516: user.depositedNFTs[tokenId] = false;
517: raacNFT.transferFrom(address(this), stabilityPool, tokenId);
518: }
519: delete user.nftTokenIds;
520:
521: // Burn DebtTokens from the user
522: (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
523:
524: // Transfer reserve assets from Stability Pool to cover the debt
525:@---> IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
526:
527: // Update user's scaled debt balance
528: user.scaledDebtBalance -= amountBurned;
529: reserve.totalUsage = newTotalSupply;
530:
531: // Update liquidity and interest rates
532: ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
533:
534:
535: emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
536: }

Mitigation

File: contracts/core/pools/StabilityPool/StabilityPool.sol
449: function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
450: _update();
451: // Get the user's debt from the LendingPool.
+ 451: lendingPool.updateState();
452: uint256 userDebt = lendingPool.getUserDebt(userAddress);
453: uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
454:
455: if (userDebt == 0) revert InvalidAmount();
456:
457: uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
458: if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
459:
460: // Approve the LendingPool to transfer the debt amount
461: bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
462: if (!approveSuccess) revert ApprovalFailed();
463: // Update lending pool state before liquidation
- 464: lendingPool.updateState();
465:
466: // Call finalizeLiquidation on LendingPool
467: lendingPool.finalizeLiquidation(userAddress);
468:
469: emit BorrowerLiquidated(userAddress, scaledUserDebt);
470: }
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

StabilityPool: liquidateBorrower should call lendingPool.updateState earlier, to ensure the updated usageIndex is used in calculating the scaledUserDebt

Support

FAQs

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