Core Contracts

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

Uncapped Stability Pool Losses Due to Debt Expansion and Inefficient Liquidations

Vulnerability Details

The StabilityPool is exposed to potentially unlimited losses due to a combination of uncapped debt growth and forced absorption of underwater positions during liquidations. This vulnerability stems from the interaction between DebtToken's compound interest mechanism and the StabilityPool's liquidation responsibilities.

The DebtToken contract implements automatic debt scaling through its balance calculation:

function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}

Durning liquidations, the stability pool must cover the entire scaled debt amount

function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
}

The liquidation mechanism includes a grace period during which debt continues to compound:

if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}

This creates a feedback loop where:

  • DebtToken's balance grows exponentially through compound interest (up to 400% APR)

  • The grace period allows additional debt accumulation before liquidation

  • The StabilityPool must absorb the full debt regardless of collateral value

  • High interest rates combined with falling collateral values can create severe losses

Impact

  • StabilityPool depositors face unlimited downside risk

  • Single toxic position can drain significant pool liquidity

  • System vulnerable to malicious debt accumulation

  • Risk of cascading liquidations during market stress

  • Potential for complete protocol insolvency

Proof of Concept

A coded function in the LendingPool.test

it("should handle severe collateral value drop leaving stability pool with bad debt", async function () {
// Use the known working borrow amount
const borrowAmount = ethers.parseEther("30");
await lendingPool.connect(user1).borrow(borrowAmount);
// Log initial state
const initialHealthFactor = await lendingPool.calculateHealthFactor(
user1.address
);
const initialDebt = await lendingPool.getUserDebt(user1.address);
console.log("\nInitial State:");
console.log(
"Initial Health Factor:",
ethers.formatEther(initialHealthFactor)
);
console.log("Initial Debt:", ethers.formatEther(initialDebt));
// Drop collateral value severely
await raacHousePrices.setHousePrice(1, ethers.parseEther("5"));
// Check new health factor
const newHealthFactor = await lendingPool.calculateHealthFactor(
user1.address
);
console.log("\nAfter Price Drop:");
console.log("New Health Factor:", ethers.formatEther(newHealthFactor));
// Initiate liquidation
await lendingPool.connect(user2).initiateLiquidation(user1.address);
expect(await lendingPool.isUnderLiquidation(user1.address)).to.be.true;
// Wait for grace period
await ethers.provider.send("evm_increaseTime", [72 * 60 * 60 + 1]);
await ethers.provider.send("evm_mine", []);
// Setup stability pool
await crvusd
.connect(owner)
.mint(owner.address, ethers.parseEther("1000"));
await lendingPool.connect(owner).setStabilityPool(owner.address);
// Record stability pool balance
const stabilityPoolInitialBalance = await crvusd.balanceOf(owner.address);
// Execute liquidation
await lendingPool.connect(owner).finalizeLiquidation(user1.address);
// Calculate stability pool loss
const stabilityPoolFinalBalance = await crvusd.balanceOf(owner.address);
const stabilityPoolLoss =
stabilityPoolInitialBalance - stabilityPoolFinalBalance;
// Verify outcomes
expect(await lendingPool.isUnderLiquidation(user1.address)).to.be.false;
expect(await raacNFT.ownerOf(1)).to.equal(owner.address);
expect(await lendingPool.getUserDebt(user1.address)).to.equal(0);
// The key finding: stability pool loss is much larger than the recovered collateral value
expect(stabilityPoolLoss).to.be.gt(
await raacHousePrices.tokenToHousePrice(1)
);
console.log("\nStability Pool Impact:");
console.log("Total Loss:", ethers.formatEther(stabilityPoolLoss));
console.log(
"Final Collateral Value:",
ethers.formatEther(await raacHousePrices.tokenToHousePrice(1))
);
});

the Console logs shows

Initial State:
Initial Health Factor: 0.727272725826131806
Initial Debt: 110.000000218797564727
After Price Drop:
New Health Factor: 0.03636363629130659
Stability Pool Impact:
Total Loss: 110.027268784048089803
Final Collateral Value: 5.0
  1. Initial debt amplification:

    • We borrowed only 30 USDC

    • But immediately the debt grew to ~110 USDC (Initial Debt: 110.000000218797564727)

    • This suggests there are significant fees or interest charges applied at the time of borrowing

  2. Collateral value vs Debt disparity:

    • Final collateral value: 5 USDC

    • Total stability pool loss: ~110 USDC

    • This means the stability pool takes a loss of ~105 USDC that cannot be recovered

  3. Health Factor behavior:

    • Initial Health Factor: ~0.73 (already below 1.0)

    • After price drop: ~0.036 (significantly impaired)

    • This drop in health factor triggers liquidation

The key vulnerability is that there's no mechanism to prevent or mitigate situations where:

  1. The debt grows significantly larger than the initial borrow amount

  2. The collateral value drops far below the outstanding debt

  3. The stability pool has to absorb the full difference with no recourse

Recommendations

  1. Cap debt growth during the grace period by modifying DebtToken.sol

  2. Implement a "circuit breaker" in StabilityPool.sol that can pause new deposits if loss rates exceed sustainable levels.

Updates

Lead Judging Commences

inallhonesty Lead Judge
4 months ago
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

StabilityPool has no ability to liquidate large positions due to all-or-nothing design - partial liquidation not supported, risking protocol insolvency

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.