For the complete documentation index, see llms.txt. This page is also available as Markdown.

58471 sc high accounting error in forcerepay doliquidation overstates tvl enabling under scaled redemptions and potential insolvency

Submitted on Nov 2nd 2025 at 14:40:22 UTC by @winnerz for Audit Comp | Alchemix V3

  • Report ID: #58471

  • 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

When _forceRepay and liquidation _doLiquidation transfer MYT out of the Alchemist, the contract does not decrement _mytSharesDeposited. Since getTotalUnderlyingValue() derives TVL from _mytSharesDeposited, reported TVL remains inflated while actual holdings drop. This depresses badDebtRatio used by the Transmuter and can delay or under‑apply scaling of redemptions, creating a path to protocol insolvency. It can also soften liquidation sizing by overstating global collateralization inputs.

Vulnerability Details

TVL is derived from _mytSharesDeposited:

// src/AlchemistV3.sol: 1238-1241
    function _getTotalUnderlyingValue() internal view returns (uint256 totalUnderlyingValue) {
        uint256 yieldTokenTVLInUnderlying = convertYieldTokensToUnderlying(_mytSharesDeposited);
        totalUnderlyingValue = yieldTokenTVLInUnderlying;
    }

In _forceRepay, MYT is transferred out but _mytSharesDeposited is not decremented, so reported TVL does not change:

Similarly, in _doLiquidation, MYT leaves the contract without decreasing _mytSharesDeposited:

By contrast, other outflow paths do reduce _mytSharesDeposited:

Transmuter claimRedemption() relies on the Alchemist TVL in its denominator; an inflated TVL depresses this ratio and delays scaling:

flow-of-funds

  • _mytSharesDeposited intent (state):

  • Why repay() only decrements fees: the principal is transferred from the payer directly to the Transmuter (Alchemist never increases its MYT), so only the fee portion is an Alchemist outflow that must reduce _mytSharesDeposited.

  • In _forceRepay and _doLiquidation, the principal leaves from the Alchemist balance itself, so _mytSharesDeposited must be decreased by what is sent out (plus any fee outflows) to keep TVL honest.

Effect on the Transmuter ratio

  • Let den_used = TVL_used + transmuterUnderlying and den_true = TVL_true + transmuterUnderlying.

  • Since TVL_used > TVL_true after drift, den_used > den_true which implies usedR = totalSyntheticsIssued / den_used < trueR = totalSyntheticsIssued / den_true.

  • Scaling in claimRedemption() uses min(1, 1/ratio). If trueR > 1 but usedR <= 1, the claim is paid unscaled when it should be scaled. If both exceed 1, the paid amount under the used ratio is larger than under the true ratio by a factor trueR/usedR > 1.

The drift persists until a path that decrements _mytSharesDeposited runs (withdraw or redeem or the fee leg of repay). Multiple _forceRepay/liquidation events can accumulate drift across blocks, affecting all subsequent redemptions and liquidation decisions protocol-wide.

Why this matters:

  • getTotalUnderlyingValue() uses _mytSharesDeposited to compute TVL. Omitting decrements during _forceRepay/liquidation keeps reported TVL unchanged even though MYT actually left the protocol.

  • Transmuter claimRedemption() computes a badDebtRatio using getTotalUnderlyingValue(). Inflated TVL depresses this ratio (usedR < trueR), delaying or under‑applying scaling when bad debt exists. This can systematically overpay claims and lead to insolvency.

  • Liquidation sizing (calculateLiquidation) that takes global collateralization as an input can be influenced to under‑liquidate due to the overstated TVL.

Impact Details

  • Impact: Protocol insolvency

  • Rationale:

    • Inflated TVL depresses badDebtRatio and reduces/delays redemption scaling, overpaying claimants when bad debt exists.

    • Over time, this can drain reserves and result in protocol insolvency.

    • Secondary: Under-liquidation due to overstated global collateralization inputs.

References

  • _forceRepay MYT outflow without _mytSharesDeposited decrement: https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L738

  • _doLiquidation MYT outflow without _mytSharesDeposited decrement: https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L867

  • withdraw() correct decrement: https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L410

  • redeem() correct decrement: https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L638

  • repay() fee decrement (principal comes from payer, not Alchemist): https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L541

Mitigation

  • In _forceRepay, decrement _mytSharesDeposited by the MYT actually sent out:

    • Subtract creditToYield and protocolFeeTotal when they are transferred to Transmuter and protocolFeeReceiver respectively.

  • In _doLiquidation, decrement _mytSharesDeposited by the full amountLiquidated (the gross MYT outflow), i.e., the portion sent to Transmuter and the fee to the liquidator.

Proof of Concept

Proof of Concept

Set-up

  • Foundry; solc 0.8.28; EVM cancun. No RPC/fork required.

Command

Work-flow

PoC 1 - TVL overstatement after _forceRepay/liquidation

  • Create a position: deposit 20,000 MYT shares and mint 12,000 debt.

  • Create a matured redemption (1,000 debt) and poke() the position to earmark.

  • Tighten collateralization bounds to 2.0 so liquidation executes.

  • Record reportedBefore = getTotalUnderlyingValue() and trueBefore = convert(MYT balanceOf(Alchemist)) in debt units.

  • Call liquidate(tokenId); record Transmuter MYT balance delta and yPaid.

  • Record reportedAfter and trueAfter.

  • Assertions: reportedAfter == reportedBefore, trueAfter < trueBefore, drift = reportedAfter - trueAfter > 0.

PoC 2 - Depressed ratio delays scaling in claimRedemption()

  • Set Transmuter fee to 0 for simple accounting.

  • Induce drift as in PoC 1 (create position, earmark, liquidate).

  • Compute two denominators for the ratio: den_used = getTotalUnderlyingValue() + convert(transmuterShares) and den_true = trueTVL + convert(transmuterShares).

  • Compute usedR = totalSyntheticsIssued / den_used and trueR = totalSyntheticsIssued / den_true; verify usedR < trueR.

  • Create a small matured redemption (500 debt) and ensure Transmuter already holds enough MYT to pay it (no redeem() call).

  • Claim and measure debtClaimed from the user’s MYT delta converted back to debt units.

  • Expected in healthy conditions: expectedDebtTrue = 500 (no scaling). Assert debtClaimed == expectedDebtTrue while usedR < trueR, showing scaling activation is delayed by the inflated denominator.

Supporting observation - under‑liquidation at the decision boundary

  • After drift, compute global ratios usedGlobalM (with getTotalUnderlyingValue()) and trueGlobalM (with actual MYT balance).

  • Set globalMinimumCollateralization to a midpoint between them.

  • Call calculateLiquidation(...) twice, once with usedGlobalM and once with trueGlobalM.

  • Expect debtBurnUsed < accountDebt while debtBurnTrue == accountDebt, demonstrating that inflated TVL can reduce liquidation severity.

Output

PoC 1: TVL overstatement after _forceRepay/liquidation

  • Asserts reported TVL (via _mytSharesDeposited) stays constant while true MYT holdings decrease; prints a positive “tvl drift”.

PoC 2: Depressed ratio delays scaling in claimRedemption()

  • Shows usedR < trueR after drift. A small claim remains unscaled, evidencing that the inflated denominator depresses the ratio and delays scaling activation.

Supporting observation

  • Demonstrates that liquidation sizing based on an inflated global collateralization can under‑liquidate compared to the “true” input computed from actual MYT holdings.

Was this helpful?