58450 sc high missing transmuter balance update after redemption blocks future earmarking and underfunds redemptions

Submitted on Nov 2nd 2025 at 13:24:20 UTC by @godwinudo for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58450

  • Report Type: Smart Contract

  • Report severity: High

  • 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

    • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Brief/Intro

The AlchemistV3 contract fails to update the lastTransmuterTokenBalance state variable after redemptions occur in the redeem() function. This causes the earmarking mechanism to incorrectly detect phantom cover in subsequent earmarking operations, which prevents legitimate debt from being earmarked for redemption. As a result, transmuter positions become systematically underfunded, meaning users who deposit synthetic tokens into the transmuter expecting to receive yield-bearing collateral will be unable to claim their full entitled amounts because the system failed to reserve sufficient collateral for their redemptions.

Vulnerability Details

The AlchemistV3 protocol implements an earmarking system to ensure that when users create transmuter positions, sufficient collateral is gradually reserved from borrowers to fulfill those redemption obligations. This earmarking process happens continuously through the _earmark() function, which gets called whenever users interact with their positions through functions like poke(), mint(), withdraw(), and others.

The _earmark() function includes logic to detect and account for cover, which represents yield tokens that borrowers have repaid directly to the transmuter. This cover should reduce the amount of new debt that needs to be earmarked because those tokens are already available for redemption. The function tracks the transmuter's token balance using a state variable called lastTransmuterTokenBalance to calculate how much new cover has accumulated since the last earmark operation.

Here is the relevant section of the _earmark() function that calculates cover:

The issue is that while _earmark() correctly updates lastTransmuterTokenBalance at the end of its execution, the redeem() function does not update this variable after it transfers collateral to the transmuter. Let me show you the relevant portion of the redeem() function:

Notice that redeem() transfers collRedeemed amount of MYT tokens to the transmuter contract, which increases the transmuter's token balance. However, the function never updates lastTransmuterTokenBalance to reflect this transfer. This creates a dangerous inconsistency between the actual transmuter balance and what the system thinks the last recorded balance was.

The consequence of this missing update becomes apparent in the next earmarking operation. When _earmark() is called again, it reads the transmuter's current balance and compares it against the stale lastTransmuterTokenBalance. Since redeem() increased the transmuter's balance without updating the tracking variable, the calculation produces a false positive for cover:

This phantom cover then gets subtracted from the amount that should be earmarked, effectively canceling out legitimate earmarking that needs to happen to fund future transmuter redemptions. The tokens sitting in the transmuter from the previous redemption are mistakenly counted as new cover, when in reality they are already committed to fulfill an existing transmuter position claim.

To illustrate the full attack path, consider this sequence of events. First, 2 borrowers each deposit 100 yield tokens and mint 50 debt tokens. A transmuter user then deposits 50 debt tokens to create a redemption position that will mature over time. After the position is halfway mature, meaning 25 debt tokens worth of earmarking should have occurred, the first borrower calls poke() which triggers earmarking of those 25 debt tokens. The transmuter then calls redeem() to claim the earmarked collateral, which successfully transfers 25 yield tokens to the transmuter, but crucially fails to update lastTransmuterTokenBalance.

Now when time passes and the transmuter position becomes fully mature, meaning another 25 debt tokens should be earmarked from the second borrower's position, the second borrower calls poke(). The _earmark() function executes and queries the transmuter graph, which correctly reports that 25 debt tokens should be earmarked. However, the function also checks the transmuter balance and sees 25 yield tokens sitting there from the previous redemption. Because lastTransmuterTokenBalance was never updated to account for that previous transfer, the system incorrectly identifies these tokens as new cover and subtracts them from the earmarking amount. The result is that 0 debt gets earmarked instead of the required 25 debt tokens.

When the transmuter user eventually tries to claim their fully mature position, they will find that only half the expected collateral has been reserved for them, because the second half of the earmarking never occurred due to the phantom cover detection bug.

Impact Details

Users who deposit synthetic tokens into the transmuter expecting to receive their equivalent value in yield-bearing collateral will receive less than their entitled amount because the earmarking mechanism fails to reserve sufficient collateral from borrowers.

Proof of Concept

Proof of Concept

Add this to AlchemistV3.t.sol, and run

Was this helpful?