Core Contracts

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

Incorrect check in borrow() leads to under collateralized loans

Summary

Due to an incorrect logic, the borrow function in the LendingPool contract allows users to borrow an amount that exceeds their collateral value and goes beyond the liquidation threshold.

Vulnerability Details

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (isUnderLiquidation[msg.sender]) revert CannotBorrowUnderLiquidation();
UserData storage user = userData[msg.sender];
uint256 collateralValue = getUserCollateralValue(msg.sender);
if (collateralValue == 0) revert NoCollateral();
ReserveLibrary.updateReserveState(reserve, rateData);
_ensureLiquidity(amount);
uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
@> if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
user.scaledDebtBalance += scaledAmount;
reserve.totalUsage = newTotalSupply;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, amount);
_rebalanceLiquidity();
emit Borrow(msg.sender, amount);
}

In the above code, before transferring RTokens to the user, the function checks if the following condition holds:

if (collateralValue < userTotalDebt.percentMul(liquidationThreshold))

If true, it reverts with NotEnoughCollateralToBorrow(). However, this check contains a logic flaw.

Here is an example scenario :

  • User deposits an NFT worth 1 ETH.

  • User attempts to borrow 1.2 ETH.

  • userTotalDebt is calculated as:

    • userTotalDebt = ((user.scaledDebtBalance * 1e27) / 1e27) + 1.2e18 = 0 + 1.2e18 = 1.2e18.

  • Next, the function checks:

    • userTotalDebt * 0.8 = 1.2e18 * 0.8 = 0.96e18.

    • collateralValue = 1e18 (1 ETH), which is greater than 0.96e18.

    • Therefore, the borrow function will not revert.

With this logic the max borrowable amount formula is:

  • maxBorrawableAmount = collateralValue/liquidationThreshold = 1e18 / 0.8 = 1.25e18

  • with 1 ETH we can borrow up to 1.25 ETH

Here is a PoC that proves this scenario, add this test to LendingPool.test.js :

describe("Borrow", function () {
it("should allow user to borrow more than collateral value", async function() {
await crvusd.connect(user1).approve(raacNFT.target, ethers.parseEther("1"));
await crvusd.mint(user1.address, ethers.parseEther("1"));
const nftPrice = ethers.parseEther("1");
const nftId = 2;
await raacHousePrices.setHousePrice(nftId, nftPrice);
await raacNFT.connect(user1).mint(nftId, nftPrice);
await raacNFT.connect(user1).approve(lendingPool.target, nftId);
await lendingPool.connect(user1).depositNFT(nftId);
const borrowAmount = ethers.parseEther("1.25");
await lendingPool.connect(user1).borrow(borrowAmount);
})
})

Impact

Under collateralized loans

Tools Used

Manual review

Recommendations

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

Lead Judging Commences

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