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 V3
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:
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
timeToTransmuteto mature(20 days set inTransmuter.t.sol), which is far more than 24 hours.
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
forceRepaythan 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:
Users are capped on the amount of
alAssetsthey can burn and are forced torepayusing MYT.
Liquidated users get their entire inflated
account.earmarkedforce repayed which might be an overkill and cost the user a bigger repayment fee.
Impact Details
Because cumulativeEarmarked was inflated, users:
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
timeToTransmutetime 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).
Bear higher repayment fees due to inflated earmark that is cleared during
forceRepayduring liquidation. Note: TheprotocolFeepaid duringforceRepayis 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
[_earmark()] (https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L1103-L1112)
[cumulativeEarmarked not cleared] (https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L605-L613)
[burn limit] (https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L470-L472)
[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.
Add the following test to
AlchemistV3.t.sol
Run test with:
forge test --mt test_PoC_Earmark_Ignores_Leftover_Transmuter_Balance -vvOutput:
Was this helpful?