58522 sc high earmark consumes excess cover inflating cumulativeearmarked
Submitted on Nov 3rd 2025 at 00:36:13 UTC by @OxPhantom for Audit Comp | Alchemix V3
Report ID: #58522
Report Type: Smart Contract
Report severity: High
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol
Impacts:
Permanent freezing of funds
Protocol insolvency
Description
Brief/Intro
_earmark() advances the transmuter balance pointer (lastTransmuterTokenBalance) to the full current balance every call, regardless of how much of the observed balance delta is actually applied as cover to reduce the earmark amount. This prematurely “consumes” future cover, causing unnecessary growth of cumulativeEarmarked, reducing liveUnearmarked, and throttling subsequent earmarks even when the transmuter still holds sufficient MYT to offset future maturations.
Vulnerability Details
Global earmarking computes the matured amount via Transmuter.queryGraph() and offsets it by a “cover” derived from the increase in transmuter MYT balance since the last earmark. However, _earmark() then updates the pointer lastTransmuterTokenBalance to the entire current transmuter balance instead of only accounting for the portion of cover that was actually applied. Any residual, unused cover from the observed delta is effectively discarded for future _earmark() calls.
Relevant flow in AlchemistV3._earmark():
By contrast, redeem() advances the pointer only by the cover actually consumed (converted back to yield), which preserves any residual cover for later:
Because _earmark() over-advances the pointer, future earmarks do not see the still-available MYT as cover, so amount remains higher than it should be, and cumulativeEarmarked increases unnecessarily. This reduces liveUnearmarked = totalDebt - cumulativeEarmarked, which then clamps future earmarks and distorts weight/survival accounting that depends on these buckets.
Note: Transmuter.claimRedemption() eventually calls setTransmuterTokenBalance() to resync the pointer to the actual transmuter MYT balance after transfers; however, that does not retroactively restore the prematurely “consumed” cover from prior _earmark() calls.
Impact Details
Inflates
cumulativeEarmarked, shrinkingliveUnearmarked = totalDebt - cumulativeEarmarkedand clamping subsequent_earmark()amounts.Skews weighting/decay attribution that relies on correct earmarked vs unearmarked bucket sizes, impacting fairness of redemption distribution.
References
https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L1102C1-L1112
Proof of Concept
Proof of Concept
to run the PoC you can copy paste this code in the IntegrationTest.t.sol test file and run forge test --mt test_transmutterBalance_earmark_PoC
In this PoC, 0xBeef mints 90 alUSD and fully repays his debt. The Transmuter therefore holds 90 yield tokens. Then, 0xDad also mints 90 alUSD and creates a redemption for 45 alUSD. The Transmuter has enough balance, so after the earmark, there is no cumulative earmark — however, the Transmuter’s final balance is set to 90 yield tokens.
After that, 0xDad creates another redemption for 22.5 alUSD. After the earmark, the cumulative earmark is set to 22.5 even though the Transmuter still has enough balance.
Was this helpful?