57534 sc low small debt positions cannot be liquidated due to zero amount checks on token vaults

Submitted on Oct 27th 2025 at 01:34:44 UTC by @X0sauce for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57534

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Protocol insolvency

Description

Vulnerability Details

We notice that during liquidations of underwater CDP positions, an outsourced fee would be calculated and send to the user.

    function calculateLiquidation(
        uint256 collateral,
        uint256 debt,
        uint256 targetCollateralization,
        uint256 alchemistCurrentCollateralization,
        uint256 alchemistMinimumCollateralization,
        uint256 feeBps
    ) public pure returns (uint256 grossCollateralToSeize, uint256 debtToBurn, uint256 fee, uint256 outsourcedFee) {
        if (debt >= collateral) {
@>            outsourcedFee = (debt * feeBps) / BPS;
            // fully liquidate debt if debt is greater than collateral
            return (collateral, debt, 0, outsourcedFee);
        }

        if (alchemistCurrentCollateralization < alchemistMinimumCollateralization) {
@>            outsourcedFee = (debt * feeBps) / BPS;
            // fully liquidate debt in high ltv global environment
            return (debt, debt, 0, outsourcedFee);
        }

        // fee is taken from surplus = collateral - debt
        uint256 surplus = collateral > debt ? collateral - debt : 0;

        fee = (surplus * feeBps) / BPS;

        // collateral remaining for margin‐restore calc
        uint256 adjCollat = collateral - fee;

        // compute m*d  (both plain units)
        uint256 md = (targetCollateralization * debt) / FIXED_POINT_SCALAR;

        // if md <= adjCollat, nothing to liquidate
        if (md <= adjCollat) {
            return (0, 0, fee, 0);
        }

        // numerator = md - adjCollat
        uint256 num = md - adjCollat;

        // denom = m - 1  =>  (targetCollateralization - FIXED_POINT_SCALAR)/FIXED_POINT_SCALAR
        uint256 denom = targetCollateralization - FIXED_POINT_SCALAR;

        // debtToBurn = (num * FIXED_POINT_SCALAR) / denom
        debtToBurn = (num * FIXED_POINT_SCALAR) / denom;

        // gross collateral seize = net + fee
        grossCollateralToSeize = debtToBurn + fee;
    }

However, in the case of liquidating ALUSD debt positions, the underlying conversion factor would be 10 ** (18 - 6) = 1e12.

A user can repeatedly open up small debt positions and when those positions go underwater, the liquidation flow will consistently revert because outsourcedFee will be truncated to zero. This is because in both AlchemistTokenVault and AlchemistETHVault, both vaults include checks that would revert on 0 amounts during withdrawals to cover outsourcedFee.

This results in protocol incurring all bad debt because cannot liquidate those positions which results in protocol insolvency

In AlchemistTokenVault

In AlchemistETHVault

Proof of Concept

  1. Create a new file AlchemistV3ZeroAmountRevert.t.sol and paste the below test

  2. Run

You'll see the below output

Was this helpful?