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:
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:
461:@---> bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
462: if (!approveSuccess) revert ApprovalFailed();
463:
464:@---> lendingPool.updateState();
465:
466:
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:
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:
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:
522: (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
523:
524:
525:@---> IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
526:
527:
528: user.scaledDebtBalance -= amountBurned;
529: reserve.totalUsage = newTotalSupply;
530:
531:
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: }