Summary
Presently a user can borrow more funds than their collateral valuation can support, this allows an attacker to drain the contract balance by depositing collaterals and taking 20% or more of the valuation of their collateral.
Vulnerability Details
A user can borrow by calling the borrow function
* @notice Allows a user to borrow reserve assets using their NFT collateral
* @param amount The amount of reserve assets to borrow
*/
function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (isUnderLiquidation[msg.sender]) revert CannotBorrowUnderLiquidation();
UserData storage user = userData[msg.sender];
uint256 collateralValue = getUserCollateralValue(msg.sender);
if (collateralValue == 0) revert NoCollateral();
ReserveLibrary.updateReserveState(reserve, rateData);
@audit>> _ensureLiquidity(amount);
@audit>>
@audit>> uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
@audit>>
@audit>> if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
user.scaledDebtBalance += scaledAmount;
reserve.totalUsage = newTotalSupply;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, amount);
_rebalanceLiquidity();
emit Borrow(msg.sender, amount);
}
Collateral Value 1000 USD
Debt to open 1250 USD
Health check for this will fail normally 800/1250 = 0.64 e18 less than 1e18
This position should never open
But The check
1000 USD < 1250 * 80 /100
1000 USD < 1000 USD
This check passes with an health factor of 0.64e18. This position is immediately open to liquidation
For a deliberate attack an attacker has made an extra 25% profit from his collateral
This can be used to drain the contract creating multiple bad debt position and selling an NFT over 25% of it actual value.
LOOK at how Aave checks solvency =>
require(vars.userCollateralInBaseCurrency != 0, Errors.COLLATERAL_BALANCE_IS_ZERO);
require(vars.currentLtv != 0, Errors.LTV_VALIDATION_FAILED);
require(
vars.healthFactor > HEALTH_FACTOR_LIQUIDATION_THRESHOLD,
Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD
);
vars.amountInBaseCurrency =
IPriceOracleGetter(params.oracle).getAssetPrice(
vars.eModePriceSource != address(0) ? vars.eModePriceSource : params.asset
) *
params.amount;
unchecked {
vars.amountInBaseCurrency /= vars.assetUnit;
}
@audit>> we use div not mul >>> vars.collateralNeededInBaseCurrency = (vars.userDebtInBaseCurrency + vars.amountInBaseCurrency)
.percentDiv(vars.currentLtv);
@audit>> require(
vars.collateralNeededInBaseCurrency <= vars.userCollateralInBaseCurrency,
Errors.COLLATERAL_CANNOT_COVER_NEW_BORROW
);
Impact
An attacker can drain the asset in the contract by selling their NFT immediately by borrowing for a worth above 25% of the actual market value this causes a big loss for liquidity providers.
Tools Used
Manual Review
Recommendations
Correct the solvency check
++ if (collateralValue < userTotalDebt.percentDiv(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}