57625 sc low incorrect cover accounting in earmark leads to earmarking failure and value leakage
Submitted on Oct 27th 2025 at 17:14:03 UTC by @fullstop for Audit Comp | Alchemix V3
Report ID: #57625
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
The _earmark function within the AlchemistV3.sol contract contains a critical logic flaw in how it accounts for "cover" – surplus yield tokens (myt) sent to the Transmuter via user repay actions. This flaw causes the entire cover amount to be consumed conceptually in a single _earmark call, even if only a fraction (or none) is needed to offset the debt being earmarked in that call. Consequently, the remaining cover value is permanently wasted and cannot offset future earmarking, hindering the protocol's core debt repayment mechanism. This issue is triggered during normal operations whenever an action like redeem or poke calls _earmark after a repay has occurred.
Vulnerability Details
The core of the issue lies in the _earmark function's handling of the transmuterDifference and the subsequent update of lastTransmuterTokenBalance.
Calculating Cover: The function correctly calculates transmuterDifference, the increase in the Transmuter's myt balance since the last check, representing the available "cover". It converts this difference into its equivalent debt value, coverInDebt.
Applying Cover Incorrectly: It subtracts this coverInDebt from the amount of debt scheduled to be earmarked in the current block range (obtained from ITransmuter(transmuter).queryGraph). If coverInDebt is greater than or equal to amount, the effective amount becomes 0.
Skipping Earmark: If amount becomes 0, the crucial logic block responsible for increasing cumulativeEarmarked and updating weights (_survivalAccumulator, _earmarkWeight) is skipped entirely.
Wasting Cover Value (The Flaw): Regardless of whether amount became 0 or how much coverInDebt was actually needed to offset amount, the lastTransmuterTokenBalance is unconditionally updated to the transmuterCurrentBalance before the potential earmarking logic.
This means that in the next call to _earmark, transmuterDifference will be calculated based on this new, higher lastTransmuterTokenBalance. Any cover value that wasn't actually used to offset amount in the current call is effectively erased from the system's accounting and cannot be used to offset future earmarking amounts.
Impact Details
The primary impact is the permanent loss of "cover" value provided by users through the repay function, leading to a failure of the earmarking mechanism.
Value Leakage: When a repay action deposits cover (C) into the Transmuter, and the next _earmark call only needs to earmark a small amount (Y where Y < C), the current logic uses the entire value of C to potentially reduce Y to 0, but then crucially sets lastTransmuterTokenBalance to the full current balance. The difference (C - Y equivalent) is lost to the system's accounting.
Blocked Earmarking & Debt Repayment: Because the cover value is prematurely consumed, subsequent calls to _earmark will calculate transmuterDifference as 0 (or a much smaller value), even though the Transmuter holds the repaid funds. This prevents the cover from offsetting future earmark amounts (Z), forcing the system to earmark debt (cumulativeEarmarked += Z) that should have been covered by the repaid funds. This directly hinders the protocol's ability to automatically repay debt using the yield generated and the surplus provided by repay actions.
Triggered by Normal Operations: This isn't just a theoretical attack vector. Any call to redeem (via claimRedemption) or poke (or other functions calling _earmark) that occurs after a repay will trigger this value leakage. It's a flaw inherent in the normal operational flow.
Indirect Collateral Impact: While funds aren't directly stolen, the failure to properly earmark and subsequently redeem debt means user debt isn't paid down as efficiently as designed. This indirectly keeps user collateral locked for longer than necessary or could lead to unexpected behavior in collateral ratio calculations down the line.
References
AlchemistV3 Contract: AlchemistV3.sol
Vulnerable Function: _earmark
Incorrect State Update: Line lastTransmuterTokenBalance = transmuterCurrentBalance;
Comparison (Correct Logic): The redeem function calculates coverToApplyDebt and only updates lastTransmuterTokenBalance based on the used portion.
Proof of Concept
Proof of Concept
The following Foundry test case, added to AlchemistV3.t.sol, demonstrates the vulnerability. It shows that after a repay creates cover (C), the first poke call skips earmarking (Y) and incorrectly updates lastTransmuterTokenBalance to C, wasting the cover. The second poke call then incorrectly earmarks (Z) because the cover is no longer recognized.
Was this helpful?