Core Contracts

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

Reserve states are not updated before approving amount to be spent by Lending Pool

Summary

In StabilityPool.sol, manager or owner can call liquidateBorrower()function, whereby a user will be liquidated. However, before calculating the user's debt, the reserve state is not updated. This will result the amount approved for Lending Pool to spend on behalf can be less than the actual debt of the user.

Vulnerability Details

Assume this scenario:

  • Alice borrowed 10,000 crvUSD and was minted 10,000 DebtTokens

  • A manager decides to liquidate Alice via liquidateBorrower(). At this point, assume the stored usage index = 1.1

liquidateBorrower()

  1. Alice's debt is calculated using usage index = 1.1

    1. in code snippet below taken from liquidateBorrower():

      uint256 userDebt = lendingPool.getUserDebt(userAddress);
      uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
    2. In lendingPool.getUserDebt(), it takes user.scaledDebtBalance.rayMul(reserve.usageIndex)

    3. userDebt will be multiplied with usage index again, as lendingPool.getNormalizedDebt()will return the reserve usage index = 1.1

    4. The above (iii) seems to be incorrect, as it is multiplying by usage Index a second time, over-scaling the debt. For the sake of this particular vulnerability, ignore the double multiplication and assume scaledUserDebt == userDebt

  2. Now, the approve happens

    1. scaledUserDebt is approved to be spent by Lending Pool

      bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
      if (!approveSuccess) revert ApprovalFailed();
      // Update lending pool state before liquidation
      lendingPool.updateState();
      // Call finalizeLiquidation on LendingPool
      lendingPool.finalizeLiquidation(userAddress);
    2. The state of the reserves is only updated after via lendingPool.updateState()

  3. Assume at the point the reserves are updated after approving scaledUserDebt, the reserve usage index is now 1.4

  4. Now, finalizeLiquidation()is called

finalizeLiquidation()

  1. The usage index used to define the amount to be approved is 1.1 in liquidateBorrower(). However, in finalizeLiquidation(), the current usage index = 1.4

    1. Again, the reserve state is updated in updateReserveState()

      // update state
      ReserveLibrary.updateReserveState(reserve, rateData);
      if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
      revert GracePeriodNotExpired();
      }
      UserData storage user = userData[userAddress];
      uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
    2. The calculation of userDebt as seen above will then use usage index = 1.4

  2. After transferring the RAAC NFTs to Stability Pool, Debt Tokens are burned

    1. In debtToken.burn(), the amount of DebtToken burned is the amountpassed, which is userDebtas seen below

      (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
    2. in the snippet below, amountis burned and then amount value is returned from debtToken.burn()

      _burn(from, amount.toUint128());
      emit Burn(from, amountScaled, index);
      return (amount, totalSupply(), amountScaled, balanceIncrease);
  3. Now, crvUSD is being transferred from Stability Pool to R Token address, using amountScaledvalue

    // Transfer reserve assets from Stability Pool to cover the debt
    IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
  4. Since usage index used to calculate userDebt for approved amount = 1.1 in liquidateBorrower(),and usage index used in finalizeLiquidation()= 1.4, that would mean the transfer above will fail, as the approved amount < amount to be transferred.

Impact

If reserve state is not updated before calculating user debt in liquidateBorrower(), the calculation will use an outdated value for the usage index. In finalizeLiquidation(), it instead uses freshest usage index. The amount approved could be more or less than the actual debt of the user. This can result finalizeLiquidation()to revert in the event amount approved is less than the actual debt.

Tools Used

Manual

Recommendations

Ensure reserve state is updated before calculating the amount to be approved for Lending Pool to spend.

Updates

Lead Judging Commences

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

StabilityPool::liquidateBorrower double-scales debt by multiplying already-scaled userDebt with usage index again, causing liquidations to fail

Support

FAQs

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

Give us feedback!