For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 V3

  • 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?