Vanguard

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

Zero fee applied to legitimate users breaks LP revenue model

Author Revealed upon completion

Description

Uniswap V4 pools are initialized with a base fee (e.g., 0.3% or 1%) that generates revenue for liquidity providers on every swap. Hooks can override this fee using dynamic fee functionality to implement custom fee logic while maintaining the base fee structure.

The TokenLaunchHook sets the fee to 0% for all users who comply with limits and cooldowns, effectively eliminating LP revenue from legitimate swaps. Only penalized users (those violating limits or cooldowns) generate any fees, creating an unsustainable economic model where LPs earn nothing from normal trading activity.

function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ... validation and phase logic ...
bool applyPenalty = false;
// Check cooldown violation
if (addressLastSwapBlock[sender] > 0) {
uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
if (blocksSinceLastSwap < phaseCooldown) {
applyPenalty = true;
}
}
// Check limit violation
if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
addressSwappedAmount[sender] += swapAmount;
addressLastSwapBlock[sender] = block.number;
// @> Fee initialized to 0 for compliant users
uint24 feeOverride = 0;
if (applyPenalty) {
// @> Only set fee when penalty applies
feeOverride = uint24((phasePenaltyBps * 100));
}
// @> Returns 0% fee for legitimate users, only penalized users pay fees
// LPFeeLibrary.OVERRIDE_FEE_FLAG overrides default fee with feeOverride variable, which is in this case 0%
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
feeOverride | LPFeeLibrary.OVERRIDE_FEE_FLAG
);
}

Risk

Likelihood: High

  • Every legitimate user who stays within limits and respects cooldowns pays 0% fees during launch phases (Phases 1 and 2)

  • The majority of users are expected to be legitimate traders rather than bots, meaning most swaps generate no LP revenue

Impact: High

  • Liquidity providers earn no fees from legitimate trading activity, removing their primary incentive to provide liquidity

  • Only bot activity (which should be rare if the anti-bot system works) generates revenue, creating a broken economic model

  • Makes the pool unattractive for LPs compared to standard Uniswap pools that charge fees on all swaps

Proof of Concept

function test_LegitimateUsersPay_ZeroFee() public {
vm.deal(user1, 10 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});
// First swap - user complies with limits and cooldown
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
// Get fee growth before second swap
(uint256 feeGrowth0Before, uint256 feeGrowth1Before) = _getFeeGrowthGlobals();
// Second swap - still compliant (respects cooldown)
vm.roll(block.number + 10);
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
// Get fee growth after second swap
(uint256 feeGrowth0After, uint256 feeGrowth1After) = _getFeeGrowthGlobals();
// PROOF: feeGrowthGlobal unchanged = 0% fee charged
assertEq(feeGrowth0After - feeGrowth0Before, 0, "No fee on ETH");
assertEq(feeGrowth1After - feeGrowth1Before, 0, "No fee on token");
vm.stopPrank();
}
// Helper to read feeGrowthGlobal from pool storage
function _getFeeGrowthGlobals() internal view returns (uint256, uint256) {
PoolId poolId = key.toId();
bytes32 slot = keccak256(abi.encode(poolId, uint256(2)));
uint256 feeGrowthGlobal0 = uint256(manager.extsload(bytes32(uint256(slot) + 2)));
uint256 feeGrowthGlobal1 = uint256(manager.extsload(bytes32(uint256(slot) + 3)));
return (feeGrowthGlobal0, feeGrowthGlobal1);
}

Recommended Mitigation

Return without overriding the fee when no penalty applies, allowing the pool to use its configured base fee:

function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ... existing validation and phase logic ...
bool applyPenalty = false;
// ... existing penalty checks ...
- uint24 feeOverride = 0;
- if (applyPenalty) {
- feeOverride = uint24((phasePenaltyBps * 100));
- }
- return (
- BaseHook.beforeSwap.selector,
- BeforeSwapDeltaLibrary.ZERO_DELTA,
- feeOverride | LPFeeLibrary.OVERRIDE_FEE_FLAG
- );
+ if (!applyPenalty) {
+ // Don't override - let pool use its configured base fee
+ return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
+ }
+
+ // Apply penalty fee
+ uint24 penaltyFee = uint24((phasePenaltyBps * 100));
+ return (
+ BaseHook.beforeSwap.selector,
+ BeforeSwapDeltaLibrary.ZERO_DELTA,
+ penaltyFee | LPFeeLibrary.OVERRIDE_FEE_FLAG
+ );
}

Support

FAQs

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

Give us feedback!