58270 sc critical incorrect handling of debt cover in redeem can affect early liquidation and incorrectly sync accounts
Submitted on Oct 31st 2025 at 21:17:05 UTC by @zeroK for Audit Comp | Alchemix V3
Report ID: #58270
Report Type: Smart Contract
Report severity: Critical
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol
Impacts:
Protocol insolvency
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Theft of unclaimed yield
Description
Brief/Intro
When users initiate a redemption claim by invoking the claimRedemption function, the contract first checks whether the Transmuter holds enough MYT tokens to fully pay the staker without requiring additional tokens from AlchemistV3. If the balance is insufficient, it calls the redeem() function with the difference between the requested amount and the transmuter’s current balance, inside redeem(), the first step is invoking _earmark(), which updates lastTransmuterTokenBalance to the current balance. However, this update causes both coverDebt and deltaYield to always be zero in the subsequent lines of the function. This behavior directly impacts the _survivalCumulative and redemptionWeight variables, both of which play a crucial role in the _sync() process when invoked for other users, additionally, because totalDebt does not decrease as expected, this can increase the likelihood of liquidation since the _doLiquidate() function checks whether the current collateralization ratio is below the minimum threshold:
if (alchemistCurrentCollateralization < alchemistMinimumCollateralization)since alchemistCurrentCollateralization heavily depends on totalDebt, an inflated debt value makes liquidation more likely, while collateral and MYT are being correctly transferred, invoking _earmark() too early causes inaccurate accounting of coverDebt, leading to higher debt persistence and potential unnecessary liquidations.
Vulnerability Details
the function claimRedemption implemented as below, it checks if current myt balance in transmuter is enough to pay the claimer, if not, redeem function gets invoke:
then the redeem function invokes _earmark at beginning of execution which in return updates the lastTransmuterTokenBalance :
due to early invoking of _earmark, the coverDebt will always be 0, because current transmuter balance == lastTransmuterTokenBalance, this way the coverDebt will never be added to total redeemedDebtTotal which used to decrease totalDebt which affect liquidation process and it never get used in calculation of _survivalAccumulator and _redemptionWeight which affect users when they invoke _sync() function, we can follow the steps below to understand how the issue occur:
alice claims 1000 alUSD which equal to 1000 MYT token, currently transmuter holds 500 myt which lead to call
redeemwith 500 myt tokens.after redeem get invoked which in returns _earmark invokes directly, this will set the lastTransmuterTokenBalance == myt.balanceOf(transmuter).
liveEarmark equals to 900 tokens.
due to early invoking the _earmark, the deltaYield equal to zero which mean coverDebt = 0 too.
now
coverToApplyDebtequals to zero (since amount always smaller or equal to liveEarmark), the coverToApplyDebt should be equal to 400 tokens because500 + 500 > 900 --> 900 - 500but these 400 tokens as cover will be ignored due to early invocation of _earmark.this will affect the calculation of both
_survivalAccumulatorand_redemptionWeightand the total amount to remove from the totalDebt as shown below:
this will directly affect _sync function behavior and _doLiquidation:
in this case we send out 500 tokens and we decrease the collateral later equal to this amount but the debt decreases by 500 instead of 900 tokens in debt.
Impact Details
Provide a detailed breakdown of possible losses from an exploit, especially if there are funds at risk. This illustrates the severity of the vulnerability, but it also provides the best possible case for you to be paid the correct amount. Make sure the selected impact is within the program’s list of in-scope impacts and matches the impact you selected.
References
https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/Transmuter.sol#L191-L233 https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L1040C1-L1075C23
https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L589-L641
https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L852-L865
Proof of Concept
Proof of Concept
run the test below in alchemistV3.t.sol:
Was this helpful?