58635 sc high cumulativeearmarked is not subtracted in forcerepay

Submitted on Nov 3rd 2025 at 18:10:51 UTC by @Tarnishedx0 for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58635

  • 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

Brief/Intro

cumulativeEarmarked is not subtracted in _forceRepay(). When Transmuter redeems based on inflated earmarks, protocol transfers non-existent collateral which leads to insolvency.

Vulnerability Details

In the _forceRepay() function called during liquidation, account.earmarked is subtracted by:

    function _forceRepay(uint256 accountId, uint256 amount) internal returns (uint256) {
        if (amount == 0) {
            return 0;
        }
        _checkForValidAccountId(accountId);
        Account storage account = _accounts[accountId];

        // Query transmuter and earmark global debt
        _earmark();

        // Sync current user debt before deciding how much is available to be repaid
        _sync(accountId);

        uint256 debt;

        // Burning yieldTokens will pay off all types of debt
        _checkState((debt = account.debt) > 0);

        uint256 credit = amount > debt ? debt : amount;
        uint256 creditToYield = convertDebtTokensToYield(credit);
        _subDebt(accountId, credit);

        // Repay debt from earmarked amount of debt first
        uint256 earmarkToRemove = credit > account.earmarked ? account.earmarked : credit;
        account.earmarked -= earmarkToRemove;

...

But the cumulativeEarmarked is not subtracted like in the repay() function:

Impact Details

In the _earmark() function:

  • liveUnearmarked will be deflated which will increase _survivalAccumulator and _earmarkWeight.

Similarly, In the redeem() function, _survivalAccumulator and _redemptionWeight will be inflated.

_sync() function will use inflated weights to calculate incorrect account.earmarked and account.debt.

The real issue is, cumulativeEarmarked compounds with each liquidation due to which redemptions may fail or return wrong amounts which breaks the whole internal accounting of the protocol, creates bad debt (We can redeem more amount than actual total earmarked as it is inflated) and make the protocol insolvent. Protocol accounting becomes inconsistent with each liquidation.

References

The above code snippets can be verified here:

  • _forceRepay(): https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L738-L782

  • repay(): https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L525-L526

  • _earmark(): https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L1098-L1132

  • redeem(): https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L589-L641

  • _sync(): https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L1042-L1095

Proof of Concept

Proof of Concept

Paste the above test in AlchemistV3.t.sol contract and set up $MAINNET_RPC_URL then run it using:

Was this helpful?