56794 sc critical liquidators can be overpaid due to accounting error

Submitted on Oct 20th 2025 at 18:26:39 UTC by @Cryptor for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #56794

  • Report Type: Smart Contract

  • Report severity: Critical

  • 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

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

Description

Brief/Intro

Liquidators can be overpaid due to an accounting error

Vulnerability Details

The function _liquidate calls the function _resolveRepaymentFee which is the reward fee sent to liquidators.

 /// @dev Handles repayment fee calculation and account deduction
    /// @param accountId The tokenId of the account to force a repayment on.
    /// @param repaidAmountInYield The amount of debt repaid in yield tokens.
    /// @return fee The fee in yield tokens to be sent to the liquidator.
    function _resolveRepaymentFee(uint256 accountId, uint256 repaidAmountInYield) internal returns (uint256 fee) {
        Account storage account = _accounts[accountId];
        // calculate repayment fee and deduct from account
        fee = repaidAmountInYield * repaymentFee / BPS;
        account.collateralBalance -= fee > account.collateralBalance ? account.collateralBalance : fee;
        emit RepaymentFee(accountId, repaidAmountInYield, msg.sender, fee);
        return fee;
    }

The function computes a nominal fee based on the repaid amount. It then deducts min(fee, account.collateralBalance) from the victim's collateral balance. However, it proceeds to return the full, potentially larger, nominal fee.

The liquidate function then pays TokenUtils.safeTransfer(myt, msg.sender, feeInYield) using that full returned fee, without clamping to the actually deducted amount.

When a victim's collateral is depleted by a forced repayment (_forceRepay), their remaining collateralBalance can be less than the calculated fee. In this case, the contract deducts the small remaining balance but pays the full fee, sourcing the difference from the contract's total holdings of MYT shares. This effectively drains collateral from other, healthy users.

This contrasts with the full liquidation path in _doLiquidation, which correctly checks account.collateralBalance >= feeInYield before paying the base fee, preventing this type of overpayment.

Impact Details

The vulnerability leads to a direct loss of funds from the contract's pooled collateral as an attacker can repeatedly liquidate eligible accounts and siphon MYT shares from the contract with each transaction.

These MYT shares back other users' deposits. The liquidator receives a fee that is subsidized by all other depositors in the system.

References

Add any relevant links to documentation or code

Proof of Concept

Proof of Concept

Add the following code to src/test/AlchemistV3.t.sol

Was this helpful?