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 V3
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
cumulativeEarmarkedis reduced by the repayment amountThe 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 → 0Transmuter balance increases by 40 MYT
Next earmark should earmark 40 new debt from queryGraph
Expected:
cumulativeEarmarkedincreases by 40Actual:
cumulativeEarmarkedincreases 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
cumulativeEarmarkedand send MYT tokens to the transmutertime passes again and for any other user action, a
_earmarkcall is triggeredHere 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
cumulativeEarmarkedduring repayment.
The result of the POC is that:
cumulativeEarmarkedis 080% of redemption time has passed
only the amount corresponding to 40% of redemption time is available in the transmuter.
any
redeemcall duringclaimRedemptionwill 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?