Core Contracts

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

Delayed State Update Leads to Underapproved Liquidation Funds and Reverted Liquidations

Summary

The issue occurs because the StabilityPool approves crvUSD for liquidation based on an outdated user debt value obtained before updating the LendingPool state. When the state is subsequently updated, the user's debt increases, causing the approved amount to be insufficient. This mismatch leads to a failed transfer during liquidation, ultimately reverting the process and potentially leaving undercollateralized positions unresolved.

Vulnerability Details

Within the StabilityPool liquidation process, the user debt is first obtained from the LendingPool and then used to approve the LendingPool to withdraw crvUSD for liquidation. However, the LendingPool state—which contains updated indices reflecting accrued interest—is updated after this approval. As a consequence, the approved amount is based on stale data and is lower than the debt calculated after the state update.

  1. Initial Debt Calculation in StabilityPool

    • The StabilityPool calls an internal update (_update()) and then retrieves the user’s debt from the LendingPool:

    // Before approval in StabilityPool.sol
    _update();
    uint256 userDebt = lendingPool.getUserDebt(userAddress);
    // userDebt reflects LendingPool's stored state, which may be outdated
  2. Approving crvUSD for Liquidation

    • after retrieving the debt, the contract approves the LendingPool to pull exactly userDebt scaled in crvUSD:

    // StabilityPool.sol, around line 508
    bool approveSuccess = crvUSDToken.approve(address(lendingPool), userDebt);
    • At this point, the approved amount is based on the pre-update state of the LendingPool with outdated usageIndex.

  3. Updating the LendingPool State

    • The StabilityPool then calls lendingPool.updateState(), which updates the reserve state and refreshes indices (e.g., the usage index) to include newly accrued interest:

    // StabilityPool.sol, around line 511-513
    lendingPool.updateState();
    • After this update, the actual user debt increases (for example, from 150 to 170 crvUSD) due to accruing interest.

  4. Finalizing Liquidation in LendingPool

    • In the LendingPool’s finalizeLiquidation function, the updated user debt is used to burn debt tokens and transfer funds for liquidation:

    // LendingPool.sol, lines ~565-569
    (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
    IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
    IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
    • Because the state update raises the user debt above the previously approved amount, the safeTransferFrom call fails due to insufficient allowance.

Impact

  • A mismatch between approved and actual debt leads to failed liquidations, blocking the process in critical situations.

  • Outdated approvals may accumulate, creating systemic liquidity issues in the protocol.

  • The reversion of liquidations due to insufficient funds can leave the system exposed to risk from non-liquidated, undercollateralized positions.

Tools Used

  • Foundry

  • Manual Review

Recommendations

To address this issue, the LendingPool state should be updated before approving crvUSD for liquidation. This could be achieved by reordering the operations in the StabilityPool's liquidation process, for example:

// StabilityPool.sol
_update();
- uint256 userDebt = lendingPool.getUserDebt(userAddress);
- bool approveSuccess = crvUSDToken.approve(address(lendingPool), userDebt);
- lendingPool.updateState();
+ lendingPool.updateState();
+ uint256 userDebt = lendingPool.getUserDebt(userAddress);
+ bool approveSuccess = crvUSDToken.approve(address(lendingPool), userDebt);
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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.

Give us feedback!