Core Contracts

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

Incorrect debt calculation in liquidation flow

Summary

The function StabilityPool::liquidateBorrower() allows the owner or manager to finalize a liquidation by using the funds in StabilityPool contract. However, the function calculates the debt amount inaccurately which can cause liquidation reverts due to insufficient balance

Vulnerability Details

The function liquidateBorrower() calls LendingPool contract to fetch user debt by lendingPool.getUserDebt(userAddress). The function LendingPool::getUserDebt() returns the debt including interest.

The problem arises when that returned value is scaled once more by lending pool's normalized debt uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt()). This causes the result of calculating debt higher than the actual position debt. Hence, this can make the balance check fail when the manager/owner only prepares enough funds (not excessive) for the liquidation.

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
@> uint256 userDebt = lendingPool.getUserDebt(userAddress);
@> uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
@> uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
@> if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
// Approve the LendingPool to transfer the debt amount
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);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}

PoC

Add below test to test/unit/core/pools/StabilityPool/StabilityPool.test.js

describe("Deposits", function () {
///...
it.only('debt is incorrectly computed in liquidate flow', async function(){
// @audit POC debt is incorrectly computed in liquidate flow
// user1 borrows
await userBorrow(user1, ethers.parseEther("100"), ethers.parseEther("100") , 1)
// time passes
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
// initiate liquidation
await lendingPool.connect(user2).initiateLiquidation(user1.address);
// grace period passed
await ethers.provider.send("evm_increaseTime", [3 * 86400 + 1]);
await ethers.provider.send("evm_mine");
// update lending pool state
await lendingPool.updateState();
// add user2 as manager
await stabilityPool.connect(owner).addManager(user2.address, 1n)
// get user1's total debt
let debt = await lendingPool.getUserDebt(user1.address);
// prepare for liquidation
// note this prepared fund is exactly enough, not excessive
await crvusd.mint(user2.address, debt)
await crvusd.connect(user2).transfer(stabilityPool.target, debt)
// this call failed
await stabilityPool.connect(user2).liquidateBorrower(user1.address);
})
// Helper function
async function userBorrow(user, collateralValue, borrowAmount, nftId){
const tokenId = nftId;
const price = collateralValue;
await raacHousePrices.setOracle(owner.address);
await raacHousePrices.setHousePrice(tokenId, price);
await crvusd.mint(user.address, price)
await crvusd.connect(user).approve(raacNFT.target, price)
await raacNFT.connect(user).mint(tokenId, price);
await raacNFT.connect(user).approve(lendingPool.target, tokenId);
await lendingPool.connect(user).depositNFT(tokenId);
await lendingPool.connect(user).borrow(borrowAmount);
}

Run the test and console shows

StabilityPool
Core Functionality
Deposits
1) debt is incorrectly computed in liquidate flow
0 passing (2s)
1 failing
1) StabilityPool
Core Functionality
Deposits
debt is incorrectly computed in liquidate flow:
Error: VM Exception while processing transaction: reverted with custom error 'InsufficientBalance()'
at StabilityPool.liquidateBorrower (contracts/core/pools/StabilityPool/StabilityPool.sol:494)

Impact

  • Incorrect debt amount calculated in liquidation flow, which forces owner/manager to use more funds than expected

Tools Used

Manual

Recommendations

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
uint256 userDebt = lendingPool.getUserDebt(userAddress);
- uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
- if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
+ if (crvUSDBalance < userDebt) revert InsufficientBalance();
// Approve the LendingPool to transfer the debt amount
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);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}
`
Updates

Lead Judging Commences

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