The penalty threshold no longer reflects an intended percentage of reserves, so legitimate swaps can be penalized too early or large swaps can avoid the intended penalty during launch phases.
The test creates an imbalanced ETH/USDC pool, derives token-based limits from pool state, and shows a material divergence from the liquidity-based limit used by the hook.
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {TokenLaunchHook} from "../src/TokenLaunchHook.sol";
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {SwapParams, ModifyLiquidityParams} from "v4-core/types/PoolOperation.sol";
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import {AmountHelpers} from "@uniswap/v4-core/test/utils/AmountHelpers.sol";
import {StateLibrary} from "v4-core/libraries/StateLibrary.sol";
contract Finding4_LiquidityMismatchReal is Test, Deployers {
using CurrencyLibrary for Currency;
MockERC20 token;
TokenLaunchHook public antiBotHook;
Currency ethCurrency = Currency.wrap(address(0));
Currency tokenCurrency;
uint256 constant PHASE1_DURATION = 100;
uint256 constant PHASE2_DURATION = 200;
uint256 constant PHASE1_LIMIT_BPS = 100;
uint256 constant PHASE2_LIMIT_BPS = 300;
uint256 constant PHASE1_COOLDOWN = 5;
uint256 constant PHASE2_COOLDOWN = 3;
uint256 constant PHASE1_PENALTY_BPS = 500;
uint256 constant PHASE2_PENALTY_BPS = 200;
function setUp() public {
deployFreshManagerAndRouters();
token = new MockERC20("USDC", "USDC", 6);
tokenCurrency = Currency.wrap(address(token));
token.mint(address(this), 1_000_000_000 * 1e6);
token.approve(address(swapRouter), type(uint256).max);
token.approve(address(modifyLiquidityRouter), type(uint256).max);
}
function test_LiquidityLimitMismatch_ForImbalancedPrice() public {
bytes memory creationCode = type(TokenLaunchHook).creationCode;
bytes memory constructorArgs = abi.encode(
manager, PHASE1_DURATION, PHASE2_DURATION,
PHASE1_LIMIT_BPS, PHASE2_LIMIT_BPS,
PHASE1_COOLDOWN, PHASE2_COOLDOWN,
PHASE1_PENALTY_BPS, PHASE2_PENALTY_BPS
);
uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG);
(address hookAddress, bytes32 salt) = HookMiner.find(address(this), flags, creationCode, constructorArgs);
antiBotHook = new TokenLaunchHook{salt: salt}(
manager, PHASE1_DURATION, PHASE2_DURATION,
PHASE1_LIMIT_BPS, PHASE2_LIMIT_BPS,
PHASE1_COOLDOWN, PHASE2_COOLDOWN,
PHASE1_PENALTY_BPS, PHASE2_PENALTY_BPS
);
require(address(antiBotHook) == hookAddress, "Hook address mismatch");
int24 targetTick = 90_000;
uint160 sqrtPriceX96 = TickMath.getSqrtPriceAtTick(targetTick);
(key,) = initPool(ethCurrency, tokenCurrency, antiBotHook, LPFeeLibrary.DYNAMIC_FEE_FLAG, sqrtPriceX96);
int24 tickLower = targetTick - 60;
int24 tickUpper = targetTick + 60;
uint256 amount0Desired = 10 ether;
uint256 amount1Desired = 100_000 * 1e6;
uint128 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(tickLower),
TickMath.getSqrtPriceAtTick(tickUpper),
amount0Desired,
amount1Desired
);
modifyLiquidityRouter.modifyLiquidity{value: amount0Desired}(
key,
ModifyLiquidityParams({
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: int256(uint256(liquidityDelta)),
salt: bytes32(0)
}),
ZERO_BYTES
);
uint256 initialLiquidity = uint256(StateLibrary.getLiquidity(manager, key.toId()));
uint256 maxSwapAmount = (initialLiquidity * PHASE1_LIMIT_BPS) / 10_000;
ModifyLiquidityParams memory rangeParams = ModifyLiquidityParams({
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: int256(uint256(liquidityDelta)),
salt: bytes32(0)
});
(uint256 amount0InPool, uint256 amount1InPool) = AmountHelpers.getMaxAmountInForPool(manager, rangeParams, key);
uint256 token1Limit = (amount1InPool * PHASE1_LIMIT_BPS) / 10_000;
uint256 largerLimit = maxSwapAmount > token1Limit ? maxSwapAmount : token1Limit;
uint256 smallerLimit = maxSwapAmount > token1Limit ? token1Limit : maxSwapAmount;
assertGt(amount0InPool, 0, "Token0 amount should be non-zero");
assertGt(amount1InPool, 0, "Token1 amount should be non-zero");
assertGt(largerLimit, smallerLimit * 3, "Liquidity-based limit should diverge materially from token1 limit");
}
}
Convert liquidity to token amounts at the current price and compare against the relevant token-side amount, or store limits in explicit token units.