58456 sc medium account can enter unliquidatable state with residual debt

Submitted on Nov 2nd 2025 at 13:38:14 UTC by @gizzy for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58456

  • Report Type: Smart Contract

  • Report severity: Medium

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

  • Impacts:

    • Protocol insolvency

Description

Summary

The liquidation flow in AlchemistV3._liquidate() has a critical flaw in its ordering of operations. After _forceRepay() repays earmarked debt and potentially brings an account back to healthy collateralization, the function deducts a repayment fee that can consume ALL remaining collateral. This leaves the account with residual debt but ZERO collateral, creating an unliquidatable state where the account cannot be liquidated again, repaid, or burned.

Additionally, if the user claims their redemption from the Transmuter, totalSyntheticsIssued drops to 0 while the account still has debt, breaking the protocol's core accounting invariant.

Vulnerability Details

Root Cause

In AlchemistV3.sol:798-851, the _liquidate() function follows this sequence:

  1. Line 828: Call _forceRepay() to repay earmarked debt

  2. Line 831-835: If debt is fully cleared (debt == 0), calculate and deduct repayment fee, then return

  3. Line 838-844: Otherwise, recalculate collateralization ratio and check if liquidation is needed

  4. Line 845-850: If ratio is now healthy (> collateralizationLowerBound), calculate and deduct repayment fee, then return

The bug occurs in step 4: The repayment fee is calculated based on the full amount repaid by _forceRepay(), but the collateralization check in step 3 does NOT account for this fee deduction.

Repayment Fee Calculation

From AlchemistV3.sol:909-916:

Key Issue: If fee > account.collateralBalance, the entire remaining collateral is wiped to 0, while still with debt .

Step-by-Step Attack Vector

Initial Setup

  • Alice: Deposits 1,000 MYT shares worth $1,000 USD

  • Bob: Deposits 1,000 MYT shares worth $1,000 USD (provides liquidity, doesn't borrow)

  • Alice: Mints maximum debt = $900 (90% LTV at 110% minimum collateralization)

  • Alice: Creates redemption in Transmuter with all 900 alTokens

Attack Execution

Step 1: Wait for Full Transmutation

  • Time passes: (5,256,000 blocks)

  • Earmarked debt decays to ≈$899 due to rounding

Step 2: Price Crash

  • MYT price drops 9.5%

  • Alice's collateral value: 1,000 shares × $0.905 = $905

  • Alice's debt: $900

  • Alice's earmarked: $899

  • Collateralization ratio: $905 / $900 = 100.5% (below 105% collateralizationLowerBound)

Step 3: Liquidator Calls liquidate(aliceTokenId)

The function executes as follows:

this makes

  1. Cannot liquidate again: liquidate() reverts with LiquidationError() because:

    • collateralInUnderlying = 0

    • collateralizationRatio = 0 / debt = 0%

    • Check passes (0% < 105%), proceeds to repay

    • _forceRepay() tries to repay but account.earmarked = 0, returns 0

    • debt is still > 0, so continues to _doLiquidation()

    • calculateLiquidation() returns liquidationAmount = 0 (no collateral to seize)

    • Function returns (0, 0, 0) which triggers revert at line 556

  2. Cannot burn/repay: If Alice tries to burn alTokens to repay the debt:

  3. Protocol accounting breaks: If Alice claims her Transmuter redemption:

    • Transmuter calls alchemist.redeem(899 alTokens)

    • This burns 900 alTokens from circulation

    • totalSyntheticsIssued -= 900 → becomes 0

    • BUT Alice's account still has debt

    • Broken invariant: totalDebt > totalSyntheticsIssued (debt exists with 0 synthetics in circulation)

Impact

Denial of Service (DOS)

  • Account becomes permanently unliquidatable,cant burn and repay. Protocol Accounting Corruption

  • totalDebt > totalSyntheticsIssued breaks core invariant

  • Functions relying on this invariant may malfunction _earmark() could enter undefined behavior

Bad Debt Accumulation

  • Residual debt becomes uncollectable "ghost debt"

  • Scales across multiple users during market volatility

Likelihood

High - This occurs in common scenarios:

  • Any price drop of 5-15% for high-LTV positions

  • Repayment fee > 1% (default is 3%, making it very likely)

##Reference

https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L818C7-L842C10

Solution: Deduct Repayment Fee BEFORE Health Check

File: src/AlchemistV3.sol:798-851

Modify the _liquidate() function to deduct the repayment fee immediately after _forceRepay(), then perform the health check on the adjusted collateral balance.

Proof of Concept

Proof of Concept

Add this test to src/test/AlchemistV3.t.sol (https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/test/AlchemistV3.t.sol):

Expected Output

Was this helpful?