57692 sc high alchemistv3 liquidation fee loss vulnerability

Submitted on Oct 28th 2025 at 08:26:49 UTC by @legion for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57692

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol

  • Impacts:

    • Contract fails to deliver promised returns, but doesn't lose value

Description

Brief/Intro

liquidator incentive failure: The _doLiquidation function checks whether to pay the liquidator fee against the victim's remaining collateral balance after seizure, rather than validating against the seized amount. When a liquidation nearly depletes the victim's position, the fee which was already included in the seized collateral is never transferred to the liquidator, remaining trapped in the Alchemist contract.

Vulnerability Details

Liquidator fee withheld when collateral depleted

  • Summary: In _doLiquidation (lines 867-895), the liquidation fee payment logic contains a critical flaw:

    1. Line 867-868: amountLiquidated and feeInYield are calculated from calculateLiquidation, where grossCollateralToSeize = debtToBurn + fee (line 1292)

    2. Line 871: The victim's collateral balance is reduced by the entire seized amount:

      account.collateralBalance = account.collateralBalance > amountLiquidated 
          ? account.collateralBalance - amountLiquidated 
          : 0;
    3. Line 877: The fee transfer is conditioned on the post-seizure remaining balance:

      if (feeInYield > 0 && account.collateralBalance >= feeInYield) {
          TokenUtils.safeTransfer(myt, msg.sender, feeInYield);
      }

    The bug: The check uses account.collateralBalance after deducting the full seizure (which already includes the fee). In scenarios where liquidation consumes most/all of the victim's collateral, the remaining balance falls below feeInYield, causing the condition to fail. The fee was already seized from the user and is held by the Alchemist contract (line 874 only sends amountLiquidated - feeInYield to the transmuter), but it's never transferred to the liquidator.

Impact Details

Liquidators receive no compensation despite performing economically rational liquidations, creating a systemic disincentive that leaves under-collateralized positions unaddressed. Accumulated trapped fees represent protocol insolvency, and the lack of liquidation during volatile periods can cascade into bad debt.

Recommended: The math in calculateLiquidation guarantees that grossCollateralToSeize = debtToBurn + fee (line 1292), and amountLiquidated is derived directly from grossCollateralToSeize. The seized collateral is in the contract's custody, so the fee is always payable. The balance check adds no safety and only introduces this bug.

References

-src/AlchemistV3.sol#L852-L895 — _doLiquidation function containing the liquidation fee payment vulnerability.

-src/AlchemistV3.sol#L867-L868 — Calculation of amountLiquidated and feeInYield from calculateLiquidation results.

-src/AlchemistV3.sol#L871 — Victim's collateral balance reduced by the entire seized amount (including fee) -src/AlchemistV3.sol#L874 — Transfer of seized collateral minus fee to the transmuter.

-src/AlchemistV3.sol#L877 — Buggy fee payment check using victim's remaining collateral balance after seizure.

-src/AlchemistV3.sol#L1244-L1295 — calculateLiquidation function that computes grossCollateralToSeize including the fee.

-src/AlchemistV3.sol#L1290 — grossCollateralToSeize = debtToBurn + fee calculation ensuring fee is part of seized amount

Proof of Concept

Proof of Concept

Was this helpful?