Liquidator base fee can be seized from the borrower’s position but not paid to the liquidator due to an order-of-operations bug in src/AlchemistV3.sol. Specifically, the account’s collateralBalance is reduced by the full liquidation amount (which already includes the base fee) before deciding whether to transfer the fee to the liquidator. The subsequent check then (incorrectly) evaluates the post‑deduction balance, causing the fee transfer to be skipped. The seized fee remains stuck inside the Alchemist contract, liquidators are underpaid relative to the returned feeInYield value, and liquidation incentives degrade in production.
Vulnerability Details
Root cause is in _doLiquidation where the gross seizure and fee are computed, the account is debited by the gross amount, the net is transferred to the transmuter, and only then is the liquidator fee paid subject to a balance check that now uses the reduced collateralBalance.
Below code snippet from in src/AlchemistV3.sol:867-879(https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol#L867-L879):
// amountLiquidated includes the feeamountLiquidated =convertDebtTokensToYield(liquidationAmount);feeInYield =convertDebtTokensToYield(baseFee);// update user balance and debtaccount.collateralBalance = account.collateralBalance > amountLiquidated ? account.collateralBalance - amountLiquidated :0;// src/AlchemistV3.sol:871_subDebt(accountId, debtToBurn);// send liquidation amount - fee to transmuterTokenUtils.safeTransfer(myt, transmuter, amountLiquidated - feeInYield);// send base fee to liquidator if availableif(feeInYield >0&& account.collateralBalance >= feeInYield){// src/AlchemistV3.sol:878 TokenUtils.safeTransfer(myt,msg.sender, feeInYield);}
The bug is that account.collateralBalance is reduced by amountLiquidated (which already includes feeInYield) at src/AlchemistV3.sol:871. Then at src/AlchemistV3.sol:878, the code requires account.collateralBalance >= feeInYield to pay the liquidator. When the liquidation consumes the full remaining collateral (or nearly so), this condition fails because the check looks at the post‑seizure balance, even though the fee portion was included in the seizure amount. As a result, the base fee is withheld and remains inside the Alchemist contract balance rather than being sent to the liquidator.
Consequences reflected in the return values: _doLiquidation returns feeInYield regardless of whether the transfer actually occurred. This leads to a mismatch where callers see a non‑zero feeInYield but receive less than that on‑chain.
Impact Details
Underpayment to liquidators: The liquidator receives less than the returned feeInYield, breaking assumptions for integrators and scripts that rely on return values.
Permanent freezing of tokens: The fee portion is seized from the borrower but not forwarded to the liquidator, accumulating as stranded myt in the Alchemist contract.
Possibility of loss: The stranded amount per liquidation is up to the computed base fee component; with elevated liquidatorFee settings or edge collateralization ranges, this can be material across many liquidations.
Proof of Concept
Proof of Concept
Place the test in src/test directory and you can then run it(from root dir) with the following command: