58611 sc medium double counting of earmarked debt repayments as cover leads to user funds being stuck and protocol insolvency

Submitted on Nov 3rd 2025 at 15:11:51 UTC by @Tadev for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58611

  • 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

Brief/Intro

The _earmark function allows for earmarking debt for redemption. This function computes a cover, which is the MYT tokens the transmuter accumulated since last earmark. These MYT tokens come from repay calls, or liquidations that internally call _forceRepay. This cover is then used to reduce the graph query amount (debt to earmark):

        uint256 transmuterCurrentBalance = TokenUtils.safeBalanceOf(myt, address(transmuter));
        uint256 transmuterDifference = transmuterCurrentBalance > lastTransmuterTokenBalance ? transmuterCurrentBalance - lastTransmuterTokenBalance : 0;

        uint256 amount = ITransmuter(transmuter).queryGraph(lastEarmarkBlock + 1, block.number);

        // Proper saturating subtract in DEBT units
        uint256 coverInDebt = convertYieldTokensToDebt(transmuterDifference);
        amount = amount > coverInDebt ? amount - coverInDebt : 0;

The problem arises because the _earmark function incorrectly treats MYT tokens sent through repayments as "cover", causing the same repayment to reduce earmarked debt twice in the system's accounting.

This double-counting creates a shortfall in earmarked debt, potentially making redemptions unclaimable and leading to protocol insolvency.

Vulnerability Details

When a user's debt is repaid via repay or _forceRepay, the following occurs:

  • MYT tokens are transferred to the transmuter

  • cumulativeEarmarked is reduced by the repayment amount

  • The user's individual debt is reduced

However, when _earmark() is subsequently called, it treats the balance increase in the transmuter as "cover", reducing the new amount to be earmarked for the current period.

Example Scenario:

  • Initial state: 400 total debt, 40 earmarked debt

  • User repays 40 MYT tokens (≈40 debt)

    • cumulativeEarmarked: 40 → 0

    • Transmuter balance increases by 40 MYT

  • Next earmark should earmark 40 new debt from queryGraph

    • Expected: cumulativeEarmarked increases by 40

    • Actual: cumulativeEarmarked increases by 0 (because 40 MYT is treated as "cover")

    • Result: The same 40 debt was reduced twice in the accounting

Impact Details

The impact of this issue is high as the double-counting creates a systematic shortfall in cumulativeEarmarked.

Redemptions in the transmuter expect collateral that potentially doesn't exist in the earmarked pool.

This vulnerability is quite severe as it leads to protocol insolvency, makes redemptions potentially unclaimable, and compounds over time with each repayment. Users that created redemptions have their debt tokens stuck in the transmuter and cannot claim the redemption.

Proof of Concept

Proof of Concept

Please copy paste the following test in AlchemistV3.t.sol file:

The output of the test is:

This POC highlights the root cause of this issue:

  • a user deposits MYT tokens and mint debt tokens

  • another user creates a redemption to start earmarking debt

  • time passes and first earmark happens, actually earmarking debt

  • the first user repays part of his debt with MYT tokens. This action reduces cumulativeEarmarked and send MYT tokens to the transmuter

  • time passes again and for any other user action, a _earmark call is triggered

  • Here the bug reveals: the new amount to earmark (result of the graph query since last earmark) is reduced by the new transmuter balance, acting as a cover. This is logically wrong, as the new transmuter balance corresponds to the MYT repaid by the user, which already reduced cumulativeEarmarked during repayment.

The result of the POC is that:

  • cumulativeEarmarked is 0

  • 80% of redemption time has passed

  • only the amount corresponding to 40% of redemption time is available in the transmuter.

  • any redeem call during claimRedemption will fail

Not enough is earmarked, and claimRedemption will fail during the redeem call at the line cumulativeEarmarked -= redeemedDebtTotal; . This means user is unable to claim his redemption, which breaks core invariant of the protocol.

Was this helpful?