Vanguard

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

`maxSwapAmount` depends on `initialLiquidity` and not on the actual liquidity at the time of swap

Author Revealed upon completion

Root + Impact

Description

https://github.com/CodeHawks-Contests/2026-01-vanguard/blob/main/src/TokenLaunchHook.sol#L159C34-L159C50

maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000;

Here maxSwapAmount is based solely on the initialLiquidity of the pool, but the liqudity of the pool will vary after each swap.
It means that maxSwapAmount should also vary accordingly to the liquidity available in the pool at the time of the swap.

With this code we have some issues :

1. If LPs add liquidity after launch

  • initialLiquidity is too low

  • maxSwapAmount is artificially low

=> excessive penalties

2. If liquidity is removed

  • initialLiquidity is too high

  • maxSwapAmount becomes overly permissive

=> limits can be bypassed
=> potential exploitation during phase 1 / 2

3. If the first swap happens before all intended liquidity is in place

This is the most critical case:

--> a bot triggers a micro-swap, initialLiquidity is locked in at a very low value.

All subsequent phases are permanently broken.

=> irreversible

Risk

Likelihood: High

Impact: High

Proof of Concept

Foundry Test :

// MockPoolManager.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract MockPoolManager {
uint128 public liquidity;
function setLiquidity(uint128 _liquidity) external {
liquidity = _liquidity;
}
}
// MockStateLibrary.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./MockPoolManager.sol";
library MockStateLibrary {
function getLiquidity(MockPoolManager manager, bytes32)
internal
view
returns (uint128)
{
return manager.liquidity();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/MockPoolManager.sol";
import "../src/TokenLaunchHook.sol";
contract InitialLiquidityPoC is Test {
MockPoolManager manager;
TokenLaunchHook hook;
address user = address(0xBEEF);
bytes32 constant POOL_ID = keccak256("POOL");
function setUp() external {
manager = new MockPoolManager();
hook = new VulnerableHook(manager);
}
function test_initialLiquidityFrozenAtFirstSwap() external {
// 1️. Pool starts with low liquidity
manager.setLiquidity(1_000); // 1k
// 2️. Bot/user triggers a micro swap
bool penalty1 =
hook.beforeSwap(user, POOL_ID, int256(1));
assertFalse(penalty1);
assertEq(hook.initialLiquidity(), 1_000);
// 3️. LPs add a lot of liquidity AFTER
manager.setLiquidity(1_000_000); // 1M
// 4️. User does a reasonable swap
// Expected if current liquidity used:
// max = 1% of 1_000_000 = 10_000
// But hook still uses 1% of 1_000 = 10
bool penalty2 =
hook.beforeSwap(user, POOL_ID, int256(500));
// Incorrect penalty applied
assertTrue(penalty2);
}
}

Recommended Mitigation

Use currentLiquidity instead of initialLiquidity :

uint256 currentLiquidity =
StateLibrary.getLiquidity(poolManager, key.toId());
uint256 maxSwapAmount =
(currentLiquidity * phaseLimitBps) / 10_000;

Support

FAQs

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

Give us feedback!