Vanguard

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

Phase 3 Fee Override Bug sets pool fee to 0 instead of using dynamic fee

Author Revealed upon completion

Phase 3 Fee Override Bug sets pool fee to 0 instead of using dynamic fee

Description

The TokenLaunchHook is designed to enforce fees and limits during the initial phases of a token launch (Phase 1 and 2) and revert to standard pool behavior in Phase 3. Normally, in Phase 3, the hook should allow the swap to proceed using the pool's configured dynamic fee without interference.

However, in Phase 3, the _beforeSwap function returns the LPFeeLibrary.OVERRIDE_FEE_FLAG combined with a 0 delta. This flag instructs the PoolManager to override the stored pool fee with the returned fee value. Since no fee value is provided (it defaults to 0 in the return tuple), the pool fee is effectively set to 0% for all swaps in Phase 3.

// src/TokenLaunchHook.sol
if (currentPhase == 3) {
@> return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG);
}

Risk

Likelihood:

  • High, this logic is unconditional for all swaps once Phase 3 is reached.

Impact:

  • The protocol loses all trading fee revenue in Phase 3, which is intended to be the long-term stable state of the pool.

Proof of Concept

// add test to test/TokenLaunchHookUnit.t.sol
// import BeforeSwapDelta for contract harness
import {BeforeSwapDelta} from "v4-core/types/BeforeSwapDelta.sol";
function test_Bug_FeeOverride_Phase3() public {
// Move to phase 3
vm.roll(block.number + phase1Duration + phase2Duration + 1);
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -1 ether,
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
// use the harness to inspect the internal return value of _beforeSwap
// Deploy harness with correct flags
uint160 flags = uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG);
bytes memory constructorArgs = abi.encode(
manager, phase1Duration, phase2Duration, phase1LimitBps, phase2LimitBps,
phase1Cooldown, phase2Cooldown, phase1PenaltyBps, phase2PenaltyBps
);
(address hookAddress, bytes32 salt) = HookMiner.find(address(this), flags, type(TokenLaunchHookHarness).creationCode, constructorArgs);
TokenLaunchHookHarness harness = new TokenLaunchHookHarness{salt: salt}(
manager, phase1Duration, phase2Duration, phase1LimitBps, phase2LimitBps,
phase1Cooldown, phase2Cooldown, phase1PenaltyBps, phase2PenaltyBps
);
harness.setLaunchStartBlock(antiBotHook.launchStartBlock());
harness.setInitialLiquidity(100 ether); // Mock liquidity
(,, uint24 fee) = harness.exposed_beforeSwap(address(swapRouter), key, params, "");
// Fee has OVERRIDE_FEE_FLAG (0x400000) but fee value is 0.
// This tells PoolManager to override the pool fee with 0%.
assertTrue(LPFeeLibrary.isOverride(fee), "Bug: Phase 3 should not override fee");
assertEq(fee, LPFeeLibrary.OVERRIDE_FEE_FLAG, "Bug: Phase 3 overrides fee to 0");
}
contract TokenLaunchHookHarness is TokenLaunchHook {
constructor(
IPoolManager _poolManager,
uint256 _phase1Duration,
uint256 _phase2Duration,
uint256 _phase1LimitBps,
uint256 _phase2LimitBps,
uint256 _phase1Cooldown,
uint256 _phase2Cooldown,
uint256 _phase1PenaltyBps,
uint256 _phase2PenaltyBps
) TokenLaunchHook(
_poolManager,
_phase1Duration,
_phase2Duration,
_phase1LimitBps,
_phase2LimitBps,
_phase1Cooldown,
_phase2Cooldown,
_phase1PenaltyBps,
_phase2PenaltyBps
) {}
function exposed_beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata hookData)
external
returns (bytes4 selector, BeforeSwapDelta delta, uint24 fee)
{
return _beforeSwap(sender, key, params, hookData);
}
function setLaunchStartBlock(uint256 blockNumber) external {
launchStartBlock = blockNumber;
}
function setInitialLiquidity(uint256 liquidity) external {
initialLiquidity = liquidity;
}
}

Recommended Mitigation

Use 0 instead of LPFeeLibrary.OVERRIDE_FEE_FLAG to indicate that no override should occur.

if (currentPhase == 3) {
- return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG);
+ return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}

Support

FAQs

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

Give us feedback!