57633 sc high block gated earmark call in redeem nullifies prefunded transmuter cover on the first redemption of each block leading to collateral overpayment and potential protocol insolvency
Because redeem() always calls _earmark() first, and _earmark() snapshots the transmuter’s balance into lastTransmuterTokenBalance, the first redemption in each block effectively erases any prefunded MYT cover accumulated before that block. When redeem() then computes the delta (transmuterBal - lastTransmuterTokenBalance), it sees zero and ignores the available cover. As a result, the protocol burns earmarked debt and pulls new collateral even though matching MYT already exists in the Transmuter, gradually draining collateral and unclaimed yield, risking long-term protocol insolvency.
Vulnerability Details
Root cause
The Alchemist determines how much prefunded MYT (“cover”) exists on the Transmuter by comparing the current vault balance to the previously stored lastTransmuterTokenBalance. The _earmark() function both computes and updates this reference value.
When redeem() calls _earmark() first (and this is the first _earmark() of the block), the reference point is immediately reset before redeem() measures the delta. This causes deltaYield to evaluate to zero, even if prefunded cover was present.
When it triggers
On the first call to _earmark() within a block, typically the first redeem() that block.
Any prior prefunded MYT (yield or earmarked debt repayments) will be “seen” by _earmark(), but then immediately overwritten in lastTransmuterTokenBalance. Subsequent redemptions in the same block skip _earmark() due to the block guard, so they may work correctly — but by then, the prefunded cover has already been zeroed out.
Timeline illustration Block N — before redemption Transmuter MYT balance: 1,000 Alchemist.lastTransmuterTokenBalance = 0 Prefunded cover available: 1,000 MYT Expected: next redeem uses this 1,000 as cover
Block N+1 — redeem() starts
redeem() calls _earmark() • Reads balance = 1,000 • Computes coverInDebt = convert(1,000) • Updates lastTransmuterTokenBalance = 1,000 → Cover is acknowledged but not spent.
redeem() resumes • Reads transmuterBal = 1,000 • Computes deltaYield = 0 • Believes no cover is available → overdraws collateral.
Result • Prefunded MYT remains unused. • Earmarked debt reduced as if no cover existed. • Collateral and accounting drift apart.
Why the bug persists and matters
Accounting invariant break: the relationship between totalDebt, cumulativeEarmarked, and transmuter-held MYT is violated as soon as the first redemption runs.
No self-correction: subsequent _earmark() calls only recognize new deltas; the lost prefunded cover never re-enters accounting.
Masking by ongoing yield: future yield can hide the mismatch temporarily, but once yield slows or stops, redemptions start pulling principal.
System-wide desync: LTV, solvency, and earmarking calculations now operate from an incorrect baseline.
Irreversible: no user-accessible function re-syncs lastTransmuterTokenBalance, so the inconsistency persists until patched.
Even if the MYT tokens remain physically in the Transmuter, the logic that tracks and applies them to reduce debt has disconnected — meaning the protocol appears solvent while silently leaking collateral.
Impact Details
Impact Details
Each affected redemption ignores existing prefunded cover, over-repaying debt with collateral. The Transmuter accumulates idle MYT that the Alchemist no longer accounts for. Over time, totalDebt and actual assets diverge; the protocol becomes over-leveraged. When new yield halts, redemptions begin consuming principal, depleting reserves.
When losses materialize:
During periods of low yield or paused strategies, unclaimed cover is permanently lost.
After many redemptions, cumulative collateral depletion can render the protocol insolvent.
During audits or migrations, apparent on-chain solvency may conceal missing collateral equivalent to the unrecognized prefunded cover.
Severity classification:
This issue causes prefunded MYT (unclaimed yield) held by the Transmuter to become permanently unrecognized by the Alchemist’s accounting logic. Although the tokens remain on-chain, they are frozen from the protocol’s perspective — they will never again be used to reduce debt or satisfy redemptions.
Because the loss is deterministic, accumulative, and triggered by normal user operations (redeem()), this aligns precisely with the High severity “Permanent freezing of unclaimed yield”.
The bug does not immediately cause theft or insolvency (so it is not Critical), but it leads to a permanent loss of claimable yield and a progressive erosion of solvency over time, which is economically equivalent to a high-severity vulnerability.
Proof of Concept
Proof of Concept
This PoC reuses the same contracts and libraries from the project’s integration tests but simplifies the environment to reproduce the bug deterministically. Unlike the full integration suite—which deploys through proxies, allocators, strategies, and multi-user scenarios over millions of blocks—this harness directly instantiates a fresh Transmuter and AlchemistV3Harness (a minimally modified version of AlchemistV3 with manual initialization). This isolates the relevant state transition (_earmark() → redeem()) without unrelated side effects or governance layers. All other dependencies, interfaces, and math are identical to production code; only the initialization path is shortened to make the test deterministic and focused.
Copy this file in the test folder src/test/, same place as IntegrationTest.t.sol
and run $forge test --mt testAudit_PrefundedCoverFrozen_FirstRedeemInBlock_PoC --rpc-url $MAINNET_RPC_URL -vvvv