Fair Launch
What it does
- The fee starts at 25% when the pool opens and falls in a straight line to zero over 10 minutes.
- A wallet that just swapped in this pool waits 30 seconds before its next swap.
- Liquidity added to the pool cannot be removed for 1 day after the pool opens.
Anti-snipe feeCooldownLiquidity lock
src/FairLaunch.sol, as verified on Sourcify
// SPDX-License-Identifier: MIT pragma solidity 0.8.26; // FairLaunch: a Uniswap v4 hook, written by v4 Builder (v4builder/2) from the recipe below. // Callbacks: afterInitialize, beforeRemoveLiquidity, beforeSwap. Its address carries them in its low bits. // Open its pools with the dynamic fee flag (0x800000). Every swap pays at least 0.3%. // // - The fee starts at 25% when the pool opens and falls in a straight line to zero over 10 minutes. // - A wallet that just swapped in this pool waits 30 seconds before its next swap. // - Liquidity added to the pool cannot be removed for 1 day after the pool opens. import {BaseHook} from "@openzeppelin/uniswap-hooks/src/base/BaseHook.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {SwapParams, ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; contract FairLaunch is BaseHook { using PoolIdLibrary for PoolKey; using LPFeeLibrary for uint24; uint24 public constant BASE_FEE = 3000; // 0.3%, the floor of every swap's fee uint256 public constant LAUNCH_FEE = 250000; // 25% at the open uint256 public constant LAUNCH_WINDOW = 600; // seconds for it to fall to zero uint256 public constant COOLDOWN = 30; // seconds between two swaps of one wallet uint256 public constant LP_LOCK = 86400; // 1 days after the open uint8 internal constant COOLING_DOWN = 1; struct PoolState { uint64 openedAt; } mapping(PoolId => PoolState) public pools; mapping(PoolId => mapping(address => uint256)) public lastSwapAt; error CoolingDown(uint256 until); error NeedsDynamicFee(); error LiquidityLocked(uint256 until); constructor(IPoolManager poolManager_) BaseHook(poolManager_) { } function getHookPermissions() public pure override returns (Hooks.Permissions memory) { return Hooks.Permissions({ beforeInitialize: false, afterInitialize: true, beforeAddLiquidity: false, afterAddLiquidity: false, beforeRemoveLiquidity: true, afterRemoveLiquidity: false, beforeSwap: true, afterSwap: false, beforeDonate: false, afterDonate: false, beforeSwapReturnDelta: false, afterSwapReturnDelta: false, afterAddLiquidityReturnDelta: false, afterRemoveLiquidityReturnDelta: false }); } function _afterInitialize(address, PoolKey calldata key, uint160, int24) internal override returns (bytes4) { if (!key.fee.isDynamicFee()) revert NeedsDynamicFee(); PoolId id = key.toId(); PoolState storage s = pools[id]; s.openedAt = uint64(block.timestamp); return this.afterInitialize.selector; } function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata) internal override returns (bytes4, BeforeSwapDelta, uint24) { (uint24 fee, uint8 rebate, uint8 refused) = _rules(key.toId(), tx.origin, params.zeroForOne); if (refused == COOLING_DOWN) revert CoolingDown(lastSwapAt[key.toId()][tx.origin] + COOLDOWN); uint256 f = fee > BASE_FEE ? fee : BASE_FEE; if (rebate > 0) f = f * (100 - rebate) / 100; return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, uint24(f) | LPFeeLibrary.OVERRIDE_FEE_FLAG); } function _beforeRemoveLiquidity(address, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata) internal view override returns (bytes4) { uint256 until = pools[key.toId()].openedAt + LP_LOCK; if (block.timestamp < until) revert LiquidityLocked(until); return this.beforeRemoveLiquidity.selector; } /// The rules, in order: the refusals first (nothing is written when a swap is refused), /// then the bookkeeping, then the fee proposals (the highest wins) and the rebates (the largest wins). function _rules(PoolId id, address origin, bool) internal returns (uint24, uint8, uint8) { uint24 fee; PoolState storage s = pools[id]; uint256 last = lastSwapAt[id][origin]; if (last != 0 && block.timestamp < last + COOLDOWN) return (0, 0, COOLING_DOWN); lastSwapAt[id][origin] = block.timestamp; { uint256 elapsed = block.timestamp - s.openedAt; if (elapsed < LAUNCH_WINDOW) { uint256 f = LAUNCH_FEE * (LAUNCH_WINDOW - elapsed) / LAUNCH_WINDOW; if (f > fee) fee = uint24(f); } } return (fee, 0, 0); } }
The recipe, as written in the factory's event
{"v":1,"g":"v4builder/2","name":"Fair Launch","target":"hook","baseFee":3000,"modules":[{"id":"launchFee","p":{"startFee":250000,"duration":600}},{"id":"cooldown","p":{"seconds":30}},{"id":"lpLock","p":{"days":1}}]}Start from this recipe