Core Contracts

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

Liquidation will revert due to updating state of reservers after calculation

Summary

When the admin or manager calls the liquidateBorrower function, it will revert if the index of liquidity or usage is not updated.

Vulnerability Details

To liquidate a borrower, they must first be added to the liquidation mapping in the lending pool contract. After the grace period expires, the admin/manager can call liquidateBorrower to proceed with liquidation.

However, this function will revert if the reserves are not updated, as it first calculates the debt amount the caller must pay on behalf of the liquidator to liquidate the borrower's position.

let have a look at liquidateBorrower function:

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

From the above code, it is evident that:

  • The scaledUserDebt amount is first calculated, and approval is given to the lending pool.

  • The updateState function is then called to update liquidityIndex and usageIndex.

The liquidation process first determines the amount the liquidator must pay before updating the indexes.If updateState modifies the indexes during execution, it can cause the transaction to revert.

Attackers can front-run liquidation calls, borrowing or repaying loans to ensure updateState modifies the usageIndex, triggering a reversion. Block stuffing can delay liquidation, causing repeated reverts, leading to an increasing debt burden.

POC

Add following POC to file and run with command :npx hardhat test:

it.only("should allow user to borrow crvUSD using NFT collateral", async function () { // @audit POC
const borrowAmount = ethers.parseEther("100");
await lendingPool.connect(user1).borrow(borrowAmount);
await raacHousePrices.setHousePrice(1, ethers.parseEther("10"));
await lendingPool.connect(user2).initiateLiquidation(user1.address);
await ethers.provider.send("evm_increaseTime", [72 * 60 * 60 + 1]);
await ethers.provider.send("evm_mine");
const crvUSDBalance = await crvusd.balanceOf(user1.address);
await stabilityPool.liquidateBorrower(user1.address)
});

Note : In the above test case, I fast-forwarded time to ensure that updateState affects the usageIndex. However, any user can front-run this call or ensure that it gets executed in the next block.

Impact

Updating the state after calculation will result in a revert if the updateState function modifies the usageIndex.

Tools Used

Manual Review

Recommendations

diff --git a/contracts/core/pools/StabilityPool/StabilityPool.sol b/contracts/core/pools/StabilityPool/StabilityPool.sol
index 8dd9f17..164b24e 100644
--- a/contracts/core/pools/StabilityPool/StabilityPool.sol
+++ b/contracts/core/pools/StabilityPool/StabilityPool.sol
@@ -450,6 +450,8 @@ contract StabilityPool is IStabilityPool, Initializable, ReentrancyGuard, Ownabl
*/
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
+ lendingPool.updateState();
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!