58168 sc medium safe position liquidation vulnerability in alchemistv3 when minimumcollateralization equals collateralizationlowerbound

Submitted on Oct 31st 2025 at 04:54:26 UTC by @unique for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58168

  • Report Type: Smart Contract

  • Report severity: Medium

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

  • Impacts:

    • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Summary:

A critical vulnerability exists in the AlchemistV3 liquidation mechanism that allows attackers to liquidate positions that should be considered safe (at minimum collateralization ratio) and steal collateral as fees. The root cause is that the protocol permits collateralizationLowerBound to be set equal to minimumCollateralization, combined with flawed fee calculation logic that reduces collateral before checking liquidation eligibility. This breaks fundamental safety guarantees of the lending protocol and enables theft of user funds from properly collateralized positions.

Description:

A critical vulnerability exist in the AlchemistV3 contract liquidation function where attacker can liquidate save position(their collateralization ratio is equal to min-collateralization ratio) descrease their collateral and steal it as a fee from user collateral balance, the main issue arise from that contract allow that minimumCollateralization is be equal to collateralizationLowerBound. Code part: i add audit tag comment and describe the code.

function initialize(AlchemistInitializationParams memory params) external initializer {
        _checkArgument(params.protocolFee <= BPS);
        _checkArgument(params.liquidatorFee <= BPS);
        _checkArgument(params.repaymentFee <= BPS);

        debtToken = params.debtToken;
        underlyingToken = params.underlyingToken;
        underlyingConversionFactor = 10 ** (TokenUtils.expectDecimals(params.debtToken) - TokenUtils.expectDecimals(params.underlyingToken));
        depositCap = params.depositCap;

        // @audit no validation here done to check that that minimumCollateralization and collateralizationLowerBound shouldn't be equal.

        minimumCollateralization = params.minimumCollateralization;
        globalMinimumCollateralization = params.globalMinimumCollateralization;
        collateralizationLowerBound = params.collateralizationLowerBound;
        admin = params.admin;
        transmuter = params.transmuter;
        protocolFee = params.protocolFee;
        protocolFeeReceiver = params.protocolFeeReceiver;
        liquidatorFee = params.liquidatorFee;
        repaymentFee = params.repaymentFee;
        lastEarmarkBlock = block.number;
        lastRedemptionBlock = block.number;
        myt = params.myt;
    }

    /// @inheritdoc IAlchemistV3AdminActions
    function setMinimumCollateralization(uint256 value) external onlyAdmin {
        _checkArgument(value >= FIXED_POINT_SCALAR);
        minimumCollateralization = value;

        emit MinimumCollateralizationUpdated(value);
    }

    // @audit here we can see that if new value of collateralizationLowerBound is get = to minimumCollateralization is ok and set.

    /// @inheritdoc IAlchemistV3AdminActions
    function setCollateralizationLowerBound(uint256 value) external onlyAdmin {
        _checkArgument(value <= minimumCollateralization);
        _checkArgument(value >= FIXED_POINT_SCALAR);
        collateralizationLowerBound = value;
        emit CollateralizationLowerBoundUpdated(value);
    }

As per protocol rules if user collateralization ratio is = to minimumCollateralization it's save and shouldn't be liquidate look at mint function minting logic.

the main problem will happen in the _liquidate function when the collaterlization ratio of user get = to collateralizationLowerBound the function call actual _doLiquidation function and get liquidate user, since above i describe that contract logic allow the admin to set the minimumCollateralization equal with collateralizationLowerBound. I add comment in the below function code.

Then _doLiquidation function call calculateLiquidation to calculate the fee and collateral amount that get liquidate.

This function is the main point that lead to the vulnerability the function first decrease fee from collateral then check the collaterlization ratio of user position with his debt minimumCollateralization ratio, if user position collaterlizationRatio = minimumCollateralizatioRatio the user in save state, since the above liquidation function allow liquidator to liquidate save user position and this function wrong logic that decrease fee first from user collateral and then check the collateralization ratio lead to liquidation of save user position and still of myt token from user collateral as fee.

I add comment in the code please read it.

after wrong calculateLiquidation function user will get liquidate and loss his collateral as fee to liquidator even he is not in liquidation state.

Impact

  • user will get liquidate even they are in save state and not under collateralization ratio

  • attacker/malicious user can liquidate all those user who's collateralization position ratio is = to minimumCollateralizationRation when the protocol set the minimumCollateralization = to collateralizationLowerBound

Mitigation step

The collateralizationLowerBound should never be get = to minimumCollateralization, otherwise save position will get liquidate and liquidator will steal their myt as fee.

Proof of Concept

Proof of Concept

Add the following test function into AlchemistV3.t.sol file and run the test. forge test --mt testLiquidateSavePosition -vv

Was this helpful?