Core Contracts

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

Stability Pool lacks crvUSD deposit mechanism, breaking liquidation functionality

Summary

The Stability Pool contract lacks a mechanism to obtain crvUSD needed for liquidations, despite accepting rToken deposits from users. This breaks the core liquidation functionality

Vulnerability Details

The StabilityPool contract accepts rToken deposits from users and is meant to participate in liquidations by providing crvUSD to repay defaulted loans. However, there is no mechanism to convert the deposited rToken to crvUSD needed for liquidations:

  1. The StabilityPool::liquidateBorrower() function requires crvUSD to repay debt:

uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
  1. But the contract only accepts rToken deposits and has no way to:

  • Accept direct crvUSD deposits

  • Withdraw rToken from LendingPool to get crvUSD

This means users can deposit rToken but the StabilityPool can never perform its core liquidation function.

Impact

  1. Core protocol functionality is broken - liquidations through StabilityPool cannot work

  2. Protocol security is compromised as bad debt cannot be liquidated through the StabilityPool

Tools Used

Manual review

Proof of Concept

Add the following test to test/e2e/protocols-tests.js:

describe("StabilityPool", function() {
// other tests...
it('does not have a mechanism to get crvUSD', async function () {
// Setup stability pool deposit
await contracts.stabilityPool.connect(user1).deposit(STABILITY_DEPOSIT);
// Create position to be liquidated
const newTokenId = HOUSE_TOKEN_ID + 2;
await contracts.housePrices.setHousePrice(newTokenId, HOUSE_PRICE);
await contracts.crvUSD.connect(user2).approve(contracts.nft.target, HOUSE_PRICE);
await contracts.nft.connect(user2).mint(newTokenId, HOUSE_PRICE);
await contracts.nft.connect(user2).approve(contracts.lendingPool.target, newTokenId);
await contracts.lendingPool.connect(user2).depositNFT(newTokenId);
await contracts.lendingPool.connect(user2).borrow(BORROW_AMOUNT);
// Trigger and complete liquidation
await contracts.housePrices.setHousePrice(newTokenId, HOUSE_PRICE * 10n / 100n);
await contracts.lendingPool.connect(user3).initiateLiquidation(user2.address);
await time.increase(73 * 60 * 60);
// Will call the lendingPool.finalizeLiquidation(user2.address)
// We need stability pool to have crvUSD to cover the debt, but the stability pool only has rToken
// and there is no way to convert it to crvUSD
const initialcrvUSDBalance = await contracts.crvUSD.balanceOf(contracts.stabilityPool.target);
const initialRTokenBalance = await contracts.rToken.balanceOf(contracts.stabilityPool.target);
expect(initialcrvUSDBalance).to.be.equal(0);
expect(initialRTokenBalance).to.be.gt(0);
await contracts.lendingPool.connect(owner).updateState();
// Liquidation fails due to lack of crvUSD
await expect(contracts.stabilityPool.connect(owner).liquidateBorrower(user2.address)).to.be.revertedWithCustomError(contracts.stabilityPool, "InsufficientBalance");
});
});

Recommendations

  1. Add mechanism to convert rTokens to crvUSD:

function withdrawRTokensForLiquidation(uint256 amount) internal {
lendingPool.withdraw(amount); // Converts rTokens to crvUSD
}
  1. Call this before liquidations:

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
// other code..
uint256 userDebt = lendingPool.getUserDebt(userAddress);
withdrawRTokensForLiquidation(userDebt);
// Continue with liquidation
}
Updates

Lead Judging Commences

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

StabilityPool design flaw where liquidations will always fail as StabilityPool receives rTokens but LendingPool expects it to provide crvUSD

Support

FAQs

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