Core Contracts

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

Incorrect collateral check in borrow() and withdrawNFT().

Bug description

When a user tries to borrow assets from the lending pool, the borrow() function checks if a user has enough collateral.

LendingPool.sol#L344-L346

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

However, the check used is incorrect. It applies the liquidation threshold to debt instead of collateral. The liquidation threshold determines the maximum percentage of value that can be borrowed against an asset; therefore, it should be applied to collateral instead.

Consider a scenario where the collateral asset and asset supplied by the lender have a 1:1 exchange rate, the liquidation threshold is set to 80% and the user has provided 1000 collateral assets. Therefore he should be able to borrow 1000 * 80% = 800 assets at most. However with the current check If the user attempts to borrow 1250 assets, the check would evaluate as 1000 < 1250 * 0.8 = false, because 1250 * 0.8 = 1000. Because of that by supplying 1000 assets of collateral, the user can borrow 1250 assets and never repay his loan as he just profited by 250 assets.

The same issue is present in withdrawNFT(), where the user can withdraw an NFT leaving himself undercollateralized.

LendingPool.sol#L302-L304

if (
collateralValue - nftValue <
userDebt.percentMul(liquidationThreshold)
) {
revert WithdrawalWouldLeaveUserUnderCollateralized();
}

The issue becomes even more evident if we look at the calculateHealthFactor() function.

LendingPool.sol#L545-L554

uint256 collateralThreshold = collateralValue.percentMul(
liquidationThreshold
);
return (collateralThreshold * 1e18) / userDebt;

As we can see, the liquidation threshold is applied to collateral instead of debt.

Impact

Users can steal assets by borrowing more than supplied collateral and never repaying their loans.

Proof of Concept

Please add this test to LendingPool.test.js and run it with npx hardhat test --grep "allows borrowing more than it should".

describe("sl1", function () {
it("allows borrowing more than it should", async function () {
await raacHousePrices.setHousePrice(1, ethers.parseEther("1000"));
await ethers.provider.send("evm_mine", []);
// user2 is lender
// there are 1250 assets available for borrow
const depositAmount = ethers.parseEther("1250");
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
// user1 deposits NFT collateral with a value of 1000, he should be able to borrow up to 800
await raacNFT.connect(user1).approve(lendingPool.target, 1);
await lendingPool.connect(user1).depositNFT(1);
let crvBalanceBorrowerBefore = await crvusd.balanceOf(user1);
await lendingPool.connect(user1).borrow(ethers.parseEther("1250"));
let crvBalanceBorrowerAfter = await crvusd.balanceOf(user1);
expect(crvBalanceBorrowerAfter - crvBalanceBorrowerBefore).to.equal(
ethers.parseEther("1250")
);
});
}

Recommended Mitigation

When determining health of the borrower's position, either multiply collateral value by the liquidation threshold or divide debt by the same liquidation threshold.

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.