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();
UserData storage user = userData[msg.sender];
if (!user.depositedNFTs[tokenId]) revert NFTNotDeposited();
ReserveLibrary.updateReserveState(reserve, rateData);
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
uint256 collateralValue = getUserCollateralValue(msg.sender);
uint256 nftValue = getNFTPrice(tokenId);
@audit>> if (collateralValue - nftValue < userDebt.percentMul(liquidationThreshold)) {
revert WithdrawalWouldLeaveUserUnderCollateralized();
}
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);
}
* @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
Total collateral value is 1000 USD
Userdebt is 800 USD
the check is flawed hence a user can withdraw more to set the position below the loan value
Health check 80* * collateral / 100 = 800 USD , DEBT 800 USD . HEALTH is 1 e18. *
User should not be able to withdraw here but
User calls to withdraw an nft WORTH 360 USD
FLAWED CHECK checks 1000 - 360 = 640 USD
Against DEBT * 80 /100 = 640 USD
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:
* @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) {
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();
}