## Description
The `TokenLaunchHook` calculates swap limits based on a percentage of `initialLiquidity`. This variable is set via a "Lazy Load" mechanism inside `_beforeSwap`: if `initialLiquidity` is 0, it fetches the current pool liquidity and saves it.
**The Critical Flaw:**
This logic executes **only once**. Once `initialLiquidity` is set to a non-zero value, it is never updated again. The hook fails to account for any subsequent liquidity additions.
If a pool starts with low liquidity (e.g., 1 ETH) and later grows significantly (e.g., to 1,000 ETH), the swap limits will remain calculated based on the original 1 ETH. This effectively creates a "Permanent Throttle," locking the swap limit to a tiny amount forever, rendering the pool unusable as it scales.
```solidity
// @audit works for the first swap only. Never updates again.
if (initialLiquidity == 0) {
uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
initialLiquidity = uint256(liquidity);
}
```
## Risk
**Likelihood**: High (Certainty)
* Standard pool lifecycles involve adding liquidity over time. This logic guarantees the hook uses outdated data.
**Impact**: High
* **Protocol Broken / DoS:** As the pool grows, the effective swap limit (relative to total liquidity) shrinks toward 0%.
* **Example:**
* Initial: 10 ETH. Limit (1%) = 0.1 ETH.
* New Liquidity: +990 ETH. Total = 1,000 ETH.
* Actual Limit: Still 0.1 ETH.
* Correct Limit should be: 10 ETH.
* **Result:** Users can only trade 0.01% of the pool, making it functionally dead.
\
## Proof of Concept
Add this test to your `TestTokenLaunchHook` contract. It proves that adding massive liquidity does not increase the `initialLiquidity` variable.
```solidity
function test_PoC_StaleLiquidity_DoesNotUpdate() public {
// 1. SETUP: Start with small liquidity (defined in setUp, e.g., 10 ETH)
// 2. ACTION: Trigger the 'Lazy Load' with a tiny swap
// This locks 'initialLiquidity' to the starting amount (10 ETH)
vm.deal(user1, 1 ether);
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -100, // Tiny amount
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
vm.startPrank(user1);
swapRouter.swap{value: 100 wei}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
// Capture the locked value
uint256 lockedLiquidity = antiBotHook.initialLiquidity();
console.log("Liquidity Locked at:", lockedLiquidity);
// 3. ACTION: Add MASSIVE liquidity (1,000 ETH)
uint256 ethToAdd = 1000 ether;
// Calculate the liquidity delta for 1000 ETH at current price
uint160 sqrtPriceAtTickUpper = TickMath.getSqrtPriceAtTick(60);
uint128 liquidityDelta = LiquidityAmounts.getLiquidityForAmount0(
SQRT_PRICE_1_1,
sqrtPriceAtTickUpper,
ethToAdd
);
modifyLiquidityRouter.modifyLiquidity{value: ethToAdd}(
key,
ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: int256(uint256(liquidityDelta)),
salt: bytes32(0)
}),
ZERO_BYTES
);
// 4. ASSERTION: Verify the hook ignores the new liquidity
// We can check this by performing a swap that SHOULD be valid under new liquidity
// but fails under the old locked liquidity.
// Old Limit (1% of 10 ETH) = 0.1 ETH
// New Limit (1% of 1010 ETH) = 10.1 ETH
// We try to swap 1 ETH.
uint256 swapAmount = 1 ether;
params.amountSpecified = -int256(swapAmount);
// The hook doesn't revert, but it applies a penalty if over limit.
// We can inspect the public variable 'initialLiquidity' to prove it hasn't changed.
uint256 currentHookLiquidity = antiBotHook.initialLiquidity();
console.log("Current Pool Liquidity: ~1010 ETH");
console.log("Hook Stored Liquidity: ", currentHookLiquidity);
assertEq(currentHookLiquidity, lockedLiquidity, "FAIL: Hook did not update liquidity after addition");
assertLt(currentHookLiquidity, 100 ether, "FAIL: Hook is using stale data");
}
```
## Recommended Mitigation
Do not cache the liquidity in a state variable. Instead, fetch the **current** liquidity dynamically from the StateLibrary during every swap calculation. This ensures the limit is always 1% (or 3%) of the *actual* available liquidity.
```diff
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
if (launchStartBlock == 0) revert PoolNotInitialized();
// REMOVE THIS BLOCK
- if (initialLiquidity == 0) {
- uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
- initialLiquidity = uint256(liquidity);
- }
// ... phase calculation ...
// ADD THIS: Fetch current liquidity dynamically
+ uint256 currentLiquidity = uint256(StateLibrary.getLiquidity(poolManager, key.toId()));
// Update calculation to use currentLiquidity
- uint256 maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000;
+ uint256 maxSwapAmount = (currentLiquidity * phaseLimitBps) / 10000;
```
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.