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).
Recommended Mitigation
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?