when all earmarked debt is redeemed, cumulativeEarmarked = 0.The _earmark function in AlchemistV3 cancels valid transmutation signals from the transmuter when queryGraph returns a value less than or equal to coverInDebt. This causes cumulativeEarmarked to remain zero even when users have legitimately transmuted synthetic debt into redeemable yield. As a result, the redeem function is called with amount = 0, and claimRedemption pays only the transmuter’s local balance (often dust). The user’s position is then deleted, resulting in permanent loss of 99%+ of expected redemption value, a total loss of funds.
The Transmuter claimRedemption function can finalize a user’s position and permanently delete it even when no yield has been earmarked from the Alchemist, resulting in no yield token payout (MYT) and a loss of future yield entitlement.
However, under certain protocol states, cumulativeEarmarked can remain zero, especially periods when earmark conditions are not met. This leads to an empty redeem execution and a dust or zero distributable value.
However, the system still executes:
which erases the user’s redemption position and burns the synthetic tokens, even though they were never truly converted into yield. Position is deleted , irreversible.
Impact Details
User receives little or no yield (MYT) even though some portion of their position or full position is considered transmuted.
## Recommended Mitigation
consider making these changes in claimRedeemption function
// If the contract has a balance of yield tokens from alchemist repayments then we only need to redeem partial or none from Alchemist earmarked
uint256 debtValue = alchemist.convertYieldTokensToDebt(yieldTokenBalance);
uint256 amountToRedeem = scaledTransmuted > debtValue ? scaledTransmuted - debtValue : 0;
if (amountToRedeem > 0) alchemist.redeem(amountToRedeem);
uint256 totalYield = alchemist.convertDebtTokensToYield(scaledTransmuted);
// Cap to what we actually hold now (handles redeem() rounding shortfalls).
uint256 balAfterRedeem = TokenUtils.safeBalanceOf(alchemist.myt(), address(this));
uint256 distributable = totalYield <= balAfterRedeem ? totalYield : balAfterRedeem; //this is the main issue
function redeem(uint256 amount) external onlyTransmuter {
_earmark();
uint256 liveEarmarked = cumulativeEarmarked;
if (amount > liveEarmarked) amount = liveEarmarked;