Core Contracts

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

`Stability::liquidateBorrower` Approval Amount is Wrong Causing Reverts Due to Interest Rate Synchronization

Summary

Completing liquidations via Stability::liquidateBorrower fail due to a timing mismatch in interest rate calculations compared to LendingPool::finalizeLiquidation, causing insufficient approvals for debt repayment. When StabilityPool goes to execute the liquidation the approval amount is stale if interest has accrued, while LendingPool::finalizeLiquidation tries to transfer an amount of reserve assets higher than approved from StabilityPool.

Vulnerability Details

The issue occurs in the following sequence:

[](https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/StabilityPool/StabilityPool.sol#L452-L461)

In StabilityPool::liquidateBorrower:

// Gets debt with old usageIndex (e.g., 1.0)
uint256 userDebt = lendingPool.getUserDebt(userAddress);
@> uint256 scaledUserDebt = WadRayMul.rayMul(userDebt, getNormalizedDebt()); // returns 90e18
// Approves 90e18 tokens
@> crvUSDToken.approve(address(lendingPool), scaledUserDebt);

[](https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/LendingPool/LendingPool.sol#L508-L525)

2. In LendingPool::finalizeLiquidation:

// Updates usageIndex to new value (e.g., 1.002)
@> ReserveLibrary.updateReserveState(reserve, rateData);
// Calculates new debt with updated index
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
// userDebt is now 90.24e18, and the return amountScaled will be 90.24e18
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// Attempts to transfer 90.24e18 tokens (original debt + interest)
// WILL REVERT DUE TO STEP 1 APPROVING 90e18 DUE TO STALE usageIndex
@> IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);

Real example from test trace:

Original Debt: 90000000000000000000 (90e18)
Updated Debt after interest: 90246913425588693651 (90.24e18)
Approved Amount: 90000000000000000000 (90e18)
Result: Transfer fails due to insufficient allowance

Impact

  • Breaks core liquidation functionality

  • Liquidations consistently fail when interest has accrued

  • Risk of protocol becoming undercollateralized

Tools Used

Foundry

Recommendations

  1. Add an approval buffer to account for accrued interest inside StabilityPool::liquidateBorrower:

function liquidateBorrower(address userAddress) external {
uint256 userDebt = lendingPool.getUserDebt(userAddress);
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
+ // Add 5% buffer for accrued interest or whatever possible max interest
+ uint256 buffer = (scaledUserDebt * 5) / 100;
+ uint256 approvalAmount = scaledUserDebt + buffer;
crvUSDToken.approve(address(lendingPool), approvalAmount);
...
}
Updates

Lead Judging Commences

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