Core Contracts

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

Over-Borrowing Due to Inverted LTV Check in LendingPool Borrow Function

Summary

In the LendingPool contract, the comparison used to check the Loan-to-Value (LTV) threshold is inverted, causing the protocol to allow borrowing amounts that exceed the intended liquidation threshold. Instead of enforcing a strict limit (for example, an 80% LTV), the code permits effectively borrowing up to 125% when liquidationThreshold is set to 80%. This discrepancy arises from a condition that compares collateralValue < userTotalDebt.percentMul(liquidationThreshold) rather than the expected userTotalDebt > collateralValue.percentMul(liquidationThreshold). Consequently, users can surpass the supposed LTV cap and face immediate liquidation risk, while the protocol absorbs additional potential defaults beyond the intended safety margin.

Vulnerability Details

This issue stems from an inverted check in the borrow() function of the LendingPool contract. Rather than confirming that the user’s total debt does not exceed the liquidation threshold (userTotalDebt <= collateralValue × liquidationThreshold), the code performs the reverse check (collateralValue < userTotalDebt.percentMul(liquidationThreshold)). As a result, when liquidationThreshold is configured at 80%, the user can borrow an amount equivalent to 125% of their collateral, instead of being limited to 80%. This discrepancy directly undermines the intended LTV cap and exposes both the user and the protocol to immediate liquidation scenarios, with the potential for excessive defaults beyond the originally designed safety margin.

Impact

Allowing borrowers to exceed the intended liquidation threshold significantly weakens the protocol’s security. Users can immediately open positions above the safe LTV range (e.g., 125% instead of 80%), placing them at high liquidation risk. In turn, this increases the likelihood of defaults and poses a significant financial risk to the system as a whole. The discrepancy between the stated threshold and the actual enforced limit can undermine user trust, as positions become disproportionately overleveraged, and liquidation may occur in scenarios the protocol intended to prevent.

Proof of Concept

The logical vulnerability arises from an inverted condition used to ensure the Loan-to-Value (LTV) ratio does not exceed a specified threshold. This allows users to borrow beyond the intended “liquidationThreshold” (e.g., 80%), effectively reaching a 125% LTV if liquidationThreshold = 80%. As a result, users can exceed the “safe” borrowing limit and become immediately eligible for liquidation.

Code Analysis

Below is a concise extract from the borrow() function, highlighting the key portion of the bug:

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// ... Preliminary code ...
// Retrieve collateral value
uint256 collateralValue = getUserCollateralValue(msg.sender);
// Check that collateral exists
if (collateralValue == 0) revert NoCollateral();
// (!) Inverted comparison that allows higher-than-intended LTV
if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
// Scaled debt calculation and update
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
// ... Subsequent code ...
}

Explanation

The line:

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

is logically equivalent to:

collateralValue >= userTotalDebt * liquidationThreshold

In other words:

userTotalDebt <= collateralValue / liquidationThreshold

If liquidationThreshold is 80% (0.80), this effectively allows borrowing up to 125% of collateral (1 / 0.80 = 1.25), instead of 80%. Ideally, the correct check should be:

if (userTotalDebt > collateralValue.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}

Vulnerable Scenario

  1. A user deposits an NFT valued at 1000.

  2. The liquidationThreshold is set to 80%.

  3. Due to the inverted check, the borrow() function allows the user to take on 1250 of debt instead of the intended maximum of 800.

  4. The position ends up with an LTV of 125%.

  5. Other participants can immediately trigger liquidation since the actual LTV exceeds the safety threshold.

Test and Result

The test sets an NFT’s value to 100 crvUSD and applies an 80% liquidation threshold, meaning the user should only be allowed to borrow up to 80 crvUSD. Instead, the user successfully borrows 125 crvUSD (125% LTV) without reverting. This outcome confirms that the check responsible for enforcing the threshold is inverted, thereby allowing borrow amounts above the intended limit. The balance and debt verifications further prove that the logic is flawed, as it fails to trigger the expected "NotEnoughCollateralToBorrow" error.

  • Add the following test inside the describe("Borrow and Repay", function () {}) block in test/unit/core/pools/LendingPool/LendingPool.test.js.

it("allows borrowing above the intended LTV threshold due to inverted check", async function () {
// Given a liquidation threshold of 80% and the NFT priced at 100 crvUSD,
// the user theoretically should not be able to borrow more than 80 crvUSD.
// However, due to the inverted comparison, the contract incorrectly permits ~125 crvUSD.
// Set the NFT price to 100 crvUSD
await raacHousePrices.setHousePrice(1, ethers.parseEther("100"));
// Attempt to borrow 125 crvUSD (125% of the collateral)
const borrowAmount = ethers.parseEther("125");
// Because of the inverted logic, this call does not revert
await expect(lendingPool.connect(user1).borrow(borrowAmount)).not.to.be
.reverted;
// Verify that the user’s balance is now 1125 crvUSD
// (1000 initial balance + 125 borrowed = 1125)
const user1BalanceAfterBorrow = await crvusd.balanceOf(user1.address);
expect(user1BalanceAfterBorrow).to.equal(ethers.parseEther("1125"));
// Confirm that the user’s total debt is 125
const userDebtAfterBorrow = await debtToken.balanceOf(user1.address);
expect(userDebtAfterBorrow).to.equal(borrowAmount);
// With the correct threshold check, this should have reverted:
// "NotEnoughCollateralToBorrow"
// but it does not, confirming the logical flaw.
});
LendingPool
Borrow and Repay
✔ allows borrowing above the intended LTV threshold due to inverted check

Confirmation of the Finding

  • The code comments indicate an 80% liquidation threshold.

  • Because of the inverted comparison, the user can exceed that threshold.

  • In practice, the user accrues more debt than permitted, exposing them to instant liquidation and contradicting the original validation intent.

This confirms that the issue is real and constitutes a significant logical vulnerability in how LTV is enforced within the borrow() function.

Tools Used

Manual Code Review
A thorough examination of the contract’s logical checks identified the inverted condition in the borrow() function, revealing that users could exceed the intended liquidation threshold. This manual inspection highlighted the discrepancy between the protocol’s intended LTV enforcement and its actual implementation.

Recommendations

To resolve the inverted check, correct the condition in the borrow() function to ensure the user cannot exceed the intended LTV threshold:

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

Lead Judging Commences

inallhonesty Lead Judge 3 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.