Core Contracts

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

borrow() allowed even when there isn't enough collateral

Description

borrow() allows borrower to have a debt greater than their collateral allows due to incorrect calculation. It should be something like (See Mitigation section for a better fix):

File: contracts/core/pools/LendingPool/LendingPool.sol
325: function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
326: if (isUnderLiquidation[msg.sender]) revert CannotBorrowUnderLiquidation();
327:
328: UserData storage user = userData[msg.sender];
329:
330: uint256 collateralValue = getUserCollateralValue(msg.sender);
331:
332: if (collateralValue == 0) revert NoCollateral();
333:
334: // Update reserve state before borrowing
335: ReserveLibrary.updateReserveState(reserve, rateData);
336:
337: // Ensure sufficient liquidity is available
338: _ensureLiquidity(amount);
339:
340: // Fetch user's total debt after borrowing
341: uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
342:
343: // Ensure the user has enough collateral to cover the new debt
- 344: if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
+ 344: if (collateralValue.percentMul(liquidationThreshold) < userTotalDebt) {
345: revert NotEnoughCollateralToBorrow();
346: }
347:

Attack Path:

  • Deposit NFT worth 100

  • Borrow an amount of 120

  • Protocol thinks due to faulty logic that 100 < 120 * 80% or 100 < 96 is false so things are okay and does not revert.

  • Attacker now has profit of 20 even after defaulting on the loan.

Impact

User can leave the protocol underwater and extract profit.

Proof of Concept

Modify this existing test and run to see it pass, even though it should have failed with error NotEnoughCollateralToBorrow:

it("should allow user to borrow crvUSD using NFT collateral", async function () {
- const borrowAmount = ethers.parseEther("50");
+ const borrowAmount = ethers.parseEther("120");
await lendingPool.connect(user1).borrow(borrowAmount);
const crvUSDBalance = await crvusd.balanceOf(user1.address);
- expect(crvUSDBalance).to.equal(ethers.parseEther("1050"));
+ expect(crvUSDBalance).to.equal(ethers.parseEther("1120"));
const debtBalance = await debtToken.balanceOf(user1.address);
expect(debtBalance).to.gte(borrowAmount);
+
+ console.log("liquidationThreshold in bips =", await lendingPool.liquidationThreshold(), "\n");
+ console.log("debtBalance =", debtBalance);
+ console.log("collateralValue =", await lendingPool.getUserCollateralValue(user1.address));
});

Output:

LendingPool
Borrow and Repay
liquidationThreshold in bips = 8000n
debtBalance = 120000000000000000000n
collateralValue = 100000000000000000000n
✔ should allow user to borrow crvUSD using NFT collateral
1 passing (8s)

Mitigation

While the fix shown in the Description section works, the protocol should really take into account the health factor while checking borrow eligibility and hence make the following change:

File: contracts/core/pools/LendingPool/LendingPool.sol
325: function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
326: if (isUnderLiquidation[msg.sender]) revert CannotBorrowUnderLiquidation();
327:
328: UserData storage user = userData[msg.sender];
329:
330: uint256 collateralValue = getUserCollateralValue(msg.sender);
331:
332: if (collateralValue == 0) revert NoCollateral();
333:
334: // Update reserve state before borrowing
335: ReserveLibrary.updateReserveState(reserve, rateData);
336:
337: // Ensure sufficient liquidity is available
338: _ensureLiquidity(amount);
339:
340: // Fetch user's total debt after borrowing
341: uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
342:
343: // Ensure the user has enough collateral to cover the new debt
- 344: if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
+ 344: if (calculateHealthFactor(msg.sender) < healthFactorLiquidationThreshold) {
345: revert NotEnoughCollateralToBorrow();
346: }
347:
Updates

Lead Judging Commences

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