Core Contracts

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

Incorrect debt scaling in liquidation leads to overpayment and fund loss

Description

The StabilityPool::liquidateBorrower function incorrectly calculates scaledUserDebt by multiplying the already scaled debt with the reserve usage index a second time. This results in an inflated debt value being used for liquidations, causing the stability pool to overpay for debt positions and potentially drain its reserves.

Proof of Concept

Relevant code snippet:

// StabilityPool.sol
function liquidateBorrower(address userAddress) external {
uint256 userDebt = lendingPool.getUserDebt(userAddress); // Returns already scaled debt
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt()); // Double scaling
}

Test case to demonstrate vulnerability:

In StabilityPool.test.js, add this test and run npx hardhat test --grep "overpays during liquidation due to double scaling"

// test/unit/core/pools/StabilityPool/StabilityPool.test.js
describe("PoC", function () {
it("overpays during liquidation due to double scaling", async function () {
// Mint crvUSD for stability pool and user1
const mintAmount = ethers.parseEther("1000");
await crvusd.mint(user1.address, ethers.parseEther("100"));
await crvusd.mint(stabilityPool.target, mintAmount);
// Set NFT price and mint 1 NFT for user1
const tokenId = 1;
await raacHousePrices.setHousePrice(tokenId, ethers.parseEther("100"));
await crvusd
.connect(user1)
.approve(raacNFT.target, ethers.parseEther("100"));
await raacNFT.connect(user1).mint(1, ethers.parseEther("100"));
// Setup collateral
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
// Setup borrowing
const borrowAmount = ethers.parseEther("100");
await lendingPool.connect(user1).borrow(borrowAmount);
// Accrue compound interest
await ethers.provider.send("evm_increaseTime", [86400 * 365]);
await lendingPool.updateState();
// Get actual debt with compounding
const actualDebt = await lendingPool.getUserDebt(user1.address);
// Trigger undercollateralization
await raacHousePrices.setHousePrice(tokenId, ethers.parseEther("50")); // Reduce collateral value
await lendingPool.connect(user2).initiateLiquidation(user1.address);
// Finalize liquidation after grace period
await ethers.provider.send("evm_increaseTime", [4 * 86400]);
// Liquidate
const preBalance = await crvusd.balanceOf(stabilityPool.target);
await stabilityPool.liquidateBorrower(user1.address);
const postBalance = await crvusd.balanceOf(stabilityPool.target);
const paidAmount = preBalance - postBalance;
// Overpaid as paidAmount > actualDebt
expect(paidAmount).gt(actualDebt);
});
});

Impact

High severity - Direct loss of protocol funds through inflated liquidation payments. The stability pool's crvUSD reserves will be drained faster than actual debt obligations require, potentially making the protocol insolvent.

Recommendation

  • Remove redundant debt scaling in liquidation logic:

// StabilityPool.sol
- uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
+ uint256 scaledUserDebt = userDebt;
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.