58757 sc critical forgotten cover in earmark causes systematic over earmarking and temporary freezing of user collateral

Submitted on Nov 4th 2025 at 12:06:48 UTC by @algizsec for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58757

  • Report Type: Smart Contract

  • Report severity: Critical

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

  • Impacts:

    • Temporary freezing of funds for at least 24 hour

Description

Brief/Intro

The _earmark() function in AlchemistV3.sol mishandles how it tracks "cover". When the amount of cover exceeds the required redemptions amount for a given period, the contract advances lastTransmuterTokenBalance to the full balance, effectively "forgetting" the unused portion. This inflates cumulativeEarmarked and _earmarkWeight, leading to systematic over-earmarking of each user’s debt. Subsequently, redeem() calls do not clear this inflated earmarking which leads to 2 issues:

  1. Users might be denied repaying their debt by burning alAssets (due to artifically lower unearmarked debt). They are either:

  • forced to repay using MYT - thus denying them access to their collateral OR

  • redeem their alAssets for MYT which will take timeToTransmute to mature(20 days set in Transmuter.t.sol), which is far more than 24 hours.

  1. Liquidations first clear earmarked debt to try and bring the position into healthy state again. The whole earmarked debt is always cleared. When it is inflated due to the bug above, more debt is cleared through forceRepay than needed, hence a bigger repayment fee is paid by the user (due to the excess earmarked).

Vulnerability Details

The root cause is incorrect consumption of observed Transmuter cover inside the _earmark() - when coverInDebt >= amount, the function sets lastTransmuterTokenBalance = transmuterCurrentBalance, effectively advancing the baseline to the full current balance, instead of just by the used portion only. The leftover cover is dropped and cannot be reused to offset future earmarks or redemptions.

If coverInDebt >= amount, amount becomes 0 and the function should consume only amount worth of the observed transmuterDifference and keep the remaining as future available cover. Instead, the baseline is moved to transmuterCurrentBalance, meaning subsequent calls will see transmuterDifference = 0 until new MYT arrives. The unused portion is therefore "forgotten" from system's perspective.

Because the function treated the whole delta as already consumed, it still performs the earmark bookkeeping (or at least prevents future cover from offsetting earmarks), so cumulativeEarmarked and _earmarkWeight grow relative to what is economically necessary. This creates divergence between real cover and recorded earmarks.

When claimRedemption() is called, it indeed uses the whole Transmuter balance to cover the matured amount and only pulls as much collateral as needed from AlchemistV3. This means that redeem() will not be called with as much amount expected as was marked during earmark(), which will leave cumulativeEarmarked artificially high.

Later:

  1. Users are capped on the amount of alAssets they can burn and are forced to repay using MYT.

  1. Liquidated users get their entire inflated account.earmarked force repayed which might be an overkill and cost the user a bigger repayment fee.

Impact Details

Because cumulativeEarmarked was inflated, users:

  1. Are denied the ability to burn their debt using debt tokens and access their collateral. Instead they need to repay with MYT tokens and user their alAssets to create a redemption in the Transmuter, which will take timeToTransmute time to mature (which will be far longer than 24 hours).

The user may want to access the collateral MYT immediately, but won’t be able to unless they swap the alAssets for MYT on the market (and potentially incur losses on a discount).

  1. Bear higher repayment fees due to inflated earmark that is cleared during forceRepay during liquidation. Note: The protocolFee paid during forceRepay is also higher and will be felt by the user immediately, but it has to be paid regardless at some point when debt is cleared.

References

  1. [_earmark()] (https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L1103-L1112)

  2. [cumulativeEarmarked not cleared] (https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L605-L613)

  3. [burn limit] (https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L470-L472)

  4. [forceRepay] (https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L818-L828)

Proof of Concept

Proof of Concept

The following PoC demonstrates how a large repay in the beginning of maturation of a redemption causes artificall inflation of cumulativeEarmarked for the other user.

  1. Add the following test to AlchemistV3.t.sol

  1. Run test with: forge test --mt test_PoC_Earmark_Ignores_Leftover_Transmuter_Balance -vv

  2. Output:

Was this helpful?