Vanguard

First Flight #56
Beginner FriendlyDeFiFoundry
0 EXP
Submission Details
Impact: high
Likelihood: high

Zero LP Fees on Non-Penalized Swaps

Author Revealed upon completion

Root + Impact

Description

When a user swaps without triggering a penalty (e.g., after cooldown expires), the hook returns a fee override of 0%, meaning LPs receive no fees from that swap. This is direct fund theft from liquidity providers.

Normal behavior: Every swap should generate fees for LPs. Instead, fee override logic sets fee to 0 when no penalty applies, stealing LP revenue.

Root Cause

// File: src/TokenLaunchHook.sol, lines 174-181
uint24 feeOverride;
if (phasePenaltyBps > 0) {
feeOverride = uint24((phasePenaltyBps * 100)); // Penalty fee
} else {
feeOverride = 0; // @> ISSUE: Zero fee when no penalty
}
return (
address(this),
BeforeSwapDelta.wrap(0),
feeOverride | OVERRIDE_FEE_FLAG // @> Always sets override flag, resulting in zero fee
);

Risk

Likelihood: HIGH

  • Cooldown periods expire regularly (5 blocks in phase 1), swaps after cooldown are common

  • No special conditions: normal users trigger this every time they swap after waiting

  • Affects every non-penalized swap in the protocol

Impact: HIGH

  • LP deposits earn 0% fees on 50%+ of swaps (all non-penalized ones)

  • Over time, LP returns decrease significantly

  • If protocol takes 10% APY in fees normally, LPs get ~5% APY due to this bug

  • Direct fund loss: LPs lose trading fees they're entitled to

Proof of Concept

This PoC performs a swap after the cooldown period (no penalty), then calls beforeSwap as the PoolManager to read the overridden fee. It asserts the fee value is 0, proving non‑penalized swaps return a zero fee and LPs receive no fees

function test_Finding_FeeOverrideZeroOnNoPenalty() public {
vm.deal(user1, 1 ether);
vm.startPrank(user1);
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(0.001 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings = PoolSwapTest
.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
// After cooldown, swap again with zero fee
vm.roll(block.number + phase1Cooldown + 1);
vm.prank(address(manager));
(, , uint24 fee) = antiBotHook.beforeSwap(
address(swapRouter),
key,
params,
ZERO_BYTES
);
uint24 feeValue = fee & ~LPFeeLibrary.OVERRIDE_FEE_FLAG;
assertEq(feeValue, 0, "Non-penalized swap has zero fee—LPs lose fees");
}

Recommended Mitigation

Only apply fee overrides when a penalty is required. When no penalty applies, return the normal pool fee (or do not set the override flag). This preserves LP fee revenue while keeping penalties intact during restricted phases.

// If a penalty applies, override the fee to the penalty tier.
// If no penalty applies, override to the normal pool fee (e.g., 0.30% = 3000).
// This ensures LPs still earn fees on non-penalized swaps, while penalized
// swaps charge the higher anti-bot fee.
uint24 feeOverride;
if (phasePenaltyBps > 0) {
feeOverride = uint24((phasePenaltyBps * 100)); // Penalty tier (bps -> fee units)
} else {
feeOverride = 3000; // Normal pool fee when no penalty, not zero
}
return (
address(this),
BeforeSwapDelta.wrap(0),
feeOverride | OVERRIDE_FEE_FLAG // Override always set, but fee value differs
);

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!