56719 sc high the function forcerepay reduces debt before clamp creating unbacked loan forgiveness and protocol insolvency

Submitted on Oct 19th 2025 at 21:34:47 UTC by @Cryptor for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #56719

  • 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

    • Protocol insolvency

Description

Brief/Intro

The function _forcerepay reduces debt before clamp, creating unbacked loan forgiveness and protocol insolvency

Vulnerability Details

The function _forcerepay is called during liquidation that forces a repay of debt is there is any earmarked debt shown here

    /**
     * @notice Force repays earmarked debt of the account owned by `accountId` using account's collateral balance.
     * @param accountId The tokenId of the account to repay from.
     * @param amount The amount to repay in debt tokens.
     * @return creditToYield The amount of yield tokens repaid.
     */
    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;

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

        creditToYield = creditToYield > account.collateralBalance ? account.collateralBalance : creditToYield;
        account.collateralBalance -= creditToYield;

...

The problem lies here

After reducing the debt from calling the function subdebt, the function then applies a clamp to the creditToYield function; Here, creditToYield = min(convertDebtTokensToYield(credit), account.collateralBalance). It only stays unchanged when collateralBalance >= needed yield; otherwise it’s reduced.

It is important to note that at this point, debt has already been burned via _subDebt(credit) before this clamp and transfer, enabling unbacked forgiveness.

The problem with this is that under certain circumstances this allows loans with low collateral to have their debt erased while transferring little to no tokens to the transmuter.

Impact Details

Insolvency drift: totalDebt decreases without a matching asset inflow to the transmuter, lowering system collateralization and pushing the protocol toward insolvency.

Liquidator disincentive and liveness risk: Liquidators receive little or no assets in these scenarios, removing economic incentives to liquidate and allowing unhealthy positions to persist.

References

https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L738-L783

Proof of Concept

Proof of Concept

paste this code in src/test/AlchemistV3.t.sol

Was this helpful?