Tiny Licorice Loris
Medium
Allowing partial liquidations when all of user's collateral balance will be consumed will create false bad debts.
actualDebtToLiquidate
will not == userReserveDebt
in partial liquidations even when all of user's collateral balance will be consumed in the liquidation
https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L546
https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L551
https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/LiquidationLogic.sol#L580
The root cause of this issue is that whenever vars.totalCollateralInBaseCurrency
== vars.collateralToLiquidateInBaseCurrency
in LiquidationLogic.executeLiquidationCall()
, hasNoCollateralLeft
is set to true
bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency ==
vars.collateralToLiquidateInBaseCurrency;
Now when _burnDebtTokens()
is called and hasNoCollateralLeft
is true, all of user's debt is burnt (userReserveDebt
).
function _burnDebtTokens(
DataTypes.ReserveCache memory debtReserveCache,
DataTypes.ReserveData storage debtReserve,
DataTypes.UserConfigurationMap storage userConfig,
address user,
address debtAsset,
uint256 userReserveDebt,
uint256 actualDebtToLiquidate,
bool hasNoCollateralLeft
) internal {
// Prior v3.1, there were cases where, after liquidation, the `isBorrowing` flag was left on
// even after the user debt was fully repaid, so to avoid this function reverting in the `_burnScaled`
// (see ScaledBalanceTokenBase contract), we check for any debt remaining.
if (userReserveDebt != 0) {
debtReserveCache.nextScaledVariableDebt = IVariableDebtToken(
debtReserveCache.variableDebtTokenAddress
).burn(
user,
hasNoCollateralLeft ? userReserveDebt : actualDebtToLiquidate,//@audit
debtReserveCache.nextVariableBorrowIndex
);
}
The issue lies in how outstandingDebt
is calculated.
uint256 outstandingDebt = userReserveDebt - actualDebtToLiquidate;//@audit-issue partial liquidations can create false bad debts. (happens if partial liquidations is attempted on a user whose whole collateral will be consumed on liquidation.)
if liquidation is a partial one, actualDebtToLiquidate
which is liquidator's specified debt to cover won't == userReserveDebt
. Now although all of user's debt(userReserveDebt
) was burnt and taken care of, outstandingDebt
will !=0 which is wrong.
The wrong and false outstandingDebt
will be added to debtReserve.deficit
Here's a more vivid explanation:
-
Total of Ben's debt(userReserveDebt) is == 1000$
-
Ben's position is liquidatable, so i decide to liquidate 500$ for him.(actualDebtToLiquidate) (Partial liquidation)
-
Now the liquidation will consume all of Ben's collateral, so
hasNoCollateralLeft
is set to true -
when
_burnDebtTokens()
is called, since hasNoCollateralLeft is true, Ben's 1000$ is burnt
if (userReserveDebt != 0) {
debtReserveCache.nextScaledVariableDebt = IVariableDebtToken(
debtReserveCache.variableDebtTokenAddress
).burn(
user,
hasNoCollateralLeft ? userReserveDebt : actualDebtToLiquidate,//@audit
debtReserveCache.nextVariableBorrowIndex
);
- When
outstandingDebt
is calculated, my 500$(actualDebtToLiquidate) is deducted from the Total of Ben's debt(userReserveDebt) which is 1000$.. leavingoutstandingDebt
as 500$ when in reality the whole 1000$ was burnt.
vars.totalCollateralInBaseCurrency
needs to ==vars.collateralToLiquidateInBaseCurrency
inLiquidationLogic.executeLiquidationCall()
, sohasNoCollateralLeft
is set to true
- Liquidation needs to be a partial liquidation.
No response
Allowing partial liquidations when all of user's collateral balance will be consumed will create false bad debts.
This will make Umbrella lose money by spending it on eliminating false bad debts and reserve deficits
No response
whenever liquidations will consume all of user's collateral balance don't allow partial liquidations