Core Contracts

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

Withdrawal can leave a user's position under collaterized

Summary

When a user calls to withdraw his nfttoken the position check to prevent undercollateralization is flawed and a user can withdraw at least 36% more above the solvency level and create a 20 % bad debt for the contract.

Vulnerability Details

A user can withdraw and send the contract into a bad debt also A user can front run the call to initiate liquidation when they do not have any means of repaying their loan and presently they can withdraw an NFT worth about 36% in value leaving the bad debt for the contract.

*/
function withdrawNFT(uint256 tokenId) external nonReentrant whenNotPaused {
if (isUnderLiquidation[msg.sender]) revert CannotWithdrawUnderLiquidation(); //note
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); // ensure withdrawal doesn't leave us insolvent
uint256 collateralValue = getUserCollateralValue(msg.sender);
uint256 nftValue = getNFTPrice(tokenId);
@audit>> if (collateralValue - nftValue < userDebt.percentMul(liquidationThreshold)) { // BUG the f, scaled balance not debt lol. e.g i have 1000 usdc debt 900 i can withdraw 200 dollars my balnce is now 800< 720 still pass but the account is acctually now liquidatable?? medium
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); // which one is using safetransferfrom for contract bayi abi. it will go sha na nft
emit NFTWithdrawn(msg.sender, tokenId);
}
/**
* @notice Calculates the user's health factor
* @param userAddress The address of the user
* @return The health factor (in RAY)
*/
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);
return (collateralThreshold * 1e18) / userDebt;
}

E.g

  1. Total collateral value is 1000 USD

  2. Userdebt is 800 USD

  3. the check is flawed hence a user can withdraw more to set the position below the loan value

  4. Health check 80* * collateral / 100 = 800 USD , DEBT 800 USD . HEALTH is 1 e18. *

  5. User should not be able to withdraw here but

  6. User calls to withdraw an nft WORTH 360 USD

  7. FLAWED CHECK checks 1000 - 360 = 640 USD

  8. Against DEBT * 80 /100 = 640 USD

  9. The check if 640 < 640 passes and a bad debt of 800 - 640 is created = 160 USD

/**
* @notice Executes a percentage multiplication
* @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328
* @param value The value of which the percentage needs to be calculated
* @param percentage The percentage of the value to be calculated
* @return result value percentmul percentage
*/
function percentMul(uint256 value, uint256 percentage) internal pure returns (uint256 result) {
// to avoid overflow, value <= (type(uint256).max - HALF_PERCENTAGE_FACTOR) / percentage
assembly {
if iszero(
or(
iszero(percentage),
iszero(gt(value, div(sub(not(0), HALF_PERCENTAGE_FACTOR), percentage)))
)
) {
revert(0, 0)
}
result := div(add(mul(value, percentage), HALF_PERCENTAGE_FACTOR), PERCENTAGE_FACTOR)
}
}

Impact

Users can withdraw their NFT and leave debt debts in the protocol. Apart from the attack path

Tools Used

Manual review

Recommendations

As done by Aave use PercentDiv not PercentMul

++ if (collateralValue - nftValue < userDebt.percentDiv(liquidationThreshold)) {
revert WithdrawalWouldLeaveUserUnderCollateralized();
}
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.