58338 sc critical alchemistv3 repayment fee can exceed remaining collateral leading to position insolvency

Submitted on Nov 1st 2025 at 11:23:00 UTC by @T0nraq for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58338

  • Report Type: Smart Contract

  • Report severity: Critical

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

  • Impacts:

    • Protocol insolvency

Description

Summary

When liquidating an account with earmarked debt, the repayment fee may be larger than the account’s remaining collateral after the force repayment and protocol fee are deducted. The helper _resolveRepaymentFee caps the deduction from collateralBalance, but it still returns the raw, larger fee value. The caller then transfers this larger amount to the liquidator, overpaying them from protocol-held tokens (i.e., other users’ deposits), leaving the liquidated position at zero collateral and creating an accounting shortfall. This shortfall also can/is not be added to earmarkable debt and cannot be shared globally to restore accounting parity

Vulnerability Details

_resolveRepaymentFee returns raw fee instead of the actually deducted amount

File: src/AlchemistV3.sol

function _resolveRepaymentFee(uint256 accountId, uint256 repaidAmountInYield) internal returns (uint256 fee) {
    Account storage account = _accounts[accountId];
    fee = repaidAmountInYield * repaymentFee / BPS;               // raw fee
    // Deduct only what the account can afford (caps at remaining collateral)
    account.collateralBalance -= fee > account.collateralBalance ? account.collateralBalance : fee;
    emit RepaymentFee(accountId, repaidAmountInYield, msg.sender, fee);
    return fee;                                                   // BUG: returns raw fee, not deducted amount
}

Liquidation uses the returned (raw) value for transfer

File: src/AlchemistV3.sol

Simple example

  1. Start with a position that, after force repayment and protocol fee, has 4 MYT collateral left.

  2. Repayment fee calculation yields 9 MYT.

  3. _resolveRepaymentFee deducts only 4 MYT from the account (capped at remaining collateral) but still RETURNS 9 MYT.

  4. Liquidation code transfers the returned 9 MYT to the liquidator.

Result: liquidator receives 9 MYT while the account could only afford 4 MYT. The 5 MYT shortfall is taken from protocol-held tokens (others’ deposits), position ends with 0 collateral, and the system is overpaying fees.

Impact

  • Overpayment to liquidators using protocol-held tokens (socializes losses across depositors).

  • Liquidated positions end at zero collateral even when they could not afford the full fee.

  • Accounting shortfall that compounds over multiple liquidations; can lead to insolvency risk and misleading system metrics.

Return the amount actually deducted from collateralBalance, not the raw fee. Keep calculation simple and safe by capping first, then subtracting, then returning what was paid.

Proof of Concept

Proof of Concept

  • Test file: src/test/AlchemistV3.t.sol

  • Add test function (snippet below): testLiquidate_Undercollateralized_Position_With_Earmarked_Debt_Insufficient_Collateral_For_RepaymentFee()

Run the POC:

Was this helpful?