Smart contract unable to operate due to lack of token funds
Description
Summary
A redundant collateral balance check in the liquidation process prevents liquidators from receiving their rightful fees even the fee is reduced from liquidated user, when a user's collateral balance becomes low or zero after liquidation(liquidatedAmount + fee) reduction. This creates a scenario where liquidators perform work but don't get his fee which is already reduced from liquidated user, potentially disincentivizing necessary liquidations.
Description
The issue occurs in the _doLiquidation() function where there's inconsistent handling of the liquidation fee: let's go to step by step in code i describe complete issue, i write some note on code with @audit tag please it also. the _doLiquidation call calculateLiquidation which return this value (liquidationAmount, debtToBurn, baseFee, outsourcedFFee) the liquidationAmount include base amount to get liqudate + fee that should be transfer to liquidator.
functioncalculateLiquidation(uint256collateral,uint256debt,uint256targetCollateralization,uint256alchemistCurrentCollateralization,uint256alchemistMinimumCollateralization,uint256feeBps)publicpurereturns(uint256grossCollateralToSeize,uint256debtToBurn,uint256fee,uint256outsourcedFee){if (debt >= collateral) { outsourcedFee = (debt * feeBps) /BPS; // fully liquidate debt if debt is greater than collateralreturn (collateral, debt,0, outsourcedFee);}if (alchemistCurrentCollateralization < alchemistMinimumCollateralization) { outsourcedFee = (debt * feeBps) /BPS; // fully liquidate debt in high ltv global environmentreturn (debt, debt,0, outsourcedFee);} // fee is taken from surplus = collateral - debt uint256 surplus = collateral > debt ? collateral - debt :0;@> fee = (surplus * feeBps) /BPS; // collateral remaining for margin‐restore calc uint256 adjCollat = collateral - fee; // compute m*d (both plain units) uint256 md = (targetCollateralization * debt) /FIXED_POINT_SCALAR; // if md <= adjCollat, nothing to liquidateif (md <= adjCollat) {return (0,0, fee,0);} // numerator = md - adjCollat uint256 num = md - adjCollat; // denom = m - 1 => (targetCollateralization - FIXED_POINT_SCALAR)/FIXED_POINT_SCALAR uint256 denom = targetCollateralization -FIXED_POINT_SCALAR; // debtToBurn = (num * FIXED_POINT_SCALAR) / denom debtToBurn = (num *FIXED_POINT_SCALAR) / denom; // gross collateral seize = net + fee // @audit the function return `debtToBurn + fee` as a amount to get liqudate@> grossCollateralToSeize = debtToBurn + fee;}function_doLiquidation(uint256accountId,uint256collateralInUnderlying,uint256repaidAmountInYield)internalreturns(uint256amountLiquidated,uint256feeInYield,uint256feeInUnderlying){ Account storage account = _accounts[accountId]; (uint256 liquidationAmount, uint256 debtToBurn, uint256 baseFee, uint256 outsourcedFee) =calculateLiquidation( collateralInUnderlying,account.debt, minimumCollateralization,normalizeUnderlyingTokensToDebt(_getTotalUnderlyingValue()) *FIXED_POINT_SCALAR/ totalDebt, globalMinimumCollateralization, liquidatorFee ); amountLiquidated =convertDebtTokensToYield(liquidationAmount); feeInYield =convertDebtTokensToYield(baseFee); // update user balance and debtaccount.collateralBalance =account.collateralBalance > amountLiquidated ?account.collateralBalance - amountLiquidated :0;_subDebt(accountId, debtToBurn);TokenUtils.safeTransfer(myt, transmuter, amountLiquidated - feeInYield);// @audit amountLiquidated is the amount that should be reduce from user collateral and get liquidate, feeInYield is the fee amount that// should be transfer to liquidator for liquidation, now the function reduce the amountLiquidated(amount liquidate + fee) from user collateral // then only transfer amountLiquidated - feeInYield to transmuter and sufficient feeInYield amount is left that should be transfer to liquidator, now if after reduction user collateral become 0 below wrong redundent check prevent from transfering liquidation fee to liquidator. // send base fee to liquidator if availableif (feeInYield >0&&account.collateralBalance >= feeInYield) {TokenUtils.safeTransfer(myt,msg.sender, feeInYield);} // Handle outsourced fee from vaultif (outsourcedFee >0) { uint256 vaultBalance =IFeeVault(alchemistFeeVault).totalDeposits();if (vaultBalance >0) { uint256 feeBonus =normalizeDebtTokensToUnderlying(outsourcedFee); feeInUnderlying = vaultBalance > feeBonus ? feeBonus : vaultBalance;IFeeVault(alchemistFeeVault).withdraw(msg.sender, feeInUnderlying);}} emit Liquidated(accountId,msg.sender, amountLiquidated + repaidAmountInYield, feeInYield, feeInUnderlying);return (amountLiquidated + repaidAmountInYield, feeInYield, feeInUnderlying);}
since the calculateLiquidation function return liquidationAmount which include both the liqudate amount + fee of liquidator function correclty reduce the both value as amountLiquidate from user collateral and the the function only transfer the base amount to transmuter TokenUtils.safeTransfer(myt, transmuter, amountLiquidated - feeInYield);. this mean the fee is in the contract and should be transfer to liquidator, but the redundet check if (feeInYield > 0 && account.collateralBalance >= feeInYield) will prevent from transfering fee to liquidator if user collateral become 0 when the amountLiquidated get substract from him. this redundent check will lead to not transfering fee to liquidator and that amount will be stay in contract. liquidator will loss the fee that it should be received.
Scenario Analysis:-
Normal Case (works correctly):
User collateral: 100 MYT
amountLiquidated: 10 MYT (8 principal + 2 fee)
After liquidation: 90 MYT remaining
Transmuter receives: 8 MYT
Liquidator receives: 2 MYT
Problem Case (fee payment fails):
User collateral: 10 MYT
amountLiquidated: 10 MYT (8 principal + 2 fee)
After liquidation: 0 MYT remaining
Transmuter receives: 8 MYT
Liquidator receives: 0 MYT
Impact
Liquidators not receiving their rightful fees for performing liquidations
Financial loss for liquidators who expend gas and effort without compensation
Reduced incentive for liquidators to monitor and liquidate undercollateralized positions
Severity level med:- the reason i choose the med severity level and impact (Smart contract unable to operate due to lack of token funds) because the fee that is reduced and not transfer to liquidator is lack of completing the operation and this is happening because of wrong check and lack of enough extra token amount requiring due to wrong check.