function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
if (launchStartBlock == 0) revert PoolNotInitialized();
if (initialLiquidity == 0) {
uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
initialLiquidity = uint256(liquidity);
}
uint256 blocksSinceLaunch = block.number - launchStartBlock;
uint256 newPhase;
if (blocksSinceLaunch <= phase1Duration) {
newPhase = 1;
} else if (blocksSinceLaunch <= phase1Duration + phase2Duration) {
newPhase = 2;
} else {
newPhase = 3;
}
if (newPhase != currentPhase) {
_resetPerAddressTracking();
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
if (currentPhase == 3) {
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG);
}
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
);
}
The anti-bot protection is meant to prevent bots from dumping tokens immediately after launch, not to penalize buyers. Penalizing buyers is counterproductive - it discourages legitimate demand during the launch.
The test bellow clearly shows that both buys and sells are included for swapped amount tracking, and cooldown period tracking. This then leads to both contributing to users potentially paying penalties when they go past the specified limits.
pragma solidity ^0.8.26;
import "forge-std/console.sol";
import {Test, console2} from "forge-std/Test.sol";
import {TokenLaunchHook} from "../src/TokenLaunchHook.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol";
import {IUniswapV4Router04} from "lib/hookmate/src/interfaces/router/IUniswapV4Router04.sol";
import {AddressConstants} from "lib/hookmate/src/constants/AddressConstants.sol";
import {SwapParams, ModifyLiquidityParams} from "v4-core/types/PoolOperation.sol";
import {V4Router} from "v4-periphery/src/V4Router.sol";
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol";
import {ERC1155TokenReceiver} from "solmate/src/tokens/ERC1155.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId} from "v4-core/types/PoolId.sol";
import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {SqrtPriceMath} from "v4-core/libraries/SqrtPriceMath.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
import {StateLibrary} from "v4-core/libraries/StateLibrary.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/types/BeforeSwapDelta.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {Actions} from "v4-periphery/src/libraries/Actions.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
contract TestTokenLaunchHookFork is Test, ERC1155TokenReceiver {
address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
IPoolManager poolManager;
IPositionManager positionManager;
IUniswapV4Router04 swapRouter;
MockERC20 token;
TokenLaunchHook public antiBotHook;
PoolKey public key;
Currency ethCurrency = Currency.wrap(address(0));
Currency tokenCurrency;
address user1 = address(0x1);
address user2 = address(0x2);
address bot1 = address(0xB0B1);
address bot2 = address(0xB0B2);
address deployer = makeAddr("deployer");
uint160 constant SQRT_PRICE_1_1_s = 79228162514264337593543950336;
uint256 phase1Duration = 100;
uint256 phase2Duration = 100;
uint256 phase1LimitBps = 100;
uint256 phase2LimitBps = 500;
uint256 phase1Cooldown = 5;
uint256 phase2Cooldown = 2;
uint256 phase1PenaltyBps = 1000;
uint256 phase2PenaltyBps = 500;
string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
function setUp() public {
vm.createSelectFork(MAINNET_RPC_URL);
poolManager = IPoolManager(AddressConstants.getPoolManagerAddress(block.chainid));
positionManager = IPositionManager(payable(AddressConstants.getPositionManagerAddress(block.chainid)));
swapRouter = IUniswapV4Router04(payable(AddressConstants.getV4SwapRouterAddress(block.chainid)));
token = new MockERC20("TOKEN", "TKN", 18);
tokenCurrency = Currency.wrap(address(token));
token.mint(address(this), 1000 ether);
token.mint(user1, 1000 ether);
token.mint(user2, 1000 ether);
token.mint(bot1, 1000 ether);
token.mint(bot2, 1000 ether);
bytes memory creationCode = type(TokenLaunchHook).creationCode;
bytes memory constructorArgs = abi.encode(
poolManager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
uint160 flags = uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG);
(address hookAddress, bytes32 salt) = HookMiner.find(address(this), flags, creationCode, constructorArgs);
antiBotHook = new TokenLaunchHook{salt: salt}(
poolManager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
require(address(antiBotHook) == hookAddress, "Hook address mismatch");
token.approve(address(swapRouter), type(uint256).max);
token.approve(address(poolManager), type(uint256).max);
vm.prank(user1);
token.approve(address(swapRouter), type(uint256).max);
vm.prank(user2);
token.approve(address(swapRouter), type(uint256).max);
vm.prank(bot1);
token.approve(address(swapRouter), type(uint256).max);
vm.prank(bot2);
token.approve(address(swapRouter), type(uint256).max);
key = PoolKey({
currency0: ethCurrency,
currency1: tokenCurrency,
fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
tickSpacing: 60,
hooks: IHooks(address(antiBotHook))
});
vm.startPrank(deployer);
poolManager.initialize(key, SQRT_PRICE_1_1_s);
uint160 sqrtPriceAtTickUpper = TickMath.getSqrtPriceAtTick(60);
uint256 ethToAdd = 100 ether;
uint128 liquidityDelta =
LiquidityAmounts.getLiquidityForAmount0(SQRT_PRICE_1_1_s, sqrtPriceAtTickUpper, ethToAdd);
vm.deal(deployer, 200 ether);
token.mint(deployer, 2000 ether);
token.approve(PERMIT2, type(uint256).max);
IAllowanceTransfer(PERMIT2)
.approve(address(token), address(positionManager), type(uint160).max, type(uint48).max);
bytes memory actions = abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR));
bytes[] memory params = new bytes[](2);
params[0] = abi.encode(
key, -60, 60, uint256(liquidityDelta), type(uint256).max, type(uint256).max, deployer, bytes("")
);
params[1] = abi.encode(ethCurrency, tokenCurrency);
positionManager.modifyLiquidities{value: ethToAdd}(abi.encode(actions, params), block.timestamp + 60);
vm.stopPrank();
}
function test_Fork_BuysAndSellsIncreaseTrackedSwapAmount() public {
uint256 buyAmount = 0.1 ether;
uint256 sellAmount = 1 ether;
vm.deal(user1, buyAmount);
vm.startPrank(user1);
swapRouter.swap{value: buyAmount}(-int256(buyAmount), 0, true, key, bytes(""), user1, block.timestamp + 60);
vm.roll(block.number + phase1Cooldown + 1);
swapRouter.swap{value: 0}(-int256(sellAmount), 0, false, key, bytes(""), user1, block.timestamp + 60);
vm.stopPrank();
assertEq(antiBotHook.addressSwappedAmount(address(user1)), buyAmount + sellAmount);
}
}
+ // Add state variable to track which currency is the launch token per pool
+ mapping(PoolId => Currency) public launchToken;
function _afterInitialize(address, PoolKey calldata key, uint160, int24) internal override returns (bytes4) {
if (!key.fee.isDynamicFee()) {
revert MustUseDynamicFee();
}
+ PoolId id = key.toId();
- launchStartBlock = block.number;
+ launchStartBlock[id] = block.number;
uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
- initialLiquidity = uint256(liquidity);
+ initialLiquidity[id] = uint256(liquidity);
- currentPhase = 1;
+ currentPhase[id] = 1;
- lastPhaseUpdateBlock = block.number;
+ lastPhaseUpdateBlock[id] = block.number;
+ // Store the launch token - typically currency1 in an ETH/TOKEN pair
+ launchToken[id] = key.currency1;
return BaseHook.afterInitialize.selector;
}
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
+ PoolId id = key.toId();
- if (launchStartBlock == 0) revert PoolNotInitialized();
+ if (launchStartBlock[id] == 0) revert PoolNotInitialized();
+ // Determine if this is a sell of the launch token
+ // zeroForOne = true means selling currency0 for currency1
+ // zeroForOne = false means selling currency1 for currency0
+ Currency poolLaunchToken = launchToken[id];
+ bool isSellingLaunchToken;
+ if (Currency.unwrap(poolLaunchToken) == Currency.unwrap(key.currency0)) {
+ isSellingLaunchToken = params.zeroForOne; // Selling currency0
+ } else {
+ isSellingLaunchToken = !params.zeroForOne; // Selling currency1
+ }
+
+ // Only apply anti-bot protection to sells, not buys
+ if (!isSellingLaunchToken) {
+ return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG);
+ }
// ... rest of the function applies to sells only, accessing state via id