Core Contracts

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

Incorrect liquidation threshold check allows borrowing above collateral value

Summary

The LendingPool::borrow function incorrectly checks the liquidation threshold against the debt amount instead of the collateral value, allowing users to borrow up to 125% of their collateral value.

Vulnerability Details

The issue occurs in the borrow() function where the liquidation threshold check is implemented incorrectly:

// Current implementation
if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}

The check applies the liquidation threshold (80%) to the debt amount instead of the collateral value. This inverts the intended relationship between collateral and debt.

For example, with a liquidation threshold of 80%:

  • Intended: Max debt = collateral * 80%

  • Current: collateral > debt * 80%

  • This means: collateral > debt * 0.8

  • Therefore: debt < collateral / 0.8

  • Result: debt can be up to 125% of collateral value

Attack Path

  1. User deposits NFT worth 100 ETH

  2. With 80% liquidation threshold:

    • Intended max borrow: 80 ETH

    • Actual max borrow: 125 ETH

  3. User can borrow 56.25% more than intended (125 ETH vs 80 ETH)

  4. User now has debt worth more than their collateral

Proof of Concept:

Add the following test to the LendingPool.test.js file:

describe("Allow the user to borrow more than 80% of the collateral value", function () {
beforeEach(async function () {
const depositAmount = ethers.parseEther("1000");
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
const tokenId = 1;
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
});
it("should allow the user to borrow more than 80% of the collateral value", async function () {
const user1CollateralValue = await lendingPool.getUserCollateralValue(user1.address);
const userCollateralValueThreshold = user1CollateralValue * 80n / 100n;
expect(userCollateralValueThreshold).to.be.equal(ethers.parseEther("80"));
const borrowAmount = user1CollateralValue * 100n / 80n; // The value used for throwing NotEnoughCollateralToBorrow()
expect(borrowAmount).to.be.gt(userCollateralValueThreshold);
expect(borrowAmount).to.be.equal(ethers.parseEther("125"));
const user1BalanceBeforeBorrow = await crvusd.balanceOf(user1.address);
await expect(lendingPool.connect(user1).borrow(borrowAmount)).to.not.be.reverted;
const user1BalanceAfterBorrow = await crvusd.balanceOf(user1.address);
expect(user1BalanceAfterBorrow - user1BalanceBeforeBorrow).to.equal(borrowAmount);
});
});

Impact

  • Users can borrow up to 125% of their collateral value

  • Protocol is undercollateralized from the start

Recommendations

Fix the threshold check to apply to collateral:

- if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
+ if (userTotalDebt > collateralValue.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
Updates

Lead Judging Commences

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

LendingPool::borrow as well as withdrawNFT() reverses collateralization check, comparing collateral < debt*0.8 instead of collateral*0.8 > debt, allowing 125% borrowing vs intended 80%

Support

FAQs

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