Vanguard

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

Phase 3 Conditional Check in `_beforeSwap()` Sets Swap Fee to Zero

Author Revealed upon completion

Root + Impact

- In "Phase 3" (e.g. no active launch phase) in the `_beforeSwap` hook implementation sets the fee override to zero, effectively waiving all swap fees instead of applying the standard Uniswap fee.

- As a result, under normal post-launch trading conditions, users would pay no fees on swaps, leading to zero fee revenue for liquidity providers in the pool.

Description

  • In `_beforeSwap()`, there is a series of checks, including a check for which phase we are in. If we are in Phase 3, the function immediately returns with "return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG);"

  • The override fee flag should be accompanied with a fee, such as "fee | LPFeeLibrary.OVERRIDE_FEE_FLAG", so the above effectively passes " 0 | OVERRIDE_FEE_FLAG", which results in a fee of zero and zero revenue for LPs

function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ...
if (blocksSinceLaunch <= phase1Duration) {
newPhase = 1;
} else if (blocksSinceLaunch <= phase1Duration + phase2Duration) {
newPhase = 2;
} else {
newPhase = 3;
}
if (newPhase != currentPhase) {
_resetPerAddressTracking();
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
if (currentPhase == 3) {
@> return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG); // overiding fee to 0
}
// ... rest of function
}

Risk

Likelihood: High

  • This will occur on every swap after the launch phases are complete

Impact: Medium

  • LPs will receive no fees from swaps during normal trading conditions

  • This will lead to lost revenue and may result in liquidity withdrawal.

Proof of Concept

function test_NoFeesInPhase3() public {
//ARRANGE: Deal some ETH arrange same swap params
vm.deal(user1, 1 ether);
uint256 amountIn = 0.001 ether;
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(amountIn),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
uint256 initialUser1TokenBalance = token.balanceOf(user1);
vm.roll(block.number + phase1Duration + phase2Duration + 1); //Move to phase 3
assertEq(antiBotHook.getCurrentPhase(), 3, "Should be in phase 3");
// ACT: User1 performs swap in phase 3
vm.startPrank(user1);
token.approve(address(swapRouter), type(uint256).max);
// swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
swapRouter.swap{value: amountIn}(key, params, testSettings, abi.encode(user1));
vm.stopPrank();
uint256 FinalUser1TokenBalance = token.balanceOf(user1);
//Assert: User1 should receive full amount without penalty
uint256 tolerance = 1e10; // Small tolerance for slippage
assertApproxEqAbs(FinalUser1TokenBalance - initialUser1TokenBalance, amountIn, tolerance);
}

Recommended Mitigation

  • Implement a standard fee variable to be applied during Phase 3

+ uint256 public immutable phase3StandardFeeBps;
constructor(
IPoolManager _poolManager,
uint256 _phase1Duration,
uint256 _phase2Duration,
uint256 _phase1LimitBps,
uint256 _phase2LimitBps,
uint256 _phase1Cooldown,
uint256 _phase2Cooldown,
uint256 _phase1PenaltyBps,
- uint256 _phase2PenaltyBps
+ uint256 _phase2PenaltyBps,
+ uint256 _phase3StandardFeeBps
) BaseHook(_poolManager) {
if (_phase1Duration == 0 || _phase2Duration == 0) revert InvalidConstructorParams();
if (
- _phase1LimitBps > 10000 || _phase2LimitBps > 10000 || _phase1PenaltyBps > 10000 || _phase2PenaltyBps > 10000
+ _phase1LimitBps > 10000 || _phase2LimitBps > 10000 || _phase1PenaltyBps > 10000 || _phase2PenaltyBps > 10000 || _phase3StandardFeeBps > 10000
) revert InvalidConstructorParams();
// ...
+ phase3StandardFeeBps = _phase3StandardFeeBps;
}
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ...
if (blocksSinceLaunch <= phase1Duration) {
newPhase = 1;
} else if (blocksSinceLaunch <= phase1Duration + phase2Duration) {
newPhase = 2;
} else {
newPhase = 3;
}
if (newPhase != currentPhase) {
_resetPerAddressTracking();
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
if (currentPhase == 3) {
- return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG);
+ uint24 standardFee = uint24(phase3StandardFeeBps * 100); // Convert bps to Uniswap fee
+ return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, standardFee | LPFeeLibrary.OVERRIDE_FEE_FLAG);
}
// ... rest of function
}

Support

FAQs

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

Give us feedback!