58363 sc high accounting corruption in liquidations due to missing global counter update

Submitted on Nov 1st 2025 at 15:54:43 UTC by @Ibukun for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58363

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol

  • Impacts:

    • Protocol insolvency

Description

Summary

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:

// repay() - CORRECT implementation
uint256 earmarkToRemove = credit > account.earmarked ? account.earmarked : credit;
account.earmarked -= earmarkToRemove;

uint256 earmarkPaidGlobal = cumulativeEarmarked > earmarkToRemove ? earmarkToRemove : cumulativeEarmarked;
cumulativeEarmarked -= earmarkPaidGlobal;  // Global counter 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

  1. Bad debt calculations become wrong - The protocol uses cumulativeEarmarked to calculate health ratios

  2. Redemption weights get skewed - Distribution of yield to users depends on accurate global tracking

  3. Accounting permanently corrupted - Every liquidation with earmarked debt worsens the drift

  4. 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.

Add the same global counter update logic from repay() into _forceRepay():

Proof of Concept

Was this helpful?