Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
The _forceRepay() function, called during liquidations to repay earmarked debt using a user's collateral, fails to decrement the global cumulativeEarmarked state variable despite reducing the user's local account.earmarked. This breaks the critical accounting invariant that cumulativeEarmarked should equal the sum of all users' account.earmarked values. This causes the protocol to understate available unearmarked debt, throttling new earmarking operations, and creates an unfair distribution of redemption burdens where non-liquidated users bear disproportionate losses during transmuter redemptions
Vulnerability Details
The Accounting Invariant The protocol maintains a critical invariant:
cumulativeEarmarked = Σ(all account.earmarked values) This invariant ensures accurate tracking of how much debt has been earmarked for redemption across all position.
In _forceRepay() (in AlchemistV3.sol)
Compare this with the correct implementation in repay()
The _subDebt() function includes a safety clamp:
However, this only prevents cumulativeEarmarked from exceeding totalDebt, not from being overstated relative to actual user earmarks. This persists and accumulates with each liquidation.
Impact Details
Throttled Earmarking of New Debt The _earmark() function calculates available unearmarked debt as: uint256 liveUnearmarked = totalDebt - cumulativeEarmarked;
When cumulativeEarmarked is overstated, liveUnearmarked is understated, causing:
Less new debt gets earmarked than should be
New borrowers receive inadequate earmark coverage
The transmuter queue doesn't grow proportionally with new debt
Example:
Actual State:
Total debt: 200e18
Actual sum of earmarks: 50e18
Should earmark: 150e18
cumulativeEarmarked: 140e18 (overstated by 90e18 from liquidations)
liveUnearmarked: 200 - 140 = 60e18
Only 60e18 will be earmarked instead of 150e18
90e18 of debt is incorrectly excluded from earmarking
2. Unfair Redemption Burden Distribution
3. Compounding Effect
Each liquidation that triggers _forceRepay adds to the desynchronization:
First liquidation: +90e18
Second liquidation: +50e18
Total : 140e18
Proof of Concept
Proof of Concept
add the test function to the test file of the contract