57447 sc high untracked myt outflows inflate tvl causing liquidation suppression

Submitted on Oct 26th 2025 at 10:00:39 UTC by @riptide for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57447

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Protocol insolvency

Description

Brief/Intro

The bug causes _mytSharesDeposited to not decrement during MYT outflows in _forceRepay and _doLiquidation, inflating the tracked TVL used in liquidation calculations.

If exploited on mainnet, this could suppress full liquidations of undercollateralized positions, allowing bad debt to persist, increasing insolvency risk, and potentially leading to significant financial losses for the protocol and its users as the discrepancy compounds with repeated liquidations.

Vulnerability Details

_mytSharesDeposited is used as the TVL source for liquidation math:

// src/AlchemistV3.sol
uint256 private _mytSharesDeposited;

function _getTotalUnderlyingValue() internal view returns (uint256 totalUnderlyingValue) {
    uint256 yieldTokenTVLInUnderlying = convertYieldTokensToUnderlying(_mytSharesDeposited);
    totalUnderlyingValue = yieldTokenTVLInUnderlying;
}

It directly feeds alchemistCurrentCollateralization:

Correct paths adjust _mytSharesDeposited on inflow/outflow:

Bug: internal outflows do not decrement _mytSharesDeposited:

Result: contract’s real MYT balance drops while _mytSharesDeposited stays inflated, overstating TVL and the global ratio used by calculateLiquidation, suppressing liquidation severity/paths.

Attack Steps

  1. Create a position: call AlchemistV3.deposit(C, attacker, 0) then AlchemistV3.mint(tokenIdP, D, attacker) to raise totalDebt; _mytSharesDeposited += C.

  2. Create redemption demand: approve debtToken, call Transmuter.createRedemption(R ≤ D) to drive earmarking over blocks.

  3. Wait ≥1 block; trigger liquidation: a bot or attacker calls AlchemistV3.liquidate(tokenIdP).

  4. Inside liquidate, _forceRepay(tokenIdP, account.earmarked) transfers MYT to transmuter and protocolFeeReceiver without decrementing _mytSharesDeposited.

  5. If still below bound, _doLiquidation transfers (amountLiquidated - feeInYield) to transmuter and feeInYield to caller, again without decrementing _mytSharesDeposited.

  6. Repeat step 3–5 across accounts to accumulate drift: actual MYT at address(this) decreases; _mytSharesDeposited remains high; totalDebt often decreases.

  7. Open a large undercollateralized position tokenIdQ and let it fall below collateralizationLowerBound.

  8. When anyone calls AlchemistV3.liquidate(tokenIdQ), the inflated _mytSharesDeposited yields an artificially high alchemistCurrentCollateralization, preventing the full-liquidation branch; only partial liquidation occurs.

Likelihood (high)

Any address can call AlchemistV3.liquidate/batchLiquidate on accounts that naturally become undercollateralized or have earmarked debt; no roles or capital needed. Liquidations occur routinely, are profitable via fees, and repeatedly trigger the buggy outflows.

Preconditions (positions below collateralizationLowerBound, earmarking active) arise in normal operation, especially during volatility. Execution is trivial and repeatable; the drift compounds steadily.

Impact (critical)

Inflated alchemistCurrentCollateralization suppresses the alchemistCurrentCollateralization < globalMinimumCollateralization full-liquidation path and reduces liquidation sizes, allowing severely undercollateralized positions to persist.

The deviation equals the cumulative MYT transferred but not decremented (sum of creditToYield, protocolFeeTotal, amountLiquidated - feeInYield, and feeInYield), enabling large positions to avoid full liquidation and increasing systemic insolvency risk across all accounts.

Mitigation

Implement both: (a) make AlchemistV3._getTotalUnderlyingValue use IERC20(myt).balanceOf(address(this)) (then convert to underlying) instead of _mytSharesDeposited to guarantee liquidation correctness; (b) decrement _mytSharesDeposited on every MYT outflow: in _forceRepay after transfers of protocolFeeTotal and creditToYield; in the repayment-only path of liquidate after paying feeInYield; and in _doLiquidation after transfers of (amountLiquidated - feeInYield) and feeInYield. Keep decrements inside the same conditionals as the transfers and assert _mytSharesDeposited >= amount before subtraction.

Proof of Concept

Proof of Concept

Add to test/AlchemistV3.t.sol

Was this helpful?