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 V3arrow-up-right

  • 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?