Core Contracts

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

User can withdraw NFT being used as collateral, leaving them position insolvent and protocol inherits a loss via bad debt

Summary

In LendingPool::withdrawNFT-> a user is able to withdraw their nft even if the withdraw would leave their account not only liquidatable but worse, insolvent.

A user is able to deposit their NFT and use the NFT as collateral, and with that collateral, borrow from the protocol.

The protocol imposes a limit on the amount the user can borrow by implementing liquidityThreshold-> which is 80%.

If a user is trying to withdraw their NFT, they should only be able to do so if the amount of their collateral after withdrawing the NFT will not leave their total collateral beneath the liquidation threshold -> but that is not implemented correctly and a user can withdraw their NFT even if it leaves them under the liquidation threshold.

Vulnerability Details

The problem is that the liquidationThresholdis applied to the users total debt, instead of their collateral value. This means that the amount of debt they have is multiplied by 80%, which results in a smaller value. Using this smaller value, the wrong check allows the user to withdraw their nft even when they shouldnt be able to.

Lets assume:

  • User has 2 NFT's as collateral

  • Nft 1 = 150 ; NFT 2 = 50

  • User has 160 total debt -> the liquidity threshold is 80% , so the user is able to borrow up to 80% of their 200 in collateral, which is 160

  • User initiates withdrawNFT-> withdrawing NFT 2 which is worth 50

The function operates as follows:

  • collateralValue. - nftValue. = 150

  • userDebt. * liquidationThreshold. ->. 160 * * 80%. = 128

  • The ifstatement reverts only if 150 < 128

  • The function does not revert, and the user withdraws their NFT

  • This leaves the user total collateral equal to 150

  • total debt = 160

The user is now not only above the liquidation threshold and liquidatable, but they are also insolvent by 10. This is bad debt for the protocol and an immediate loss the protocol has to pay.

function withdrawNFT(uint256 tokenId) external nonReentrant whenNotPaused {
uint256 collateralValue = getUserCollateralValue(msg.sender);
uint256 nftValue = getNFTPrice(tokenId);
if (collateralValue - nftValue < userDebt.percentMul(liquidationThreshold)) {
revert WithdrawalWouldLeaveUserUnderCollateralized();
}

The protocol correctly applies liquidationThresholdto the collateralValuehere in this function which calculates a users health factor. It then compares the collateral value AFTER the liquidationThresholdhas been applied to the collateral, against the total debt a user has. This is how the liquidationThresholdshould be applied, to the collateral.

function calculateHealthFactor(address userAddress) public view returns (uint256) {
uint256 collateralValue = getUserCollateralValue(userAddress);
uint256 userDebt = getUserDebt(userAddress);
if (userDebt < 1) return type(uint256).max;
uint256 collateralThreshold = collateralValue.percentMul(liquidationThreshold);

Impact

A user is able to deposit NFT's, borrow up to the maximum amount that the liquidationThresholdallows them. Then withdraw their nft, leaving their position insolvent.

The user leaves with a profit (in the above scenario, the user ends up with 210 in value, when initially starting with 200).

The protocol endures financial loss due to the bad debt.

The issue gets more severe as the values of NFT scale up.

Tools Used

Manual Review

Recommendations

In withdrawNFTapply the liquidationThresholdto the users total collateralValue.

IF that was done correctly, the ifstatement would have checked the following:

  • 150 * 80% = 120. (collateral - nft)

  • 160 (total debt)

  • if 120 < 160 -> revert

The function would revert and the user would not be able to withdraw their nft.

Updates

Lead Judging Commences

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