57745 sc high syn fails to update the rawlocked valuation leading to a loss of fund for users with rawlock 0 when total lock become 0

Submitted on Oct 28th 2025 at 16:13:45 UTC by @Outliers for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57745

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Brief/Intro

A logic flaw in the collateral unlocking and synchronization flow can lead to users being incorrectly charged collateral even after their debt has been fully cleared. This occurs due to the capping of rawLocked values and failure to re-synchronise the locked collateral before applying weight-based recalculations. This will result in users losing collateral unfairly or being permanently stuck with residual locked balances that will trigger a loss eventually, ultimately leading to user fund loss and protocol accounting inconsistencies.

Vulnerability Details

We cap tofree to total locked // For cases when someone above minimum LTV gets liquidated.

    /// @dev Subtracts the debt by `amount` for the account owned by `tokenId`.
    ///
    /// @param tokenId   The account owned by tokenId.
    /// @param amount  The amount to decrease the debt by.
    function _subDebt(uint256 tokenId, uint256 amount) internal {
        Account storage account = _accounts[tokenId];

        // Update collateral variables
        uint256 toFree = convertDebtTokensToYield(amount) * minimumCollateralization / FIXED_POINT_SCALAR;
        uint256 lockedCollateral = convertDebtTokensToYield(account.debt) * minimumCollateralization / FIXED_POINT_SCALAR;

@audit>>        // For cases when someone above minimum LTV gets liquidated.

@audit>>          if (toFree > _totalLocked) {
            toFree = _totalLocked;
        }

        account.debt -= amount;
        totalDebt -= amount;
        _totalLocked -= toFree;

@audit>>        account.rawLocked = lockedCollateral - toFree;                            // capping this is an issue bug price flunctuations and some users will be stuck here 

        // Clamp to avoid underflow due to rounding later at a later time
        if (cumulativeEarmarked > totalDebt) {
            cumulativeEarmarked = totalDebt;
        }
    }

But this will trigger an action where rawlocked is > 0 and total locked is = 0. This state looks harmkess at bfirst but after further interactions with the contract from the transmuter claim redemption and other new depositor and minted entering into the sytem, we end up creating doing an incorrect sync action.

The issue becomes critical during subsequent sync operations triggered by redemptions or transmuter updates. Specifically, in _sync():

In other parts of the code when we want to evaluate the locked funds, we ensure we reevaluate the locked valuation based on the debt value.

Because account.rawLocked was not re-evaluated to reflect the cleared debt before recalculating collateral removal, the system effectively charges collateral against a zero-debt position.

E.g see on new debt addtion

add debt handled this well to show that rawlocked can be subject to change based on coversion and minimumcollateralization.

Ignoring this in sync leads to charging a user without debt

Impact Details

Users can lose collateral even after repaying or being liquidated.

Residual locked funds may remain indefinitely, causing users to lose a part of their collateral on the next sync call. Accounting inconsistencies may arise between user balances and total system collateral.

References

Add any relevant links to documentation or code

Proof of Concept

Proof of Concept

I added some helpers to helo console log the impact.

Result

Was this helpful?