For the complete documentation index, see llms.txt. This page is also available as Markdown.

58274 sc high liquidation fee logic in doliquidation strands liquidator rewards when balance is exhausted freezing funds

Submitted on Oct 31st 2025 at 22:00:27 UTC by @Codexstar for Audit Comp | Alchemix V3

  • Report ID: #58274

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Permanent freezing of funds

Description

Brief / Intro

AlchemistV3._doLiquidation subtracts the entire liquidation amount—including the liquidator fee—from a borrower’s collateral balance before attempting to transfer the fee. If the seizure zeroes out the balance, the subsequent >= feeInYield check fails and the fee is never paid out. Every full-collateral liquidation therefore strands the liquidator reward inside the contract, freezing protocol funds and eroding liquidation incentives.

Vulnerability Details

  • calculateLiquidation returns grossCollateralToSeize = debtToBurn + fee (see src/AlchemistV3.sol:1244-1290). _doLiquidation converts this to yield tokens as amountLiquidated, while feeInYield is calculated from the same baseFee (src/AlchemistV3.sol:858-868).

  • The borrower’s collateral balance is immediately reduced by the full amountLiquidated:

    account.collateralBalance = account.collateralBalance > amountLiquidated
        ? account.collateralBalance - amountLiquidated
        : 0;

    src/AlchemistV3.sol:867-872

  • Only after the balance update does the contract try to pay the liquidator:

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

    src/AlchemistV3.sol:877-879

  • Because amountLiquidated already includes the fee, any liquidation that seizes the full collateral sets the balance to zero, causing account.collateralBalance >= feeInYield to fail. The fee remains trapped inside the Alchemist contract. No other code path compensates the liquidator or removes the frozen funds.

Impact Details

  • Liquidators lose their fee in common scenarios (e.g., badly undercollateralized accounts). The stuck reward removes the incentive to liquidate, threatening protocol solvency.

  • The trapped MYT is permanently frozen—there is no redemption mechanism once the transfer fails.

  • Example: collateral balance 100 MYT, amountLiquidated = 100, feeInYield = 5. After the balance is zeroed, the fee transfer condition fails, and 5 MYT remains stranded in the contract.

References

  • Balance update that consumes the entire amountLiquidated: src/AlchemistV3.sol:867-872

  • Fee transfer guarded by depleted balance check: src/AlchemistV3.sol:877-879

  • Fee included in grossCollateralToSeize: src/AlchemistV3.sol:1244-1290

Proof of Concept

Proof of Concept

  1. Open a borrower position with 100 MYT collateral and enough debt for liquidation to seize the full balance (e.g., collateralization well below the lower bound with a 5% liquidator fee).

  2. Call alchemist.liquidate(accountId).

  3. Observe post-call state:

    • account.collateralBalance is set to 0 because amountLiquidated equals the full collateral.

    • The transmuter receives amountLiquidated - feeInYield (e.g., 95 MYT).

    • The liquidator receives nothing; account.collateralBalance >= feeInYield is false.

  4. Query IERC20(myt).balanceOf(address(alchemist)) to confirm the 5 MYT fee remains inside the contract with no exit path, demonstrating permanent fund freezing.

Runnable Foundry Test

Run with:

Was this helpful?