Core Contracts

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

User could not be liquidated because debt is calculate with an outdated reserve state

Summary

User could not be liquidated because debt is calulated on an outdated state of the reserve in the StabilityPoolwhen we call the liquidateBorrower function, we aprove the amount to be spend and after that in the LendingPool when the state is updated we get higer debt that could not be payed as a smaller value for the LendingPool to spend has been approved.

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));
console.log("User debt amount approved to be spent by Stability Pool:", scaledUserDebt);
console.log("crvUSD balance of Stability Pool is:", crvUSDBalance);
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
//@Audit Lending Pool state is update after debt is calculated and the amount to spend is approved
lendingPool.updateState();
// Call finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}

Vulnerability Details

The vunerability root cause is that the state of the reserve of the LendingPool is outdated when we calculate and approve the amount of debt to be payed by the StabilityPool.

Impact

Users are not liquidatable unless someone updates the state of the reserve in the LendingPool.

Tools Used

  • Manual Review

  • Unit Test

PoC

Place this test in the StabilityPool.test.js file as part of the describe("Core Functionality") test suite group.

it("POC Stability Pool insufficent balance approval to cover liquidation debt", async function () {
//We approve additional amount of asset to users1 in order to be able to buy an NFT
const mintAmount = ethers.parseEther("1000");
await crvusd.mint(user1.address, mintAmount);
//We mint enough curvUSD assets to the StabilityPool in order to be able to repay the user debt
await crvusd.mint(stabilityPool.target, ethers.parseEther("81"));
//We set a price for the NFT and buy it as user1
await raacHousePrices.setOracle(owner.address);
await raacHousePrices.setHousePrice(1, ethers.parseEther("100"));
await ethers.provider.send("evm_mine", []);
const tokenId = 1;
const amountToPay = ethers.parseEther("100");
await crvusd.connect(user1).approve(raacNFT.target, amountToPay);
await raacNFT.connect(user1).mint(tokenId, amountToPay);
// User1 deposits NFT into Lending Pool and borrows 80 worth of ETH form the LendingPool
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
const borrowAmount = ethers.parseEther("80");
await lendingPool.connect(user1).borrow(borrowAmount);
//NFT price degrades and user1 is now could be liquidated
await raacHousePrices.setHousePrice(1, ethers.parseEther("90"));
console.log("User 1 health factor is: ", await lendingPool.calculateHealthFactor(user1.address));
await lendingPool.connect(user2).initiateLiquidation(user1.address);
console.log("Simulating 72hours have passed(this is the grace period)....")
// Advance time beyond grace period (72 hours)
await ethers.provider.send("evm_increaseTime", [72 * 60 * 60 + 1]);
await ethers.provider.send("evm_mine");
//User1 has not repayed his debt so we can finilize the liquidation
console.log("User 1 debt is: ", await lendingPool.getUserDebt(user1.address));
const actualUserDebt = 80018031266118120000n;
const calculateUserDebt = ethers.parseEther("80");
//StabilityPool attempts to liquidate user1 and fails because not enough balance is approved to be spent
await expect(stabilityPool.liquidateBorrower(user1.address))
.to.be.revertedWithCustomError(crvusd, "ERC20InsufficientAllowance")
.withArgs(lendingPool, calculateUserDebt, (actualAmount) => {
expect(actualAmount).to.be.closeTo(actualUserDebt, ethers.parseEther("0.0000001"));
return true; // This makes .withArgs() accept the value as it is not an exact match
});
});

Recommendations

Move the lendingPool.updateState() call to the top of the liquidateBorrower function so that the debt is calculated against an up to date reserve state:

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
+ lendingPool.updateState();
// 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));
console.log("User debt amount approved to be spent by Stability Pool:", scaledUserDebt);
console.log("crvUSD balance of Stability Pool is:", crvUSDBalance);
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);
}
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!