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:
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
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).
Call alchemist.liquidate(accountId).
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.
Query IERC20(myt).balanceOf(address(alchemist)) to confirm the 5 MYT fee remains inside the contract with no exit path, demonstrating permanent fund freezing.