Core Contracts

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

Incorrect Collateralization Check in LendingPool::Borrow Function Allows 100% LTV

Summary

The borrow function in the LendingPool contract is intended to enforce that users can borrow only up to a safe proportion of their collateral value. However, the current implementation of the collateralization check is flawed. In a scenario where a user has deposited an NFT valued at 100 ETH, the function allows the user to borrow 100 ETH even though the liquidation threshold is 80% (i.e. the maximum safe borrow should be 80 ETH). This means that the check does not properly enforce the intended risk management, compromising the protocol’s solvency.

Vulnerability Details

Repo link:
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/LendingPool/LendingPool.sol#L344

The issue lies in the borrow function's collateralization check:

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// ... other checks ...
uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
// @audit Incorrect check
if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
}
  • Current Check Analysis:
    For example, if a user’s collateralValue is 100 ETH and they try to borrow an amount such that the total debt becomes 100 ETH:

    • userTotalDebt = 100 ETH

    • With a liquidation threshold of 80% (expressed as 8000 basis points), the check evaluates
      collateralValue < userTotalDebt.percentMul(liquidationThreshold), which becomes:
      100 ETH < 100 ETH * 0.8
      100 ETH < 80 ETH

    • As this comparison is false, the function allows the borrow amount.

  • Intended Behavior:
    In a correctly collateralized position, a user with 100 ETH of collateral should only be allowed to borrow 80 ETH. The check should ensure that the total debt does not exceed 80% of the collateral value. In other words, the condition should revert if:
    userTotalDebt > collateralValue.percentMul(liquidationThreshold)
    Because with 100 ETH collateral:
    100 ETH > 100 ETH * 0.8 (which is true), the function should then revert.

POC:

// for this test to be clear, uncomment the
// await crvusd.mint(user1.address, mintAmount); in the setup of the leanding pool
describe("Borrow and Repay", function () {
beforeEach(async function () {
await raacHousePrices.setOracle(owner.address);
// TokenID 1 price is at at 100ETH
await raacHousePrices.setHousePrice(1, ethers.parseEther("100"));
const tokenId = 1;
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
// User1 deposit the NFT to the pool which is 100ETH
await lendingPool.connect(user1).depositNFT(tokenId);
});
it.only("should allow user to borrow crvUSD using NFT collateral", async function () {
const borrowAmount = ethers.parseEther("100");
console.log(
"initial balance",
await crvusd.balanceOf(user1.address),
await rToken.balanceOf(user1.address)
);
// user1 crvusd and rToken token balance is zero since he only deposited TokenID 1 to pool
await lendingPool.connect(user1).borrow(borrowAmount);// <--- successfully borrowed 100% of the deposited collateral value
const crvUSDBalance = await crvusd.balanceOf(user1.address);
console.log(
"user borrowed",
crvUSDBalance,
borrowAmount,
await rToken.balanceOf(user1.address)
);
expect(crvUSDBalance).to.equal(ethers.parseEther("100"));
// user1 was able to borrow more than 80% of his collateral value
});
});

Impact

  • Over-borrowing:
    Users are able to borrow up to 100% of their collateral, far exceeding the intended safe borrow limit of 80%. This results in positions that are massively undercollateralized.

  • Increased Liquidation Risk:
    Positions with 100% LTV are immediately at risk of liquidation, with little or no buffer to absorb price movements, potentially leading to rapid cascades of liquidations.

  • Protocol Solvency Concerns:
    If borrowers take advantage of this flaw, the protocol might accumulate significant undercollateralized debt, risking insolvency and loss of funds.

Tools Used

  • Manual code review

Recommendations

Update the borrow function so that it properly compares the user’s total debt with the maximum allowable debt based on collateral:

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// ... other checks ...
uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
// Correct check
if (userTotalDebt > collateralValue.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month 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.