57330 sc critical resolverepaymentfee returns initial fee when fee is greater collateral balance

Submitted on Oct 25th 2025 at 09:49:55 UTC by @Josh4324 for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57330

  • Report Type: Smart Contract

  • Report severity: Critical

  • 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

The _resolveRepaymentFee function in the AlchemistV3 contract calculates and returns a repayment fee based on the repaid yield amount, but it always returns and emits the full calculated fee even when it exceeds the account's remaining collateral balance. In such cases, only the available balance is deducted, leading to mismatches between returned/emitted values and actual deductions. This can cause misleading events, and over-transfers in upstream functions, resulting in fund theft from the funds in the alchemist contract.

Vulnerability Details

function _liquidate(uint256 accountId) internal returns (uint256 amountLiquidated, uint256 feeInYield, uint256 feeInUnderlying) {
        // Query transmuter and earmark global debt
        _earmark();
        // Sync current user debt before deciding how much needs to be liquidated
        _sync(accountId);

        Account storage account = _accounts[accountId];

        // Early return if no debt exists
        if (account.debt == 0) {
            return (0, 0, 0);
        }

        // In the rare scenario where 1 share is worth 0 underlying asset
        if (IVaultV2(myt).convertToAssets(1e18) == 0) {
            return (0, 0, 0);
        }

        // Calculate initial collateralization ratio
        uint256 collateralInUnderlying = totalValue(accountId);
        uint256 collateralizationRatio = collateralInUnderlying * FIXED_POINT_SCALAR / account.debt;

        // If account is healthy, nothing to liquidate
        if (collateralizationRatio > collateralizationLowerBound) {
            return (0, 0, 0);
        }

        // Try to repay earmarked debt if it exists
        uint256 repaidAmountInYield = 0;
        if (account.earmarked > 0) {
            repaidAmountInYield = _forceRepay(accountId, account.earmarked);
        }
        // If debt is fully cleared, return with only the repaid amount, no liquidation needed, caller receives repayment fee
        if (account.debt == 0) {
   @>         feeInYield = _resolveRepaymentFee(accountId, repaidAmountInYield);
    @>        TokenUtils.safeTransfer(myt, msg.sender, feeInYield);
            return (repaidAmountInYield, feeInYield, 0);
        }

        // Recalculate ratio after any repayment to determine if further liquidation is needed
        collateralInUnderlying = totalValue(accountId);
        collateralizationRatio = collateralInUnderlying * FIXED_POINT_SCALAR / account.debt;

        if (collateralizationRatio <= collateralizationLowerBound) {
            // Do actual liquidation
            return _doLiquidation(accountId, collateralInUnderlying, repaidAmountInYield);
        } else {
            // Since only a repayment happened, send repayment fee to caller
   @>       feeInYield = _resolveRepaymentFee(accountId, repaidAmountInYield);
 @>           TokenUtils.safeTransfer(myt, msg.sender, feeInYield);
            return (repaidAmountInYield, feeInYield, 0);
        }
    }

When the calculated fee (repaidAmountInYield * repaymentFee / BPS) is greater than the account's collateralBalance:

The deduction is capped: Only the collateral balance is subtracted. However, the function returns and emits the full uncapped fee.

This discrepancy means the _liquidate function receives an overstated fee value, which is used for transfer to the liquidator. Since transfers pull from the contract's total MYT balance, this can over-transfer, stealing from other users.

Impact Details

Over-transfers steals funds from other users.

References

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

Proof of Concept

Proof of Concept

Copy the test below into src/test/AlchemistV3.t.sol Run forge test --mt testFlow -vvvv

The test below shows that the _liquidate function over transfer funds which inlcudes other users funds in order to pay the liquidator its repayment fee.

Was this helpful?