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)
{
bool applyPenalty = false;
if (addressLastSwapBlock[sender] > 0) {
uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
if (blocksSinceLastSwap < phaseCooldown) {
applyPenalty = true;
}
}
if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
addressSwappedAmount[sender] += swapAmount;
addressLastSwapBlock[sender] = block.number;
uint24 feeOverride = 0;
if (applyPenalty) {
feeOverride = uint24((phasePenaltyBps * 100));
}
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});
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
(uint256 feeGrowth0Before, uint256 feeGrowth1Before) = _getFeeGrowthGlobals();
vm.roll(block.number + 10);
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
(uint256 feeGrowth0After, uint256 feeGrowth1After) = _getFeeGrowthGlobals();
assertEq(feeGrowth0After - feeGrowth0Before, 0, "No fee on ETH");
assertEq(feeGrowth1After - feeGrowth1Before, 0, "No fee on token");
vm.stopPrank();
}
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
+ );
}