58346 sc high forcerepay fails to decrement cumulativeearmarked breaking earmark invariant and skewing redemptions

Submitted on Nov 1st 2025 at 12:37:12 UTC by @mzfr for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58346

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Contract fails to deliver promised returns, but doesn't lose value

Description

Brief/Intro

The _forceRepay() function in AlchemistV3.sol correctly reduces an individual account's earmarked debt but fails to decrement the global cumulativeEarmarked counter, breaking the core accounting invariant that cumulativeEarmarked == Σ(account.earmarked) across all positions. This causes persistent accounting drift that accumulates with every liquidation, skewing the redemption earmarking distribution mechanism. When exploited through repeated liquidations, this bug causes the protocol to incorrectly calculate the proportion of unearmarked debt, leading to unfair distribution of redemption proceeds among borrowers and degraded protocol functionality.

Vulnerability Details

The AlchemistV3 protocol maintains a critical accounting invariant:

cumulativeEarmarked (global) == Σ(account.earmarked) for all accounts

This invariant feeds directly into the _earmark() logic. In the actual code (src/AlchemistV3.sol, around the earmark body(https://github.com/alchemix-finance/v3-poc/blob/immunefi_latest/src/AlchemistV3.sol#L1098-L1128)), the protocol computes the live unearmarked debt and the fraction to earmark, then updates survival and weights:

The repay() function correctly maintains this invariant by updating both counters src/AlchemistV3.sol:521-526arrow-up-right:

However, _forceRepay() only updates the individual account (src/AlchemistV3.sol:760-767arrow-up-right):

When _forceRepay() is Called

_forceRepay() is invoked from _liquidate() around line https://github.com/alchemix-finance/v3-poc/blob/immunefi_latest/src/AlchemistV3.sol#L821 before deciding whether to proceed with a full liquidation. If the account has earmarked debt, the function repays from collateral first, then reassesses the health ratio and may call _doLiquidation():

Proof of Concept

Proof of Concept

  • I placed my POC file here(https://github.com/alchemix-finance/v3-poc/tree/immunefi_latest/src/test) and then just ran forge test --match-path src/test/PoC_ForceRepayCumulativeEarmarkedBug.t.sol -vv

Was this helpful?