Vanguard

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

Phase 1 & 2 swaps have fee overridden to 0

Author Revealed upon completion

Phase 1 & 2 swaps have fee overridden to 0

Description

  • In Phase 1 and 2, the protocol is intended to apply a penalty fee only when a user exceeds their swap limit or cooldown. Normal, compliant swaps should pay the standard pool fee.

  • However, the _beforeSwap function unconditionally sets the LPFeeLibrary.OVERRIDE_FEE_FLAG in the return value, regardless of whether a penalty is applied. When applyPenalty is false, feeOverride is 0. This results in the hook returning 0 | OVERRIDE_FEE_FLAG, causing the PoolManager to override the standard pool fee with 0%.

// src/TokenLaunchHook.sol
uint24 feeOverride = 0;
if (applyPenalty) {
feeOverride = uint24((phasePenaltyBps * 100));
}
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
@> feeOverride | LPFeeLibrary.OVERRIDE_FEE_FLAG
);

Risk

Likelihood:

  • This affects every compliant swap in Phases 1 and 2.

Impact:

  • The protocol loses fee revenue for all legitimate trades during the launch phases.

Proof of Concept

// add contract harness and test to test/TokenLaunchHookUnit.t.sol
import {BeforeSwapDelta} from "v4-core/types/BeforeSwapDelta.sol";
function test_VerifyNormalSwapFeeBug() public {
// Phase 1, compliant swap amount
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -0.001 ether, // Within limit
sqrtPriceLimitX96: 0
});
(,, uint24 fee) = hook.exposed_beforeSwap(address(this), key, params, "");
// It returns LPFeeLibrary.OVERRIDE_FEE_FLAG even for normal swaps
// This effectively sets the fee to 0
assertTrue(LPFeeLibrary.isOverride(fee), "Bug: Normal swap should NOT override fee");
assertEq(fee, LPFeeLibrary.OVERRIDE_FEE_FLAG, "Bug: Normal swap fee is overridden 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

Only set the override flag when a penalty is actually applied.

uint24 feeOverride = 0;
+ uint24 feeFlag = 0;
if (applyPenalty) {
feeOverride = uint24((phasePenaltyBps * 100));
+ feeFlag = LPFeeLibrary.OVERRIDE_FEE_FLAG;
}
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
- feeOverride | LPFeeLibrary.OVERRIDE_FEE_FLAG
+ feeOverride | feeFlag
);

Support

FAQs

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

Give us feedback!