Vanguard

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

Incomplete constructor validation allows zero limits causing all swaps to pay penalty fees

Author Revealed upon completion

Incomplete constructor validation allows zero limits causing all swaps to pay penalty fees

Description

The constructor validates that phase1Duration and phase2Duration are non-zero, but does not apply the same validation to phase1LimitBps and phase2LimitBps. If either limit is deployed as zero, the maxSwapAmount calculation in _beforeSwap during the relevant phase will always result in zero:

uint256 maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000;

This means every swap will exceed the limit, triggering penalty fees for all users regardless of their swap amount. Since these variables are immutable, this cannot be corrected after deployment and the hook will be permanently broken.

constructor(
IPoolManager _poolManager,
uint256 _phase1Duration,
uint256 _phase2Duration,
uint256 _phase1LimitBps,
uint256 _phase2LimitBps,
uint256 _phase1Cooldown,
uint256 _phase2Cooldown,
uint256 _phase1PenaltyBps,
uint256 _phase2PenaltyBps
) BaseHook(_poolManager) {
if (_phase1Duration == 0 || _phase2Duration == 0) revert InvalidConstructorParams();
if (
@> _phase1LimitBps > 10000 || _phase2LimitBps > 10000 || _phase1PenaltyBps > 10000 || _phase2PenaltyBps > 10000
) revert InvalidConstructorParams();
phase1Duration = _phase1Duration;
phase2Duration = _phase2Duration;
phase1LimitBps = _phase1LimitBps;
phase2LimitBps = _phase2LimitBps;
phase1Cooldown = _phase1Cooldown;
phase2Cooldown = _phase2Cooldown;
phase1PenaltyBps = _phase1PenaltyBps;
phase2PenaltyBps = _phase2PenaltyBps;
currentPhase = 0;
lastPhaseUpdateBlock = 0;
totalPenaltyFeesCollected = 0;
}

Risk

Likelihood:

  • The contract already validates some constructor parameters (durations), suggesting the developer intended to validate inputs but missed the limit parameters

  • A deployment script error or misconfiguration could easily pass zero for these values without any revert

Impact:

  • Every swap during the affected phase will pay penalty fees, regardless of swap size, as maxSwapAmount will be zero

  • The hook becomes effectively unusable for its intended purpose of protecting legitimate users while penalizing bots - instead it penalizes everyone equally

  • Since the limit variables are immutable, a new hook contract must be deployed to fix the issue, requiring pool migration

Proof of Concept

As shown in the code below from TokenLaunchHook::_beforeSwap, maxSwapAmount is calculated by (initialLiquidity * phaseLimitBps) / 10000, and phaseLimitBps is derived from uint256 phaseLimitBps = currentPhase == 1 ? phase1LimitBps : phase2LimitBps

This means that if phase1LimitBps or phase2LimitBps are set to zero:

maxSwapAmount = (initialLiquidity * 0) / 10000
maxSwapAmount = 0

This shows that all swaps will enter the if(applyPenalty) block and have to pay penalty fees

function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ...
@> uint256 phaseLimitBps = currentPhase == 1 ? phase1LimitBps : phase2LimitBps;
uint256 phaseCooldown = currentPhase == 1 ? phase1Cooldown : phase2Cooldown;
uint256 phasePenaltyBps = currentPhase == 1 ? phase1PenaltyBps : phase2PenaltyBps;
uint256 swapAmount =
params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);
@> uint256 maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000;
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
);
}

Recommended Mitigation

Add zero checks for the limit parameters passed into the constructor

constructor(
IPoolManager _poolManager,
uint256 _phase1Duration,
uint256 _phase2Duration,
uint256 _phase1LimitBps,
uint256 _phase2LimitBps,
uint256 _phase1Cooldown,
uint256 _phase2Cooldown,
uint256 _phase1PenaltyBps,
uint256 _phase2PenaltyBps
) BaseHook(_poolManager) {
if (_phase1Duration == 0 || _phase2Duration == 0) revert InvalidConstructorParams();
- if (
- _phase1LimitBps > 10000 || _phase2LimitBps > 10000 || _phase1PenaltyBps > 10000 || _phase2PenaltyBps > 10000
- ) revert InvalidConstructorParams();
+ if (
+ _phase1LimitBps == 0 || _phase2LimitBps == 0 ||_phase1LimitBps > 10000 || _phase2LimitBps > 10000 || _phase1PenaltyBps > 10000 || _phase2PenaltyBps > 10000
+ ) revert InvalidConstructorParams();
phase1Duration = _phase1Duration;
phase2Duration = _phase2Duration;
phase1LimitBps = _phase1LimitBps;
phase2LimitBps = _phase2LimitBps;
phase1Cooldown = _phase1Cooldown;
phase2Cooldown = _phase2Cooldown;
phase1PenaltyBps = _phase1PenaltyBps;
phase2PenaltyBps = _phase2PenaltyBps;
currentPhase = 0;
lastPhaseUpdateBlock = 0;
totalPenaltyFeesCollected = 0;
}

Support

FAQs

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

Give us feedback!