_forceRepay clears a borrower's earmarked debt locally but never subtracts the same amount from the global cumulativeEarmarked. After a handful of liquidations that hit _forceRepay, the protocol believes almost all debt is already earmarked; _earmark then stops allocating new debt to the Transmuter, so future redemptions can't be serviced and user funds become permanently stuck.
Vulnerability Details
During liquidation the contract calls _forceRepay (src/AlchemistV3.sol:819-823).
_forceRepay reduces the account's earmarked balance (src/AlchemistV3.sol:760-763), but unlike repay() or redeem() it never subtracts the removed amount from cumulativeEarmarked.
Consequently, cumulativeEarmarked only grows. Once it equals totalDebt, every call to _earmark() sees liveUnearmarked = totalDebt - cumulativeEarmarked == 0 (src/AlchemistV3.sol:1114), so no new debt is earmarked for the Transmuter queue.
When the next user tries to create or claim a redemption, the Transmuter finds no earmarked debt available and the system stalls even though borrowers are still solvent.
The issue does not require global insolvency; a single liquidation that enters _forceRepay is enough to corrupt the global counter.
Impact Details
Earmarking debt is the only way the protocol reserves collateral for Transmuter claims. Once cumulativeEarmarked stays stuck at its old value, new earmarks cease and redemption requests back up forever.
All alAsset holders waiting in the Transmuter queue are unable to exit, amounting to a permanent freeze of user funds.
Because the global counter is never corrected, the freeze persists across liquidations and redemptions until the contract is upgraded.
Proof of Concept
Proof of Concept
Logs:
The test performs the following:
Creates three borrowers (A, B, C), earmarks A and B by opening Transmuter redemptions.
Liquidates borrower A, which triggers _forceRepay.
Reads state afterwards: borrower-level earmarks are zero (attackerEarmarkAfter = 0, bystanderEarmarkAfter = 0) yet the global counter is still globalAfter = 8561643835616438357021.
Because the global counter is larger than the sum of account earmarks, future _earmark calls see no "live" debt and Transmuter redemptions cannot proceed, proving the permanent-freeze condition.
newuser@LAPTOP-MLPJMQD2:~/v3-poc$ FOUNDRY_PROFILE=default forge test --match-test testPoC_forceRepay_leaves_global_earmarks --jobs 1 --evm-version cancun
Warning: This is a nightly build of Foundry. It is recommended to use the latest stable version. Visit https://book.getfoundry.sh/announcements for more information.
To mute this warning set `FOUNDRY_DISABLE_NIGHTLY_WARNING` in your environment.
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for src/test/AlchemistV3.t.sol:AlchemistV3Test
[PASS] testPoC_forceRepay_leaves_global_earmarks() (gas: 4545164)
Logs:
attackerCollateralBefore 200000000000000000000000
attackerDebtBefore 180000000000000000018000
attackerEarmarkBefore 8561643835616438357021
globalBefore 8561643835616438357021
totalDebtBefore 180000000000000000018000
attackerRatioAfterDrop 1111111111111111111
highCollateralRequirement 1300000000000000000
attackerCollateralAfter 84066666666666666591009
attackerDebtAfter 64666666666666666608469
attackerEarmarkAfter 0
bystanderDebtAfter 180000000000000000018000
bystanderEarmarkAfter 0
globalAfter 8561643835616438357021
totalDebtAfter 244666666666666666626469
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 97.85ms (83.58ms CPU time)
Ran 1 test suite in 100.23ms (97.85ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)