Building a v4 hook? Pick what it does, set the numbers, and deploy it as your own hook or stacked on v4 Marketplace. Open the builder ↗

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 30 days 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/1) 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 30 days 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 = 2592000; // 30 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,"name":"Fair Launch","target":"hook","baseFee":3000,"modules":[{"id":"launchFee","p":{"startFee":250000,"duration":600}},{"id":"cooldown","p":{"seconds":30}},{"id":"lpLock","p":{"days":30}}]}
Start from this recipe