58705 sc low mismatch between emitted protocol fee and actual fee paid in forcerepay due to strict inequality check

Submitted on Nov 4th 2025 at 07:06:06 UTC by @Ambitious_DyDx for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58705

  • Report Type: Smart Contract

  • Report severity: Low

  • 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

Description

Description

The _forceRepay function calculates and emits a protocol fee in the ForceRepay event, but due to a strict greater-than (>) check when transferring the fee from remaining collateral, if the remaining exactly equals the computed fee (possible via integer flooring), it skips the transfer (actual paid=0) while still emitting the full value. This creates a mismatch where the event "promises" a non-zero fee but delivers 0.

Vulnerability Details

In _forceRepay (src/AlchemistV3.sol), after repaying earmarked debt with creditToYield = min(credit, remaining collateral):

  • Subtract creditToYield from collateral.

  • Compute protocolFeeTotal = creditToYield * protocolFee / BPS (floored by int div).

  • Then check if (account.collateralBalance > protocolFeeTotal) to subtract/transfer fee.

As shown:

  • If remaining == protocolFeeTotal (e.g., exact after subtract + floor), skips: actual=0 paid.

  • But always emits ForceRepay(..., protocolFeeTotal) with computed (non-zero) value.

This same function is called internally during liquidate, so the bug propagates there too.

Attack Vector

No active attack required; occurs naturally in edge cases where remaining collateral exactly matches the floored fee after repayment. For clarity, consider this scenario:

  • Position: collateral=105, debt=100, earmarked=100 (setup via storage or flow).

  • protocolFee=500 (5%) → fee=5 (100*0.05, exact no floor loss here for simplicity).

  • _forceRepay: creditToYield=100 (min(100,105)).

  • collateral -=100 → remaining=5.

  • Check: 5 >5? No → skip transfer (actual=0 paid).

  • Emit ForceRepay(...,5) → "promises" 5, but pays 0.

  • Borrower retains extra 5 in collateral (can withdraw later); protocol gets 0 instead of 5.

The PoC demonstrates this: emits 5, pays 0, mismatch.

Impact Details

  • Incorrect Event Data: Misleads off-chain tools/logs (e.g., indexers, analytics, UIs) about collected fees, failing to deliver accurate "promised" accounting/returns.

  • Minor Protocol Under-Delivery: Protocol/feeReceiver gets less (0 vs. emitted X), under-delivering expected revenue. No permanent loss (system solvent; borrower benefits, can repay/withdraw extra).

Change the strict '>' to '>=' in the if-check to handle exact equality:

This ensures transfer when remaining exactly matches fee, aligning with emission. Thanks!

Proof of Concept

Proof of Concept

Add to v3-poc/src/test/AlchemistV3.t.sol:

Run:

Expected Output:

Was this helpful?