58276 sc critical uncapped feeinyield in resolverepaymentfee allows for collateral theft from other depositors

Submitted on Oct 31st 2025 at 22:24:15 UTC by @Oxdeadmanwalking for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58276

  • Report Type: Smart Contract

  • Report severity: Critical

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

  • Impacts:

    • Smart contract unable to operate due to lack of token funds

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

Description

Brief/Intro

During a user liquidation, a force repayment of earmarked debt is prioritized to allow a user's position to improve by removing earmarked debt before actually seizing their collateral. If this happens, the liquidator gets compensated in a form of a repayment fee which is a proportion of the debt repaid. However, when the debt of the account is cleared entirely after a force repayment (ie account.debt == 0), when _resolveRepaymentFee is called to calculate the fee before actually transferring it out to the liquidator, the fee does not actually get capped to the user's collateral balance meaning if it exceeds the collateral balance of the account being liquidated, a portion of it will be paid by other depositor's collateral making them lose funds.

Vulnerability Details

liquidate() first tries to _forceRepay a position to clear earmarked debt in order to check if the account's position is improved by clearing earmarked debt first to avoid liquidation that way.

        // Try to repay earmarked debt if it exists
        uint256 repaidAmountInYield = 0;
        if (account.earmarked > 0) {
            repaidAmountInYield = _forceRepay(accountId, account.earmarked);
        }

After repayment of earmarked debt, a check is performed if ** all ** of the debt is cleared in order to return early. The caller (liquidator) gets compensated in the form of a repayment fee, calculated as a portion of the debt repaid in _resolveRepaymentFee.

_resolveRepaymentFee subtracts the fee from the user's collateral, if sufficient, but fails to cap the actual fee variable so the full calculated fee is returned in the code above before actually performing the transfer.

This means that TokenUtils.safeTransfer(myt, msg.sender, feeInYield); can send a fee greater than the user's collateral to the liquidator. In this case, the fee will come from the contract's MYT balance, meaning it will be socialized across other depositors which is problematic.

Impact Details

Since the fee can be socialized across other depositors, if everyone tries to close their position, there won't be enough collateral left to service all withdrawals, making last users lose collateral that were owed to them. The deficit will accumulate with more liquidations of this kind.

References

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

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

Proof of Concept

Proof of Concept

  1. Add this test to the end of AlchemistV3.t.sol

  1. To make the issue more clear, import "forge-std/console.sol"; at the top of AlchemistV3 and add those logs to _liquidate at the lines during and after force repayment.

  1. Run the test

  1. Observe the logs. You should see that the MYT outflow from alchemist exceeds the user's collateral liquidated. This indicates that the fee was paid from other depositor's collateral. In addition, in the logs from inside AlchemistV3 liquidation call, the user's collateral balance was 0 but feeInYield was >0.

Was this helpful?