The _forceRepay() function fails to update the cumulativeEarmarked global counter when repaying earmarked debt during liquidations. This breaks a critical accounting invariant and causes the global counter to permanently drift from reality.
Vulnerability Details
The Bug
In AlchemistV3, there's a global variable cumulativeEarmarked that tracks the total earmarked debt across all positions. When debt is repaid through the normal repay() function, both the account's earmarked amount AND the global counter are updated:
However, the _forceRepay() function (called during liquidations) only updates the account's earmarked but never touches the global counter:
Why This Matters
The protocol relies on this invariant: cumulativeEarmarked == sum(all account.earmarked)
When liquidations happen, this invariant breaks. The global counter becomes inflated, showing more earmarked debt than actually exists.
Impact
Bad debt calculations become wrong - The protocol uses cumulativeEarmarked to calculate health ratios
Redemption weights get skewed - Distribution of yield to users depends on accurate global tracking
Accounting permanently corrupted - Every liquidation with earmarked debt worsens the drift
Protocol insolvency risk - Inflated numbers can hide real protocol health issues
Proof of Concept
The PoC is in src/test/C01_CumulativeEarmarkedNotUpdated.t.sol
Run: forge test --match-test testCritical_CumulativeEarmarkedNotUpdatedInForceRepay -vv
The test sets up a position with earmarked debt, triggers liquidation, and proves that cumulativeEarmarked stays unchanged even though the position's earmarked amount decreased.
Key observation from the test:
The global counter is now permanently 10e18 too high.
Recommended Fix
Add the same global counter update logic from repay() into _forceRepay():