LaunchFactoryV4

0x3240227a53a41da9d69608a38b6953619285354b

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
12,157 bytes
ABI entries
42
27 read · 4 write
License
none

Contract information

Address
0x3240227a53a41da9d69608a38b6953619285354b
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x16d9387b41…2c8658b8c3

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (27)

FACTORY_VERSION()uint256
FULL_TICK_LOWER()int24
FULL_TICK_UPPER()int24
LP_FEE()uint24
POST_GRAD_CREATOR_BPS()uint16
TICK_LOWER()int24
TICK_SPACING()int24
TICK_UPPER()int24
TOKEN_METADATA_ONCHAIN()bool
TOTAL_SUPPLY()uint256
computeTokenAddress(bytes32)address
graduationEth()uint256
hook()address
isLaunchToken(address)bool
isOgTicker(address)bool
isPool(address)bool
isSymbolTaken(string)bool
launchFee()uint256
launches(uint256)address, address, bytes32, uint256
launchesCount()uint256
locker()address
minDevBuy()uint256
poolKeyOf(address)tuple
poolManager()address
protocolTreasury()address
symbolToToken(string)address
tokenImpl()address

Events (2)

GraduatedLaunched

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "hook_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "locker_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "protocolTreasury_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "launchFee_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minDevBuy_",
        "type": "uint256"
      },
      {
        "internalType": "int24",
        "name": "tickLower_",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "tickUpper_",
        "type": "int24"
      },
      {
        "internalType": "uint256",
        "name": "graduationEth_",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadSymbol",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedDeployment",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeTransferFailed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientValue",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotGraduatable",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethToLp",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensToLp",
        "type": "uint256"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "devBuyEth",
        "type": "uint256"
      }
    ],
    "name": "Launched",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "FACTORY_VERSION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FULL_TICK_LOWER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FULL_TICK_UPPER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LP_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POST_GRAD_CREATOR_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_LOWER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_UPPER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOKEN_METADATA_ONCHAIN",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "computeTokenAddress",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "graduate",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethToLp",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokensToLp",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "graduationEth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "contract FoundryHookV4",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isLaunchToken",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "isOgTicker",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isPool",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      }
    ],
    "name": "isSymbolTaken",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "components": [
          {
            "internalType": "string",
            "name": "logo",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "description",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "website",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "twitter",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "telegram",
            "type": "string"
          }
        ],
        "internalType": "struct LaunchpadTokenV4.TokenMeta",
        "name": "meta",
        "type": "tuple"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "components": [
          {
            "internalType": "string",
            "name": "logo",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "description",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "website",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "twitter",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "telegram",
            "type": "string"
          }
        ],
        "internalType": "struct LaunchpadTokenV4.TokenMeta",
        "name": "meta",
        "type": "tuple"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "address[]",
        "name": "payoutWallets",
        "type": "address[]"
      },
      {
        "internalType": "uint16[]",
        "name": "payoutBps",
        "type": "uint16[]"
      }
    ],
    "name": "launchWithPayout",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "launches",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "launchBlock",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchesCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "locker",
    "outputs": [
      {
        "internalType": "contract FeeLockerV4",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minDevBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "poolKeyOf",
    "outputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolTreasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      }
    ],
    "name": "symbolToToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenImpl",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
import {SwapParams} from "v4-core/src/types/PoolOperation.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {CurrencySettler} from "v4-core/test/utils/CurrencySettler.sol";
import {LiquidityAmounts} from "v4-core/test/utils/LiquidityAmounts.sol";

import {LaunchpadTokenV4} from "./LaunchpadTokenV4.sol";
import {FoundryHookV4} from "./FoundryHookV4.sol";
import {FeeLockerV4} from "./FeeLockerV4.sol";

/// @title LaunchFactoryV4
/// @notice Launches a fixed-supply token straight into a LIVE Uniswap V4 pool on Robinhood Chain
/// (native-ETH / token, with the shared `FoundryHookV4` attached), so the coin is DexScreener-indexable
/// from block one. The whole 1B supply is placed as a single-sided position that is LOCKED FOREVER in
/// `FeeLockerV4`, and a MANDATORY dev buy is executed as the pool's very first swap in the launch tx —
/// that first swap is what gets the pool indexed immediately. This is the V4 analog of the V3
/// clone+curve factory; factories are versioned, never migrated.
///
/// @dev Launch pricing (all pools share these constants): the pool is initialized at TICK_UPPER (the
/// top of the range), where a single-sided position holds only currency1 (the token) — so the full
/// supply is deposited as token liquidity. Buys (ETH→token, zeroForOne, price falling) walk the price
/// DOWN from TICK_UPPER toward TICK_LOWER, releasing tokens and accumulating ETH. FDV = token price in
/// ETH × supply; it rises as the price falls in tick terms. With the constants below:
///   - launch FDV  (at TICK_UPPER 193440) ≈ 3.97 ETH
///   - ceiling FDV (at TICK_LOWER  60000) ≈ 2.47e6 ETH (far above any tier boundary)
/// The MC-scaled fee tiers live in the hook and are driven off the live pool price.
contract LaunchFactoryV4 is IUnlockCallback {
    using SafeERC20 for IERC20;
    using PoolIdLibrary for PoolKey;
    using CurrencyLibrary for Currency;
    using CurrencySettler for Currency;
    using BalanceDeltaLibrary for BalanceDelta;

    uint256 public constant FACTORY_VERSION = 4;
    // Signals that this factory clones tokens carrying NOXA-convention on-chain metadata getters
    // (`logo()`/`description()` + socials), so terminals (GMGN et al.) display the image with no
    // DexScreener profile submission. Indexers can key off this to skip off-chain metadata fetches.
    bool public constant TOKEN_METADATA_ONCHAIN = true;
    uint256 public constant TOTAL_SUPPLY = 1_000_000_000 ether;
    uint16 public constant POST_GRAD_CREATOR_BPS = 5000; // 50/50 LP-fee split in the locker

    // Shared pool geometry (native ETH = currency0, token = currency1). See the contract notice.
    // DYNAMIC-FEE pool: the fee rate is set per-swap by the hook's beforeSwap uint24 override (MC-scaled
    // 1.25%→1.00% pre-graduation, 0 after). Using v4's native dynamic LP fee — instead of a hook delta
    // skim — is what keeps the pool scanner-clean (no CRITICAL BEFORE_SWAP_RETURNS_DELTA permission). The
    // fee accrues to the locked LP position and is collected + split 50/50 by FeeLockerV4.collectFees.
    // 0x800000 = LPFeeLibrary.DYNAMIC_FEE_FLAG.
    uint24 public constant LP_FEE = 0x800000; // DYNAMIC_FEE_FLAG
    int24 public constant TICK_SPACING = 60;

    // Full-range ticks for the graduated (deep, locked-forever) position. Min/max usable ticks for
    // TICK_SPACING 60: floor(±887272 / 60) * 60 = ±887220.
    int24 public constant FULL_TICK_LOWER = -887220;
    int24 public constant FULL_TICK_UPPER = 887220;

    // ── Curve-shape knob (steepness), per-factory-version immutables ──
    // The single-sided curve spans [TICK_LOWER, TICK_UPPER]; the pool launches at TICK_UPPER (top) holding
    // only token1, and buys walk the price DOWN toward TICK_LOWER. Because the full 1B supply is deposited
    // across this range, TICK_UPPER sets the LAUNCH FDV (≈4 ETH at the 193440 default) and is the primary
    // steepness lever: a LOWER TICK_UPPER ⇒ lower launch FDV ⇒ a given ETH inflow produces a larger MC
    // multiple (steeper pump climb). TICK_LOWER sets the price ceiling the curve can reach. Both are set at
    // deploy so the coordinator can tune the feel without editing code (defaults preserve today's curve).
    int24 public immutable TICK_LOWER;
    int24 public immutable TICK_UPPER;
    // Accumulated real ETH in the pool at which a coin may `graduate()` (progress-bar denominator).
    uint256 public immutable graduationEth;

    // Per-factory-version immutables.
    IPoolManager public immutable poolManager;
    FoundryHookV4 public immutable hook;
    FeeLockerV4 public immutable locker;
    address public immutable protocolTreasury;
    address public immutable tokenImpl;
    uint256 public immutable launchFee; // routed to treasury (0.0005 ether for prod)
    uint256 public immutable minDevBuy; // mandatory minimum first-swap dev buy (0.002 ether default)

    struct Launch {
        address token;
        address creator;
        PoolId poolId;
        uint256 launchBlock;
    }

    // On-chain token metadata (logo/description/website/twitter/telegram) is bundled into
    // `LaunchpadTokenV4.TokenMeta` — a single struct arg on both `launch` and the token's `initialize`, so
    // the clone+init path stays a single stack frame (the flat 5-string form trips stack-too-deep without
    // via-IR). `metadataURI` (the Launched-event indexer pointer) is NOT part of it — it stays a top-level
    // `launch` arg because it is emitted, not stored on the token.

    Launch[] public launches;
    mapping(address => bool) public isPool; // launch token => registered (locker trusts nothing else)
    mapping(address => bool) public isLaunchToken;
    // OG REGISTRY: normalized-symbol key => the FIRST (OG) token ever launched with that ticker
    // (case-insensitive). A ticker may now launch any number of times; the first launch claims the
    // registry entry FOREVER and later duplicates never overwrite it. The app reads it via the
    // string-keyed `symbolToToken(string)` view and `isOgTicker(address)` below.
    mapping(bytes32 => address) internal _ogTokenByKey;

    event Launched(
        address indexed token, address indexed creator, PoolId indexed poolId, string metadataURI, uint256 devBuyEth
    );
    /// @notice The "it made it" moment: the curve position was re-ranged to a full-range, deep,
    /// locked-forever position. `ethToLp`/`tokensToLp` are what the re-ranged position now holds.
    event Graduated(PoolId indexed poolId, address indexed token, uint256 ethToLp, uint256 tokensToLp);

    error InsufficientValue();
    error FeeTransferFailed();
    error BadSymbol();
    error NotPoolManager();
    error NotGraduatable();

    /// @dev The Locker<->Factory circular reference is resolved exactly like V3 (DeploySystemV3): the
    /// script deploys the locker FIRST with the PREDICTED factory address, then deploys this factory
    /// with the real locker address, and asserts both match. Both stay fully immutable — no setter. The
    /// token implementation is deployed here (self-locks via `_disableInitializers`), so it consumes the
    /// factory's own nonce and matches this factory version.
    constructor(
        address poolManager_,
        address hook_,
        address locker_,
        address protocolTreasury_,
        uint256 launchFee_,
        uint256 minDevBuy_,
        int24 tickLower_,
        int24 tickUpper_,
        uint256 graduationEth_
    ) {
        require(tickLower_ < tickUpper_, "bad ticks");
        require(tickLower_ % TICK_SPACING == 0 && tickUpper_ % TICK_SPACING == 0, "unaligned ticks");
        require(graduationEth_ > 0, "bad graduationEth");
        poolManager = IPoolManager(poolManager_);
        hook = FoundryHookV4(payable(hook_));
        locker = FeeLockerV4(payable(locker_));
        protocolTreasury = protocolTreasury_;
        launchFee = launchFee_;
        minDevBuy = minDevBuy_;
        TICK_LOWER = tickLower_;
        TICK_UPPER = tickUpper_;
        graduationEth = graduationEth_;
        tokenImpl = address(new LaunchpadTokenV4());
    }

    /// @notice Launch a new token + live V4 pool. `metadataURI` is EMITTED in `Launched` (indexers read it,
    /// unchanged from before). `meta` (logo + description + website/twitter/telegram) is written ON-CHAIN
    /// into the cloned token's NOXA-convention getters so terminals display the image AND the full socials
    /// with no off-chain submission. Any `meta` field may be empty. Requires `msg.value >= launchFee +
    /// minDevBuy`; everything above `launchFee` is spent as the creator's mandatory first-swap dev buy.
    function launch(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        LaunchpadTokenV4.TokenMeta calldata meta,
        bytes32 salt
    ) external payable returns (address token, PoolId poolId) {
        // Default payout (empty arrays → 100% to the creator). Signature/selector (0x48698c80) is
        // UNCHANGED — the app's byte-verified launch encoder keeps working exactly as before.
        return _launchCore(name, symbol, metadataURI, meta, salt, new address[](0), new uint16[](0));
    }

    /// @notice Single-signature launch WITH a creator-fee split applied in the SAME tx. Identical to
    /// `launch` plus `payoutWallets`/`payoutBps` (bps summing to 10000, <= 10 wallets, no zero address).
    /// The split is forwarded to the hook AT REGISTRATION, so a creator who dedicates fees signs ONCE
    /// (no separate `setCreatorPayout` tx). The hook runs the EXACT same validation + non-creator floor
    /// ratchet as `setCreatorPayout`, applied one-shot (registration is factory-only and reverts on any
    /// second call), so the factory can never alter payouts after launch.
    function launchWithPayout(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        LaunchpadTokenV4.TokenMeta calldata meta,
        bytes32 salt,
        address[] calldata payoutWallets,
        uint16[] calldata payoutBps
    ) external payable returns (address token, PoolId poolId) {
        return _launchCore(name, symbol, metadataURI, meta, salt, payoutWallets, payoutBps);
    }

    /// @dev Shared launch core. `payoutWallets`/`payoutBps` empty → default [creator=10000]; non-empty →
    /// the single-signature creator-fee split, forwarded to the hook at registration.
    function _launchCore(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        LaunchpadTokenV4.TokenMeta calldata meta,
        bytes32 salt,
        address[] memory payoutWallets,
        uint16[] memory payoutBps
    ) internal returns (address token, PoolId poolId) {
        if (msg.value < launchFee + minDevBuy) revert InsufficientValue();

        // Validate the symbol (rejects empty/overlong) and derive its case-insensitive registry key. A
        // duplicate ticker is now ALLOWED — we no longer revert on an already-claimed symbol; the key is
        // only used to record the OG (first) token below.
        bytes32 sk = _symbolKey(symbol);

        // Clone the token at the vanity address and mint the whole fixed supply to THIS factory (the
        // factory funds the initial liquidity, then transfers it to the locker). The on-chain logo,
        // description and socials are baked into the clone here and are immutable thereafter. Split into a
        // helper so the many string args live in their own stack frame (avoids stack-too-deep without
        // via-IR, keeping the rest of the audited build unchanged).
        token = _cloneAndInit(name, symbol, metadataURI, meta, salt);

        PoolKey memory key = _poolKey(token);
        poolId = key.toId();

        // Compute the liquidity for the full supply as single-sided token1 (curve position geometry).
        uint128 liquidity = LiquidityAmounts.getLiquidityForAmount1(
            TickMath.getSqrtPriceAtTick(TICK_LOWER), TickMath.getSqrtPriceAtTick(TICK_UPPER), TOTAL_SUPPLY
        );

        // Register creator/config + curve geometry + graduation threshold with the hook BEFORE
        // initializing the pool (registration is factory-only; unregistered pools carrying this hook are
        // inert), then apply the OPTIONAL single-signature fee split one-shot via the factory-only
        // `setInitialPayout` (before the dev-buy first trade). Isolated in a helper so the payout arrays
        // live in their own stack frame (keeps the launch core under the stack limit without via-IR). The
        // hook reads the geometry to report accumulated ETH / graduation progress off slot0.
        _registerPool(key, liquidity, payoutWallets, payoutBps);

        // Initialize the pool at the top of the range (single-sided token liquidity).
        poolManager.initialize(key, TickMath.getSqrtPriceAtTick(TICK_UPPER));

        // Lock the full-supply single-sided position forever.
        IERC20(token).safeTransfer(address(locker), TOTAL_SUPPLY);
        locker.lockLiquidity(
            key, TICK_LOWER, TICK_UPPER, liquidity, token, msg.sender, protocolTreasury, POST_GRAD_CREATOR_BPS
        );

        isPool[token] = true;
        isLaunchToken[token] = true;
        // OG registry: the FIRST launch of this ticker claims it forever; later duplicates never overwrite.
        if (_ogTokenByKey[sk] == address(0)) _ogTokenByKey[sk] = token;
        launches.push(Launch({token: token, creator: msg.sender, poolId: poolId, launchBlock: block.number}));

        // Route the launch fee to the treasury.
        if (launchFee > 0) {
            (bool ok,) = protocolTreasury.call{value: launchFee}("");
            if (!ok) revert FeeTransferFailed();
        }

        // Mandatory dev buy = the pool's first swap, executed in this tx (what gets it indexed).
        uint256 devBuyEth = msg.value - launchFee;
        poolManager.unlock(abi.encode(key, msg.sender, devBuyEth));

        emit Launched(token, msg.sender, poolId, metadataURI, devBuyEth);
    }

    /// @dev Forwards registration (creator + geometry + graduation) to the hook, then applies the OPTIONAL
    /// single-signature fee split via the factory-only, one-shot `setInitialPayout` (skipped when the
    /// arrays are empty → the pool keeps the default [creator @ 10000] seeded by `registerPool`). Both are
    /// external calls in the SAME launch tx, BEFORE the dev-buy first trade. `msg.sender` is preserved
    /// across the internal call, so the hook records the creator correctly. The payout arrays are `memory`
    /// here and ABI-encoded into the hook's calldata by the external call.
    function _registerPool(
        PoolKey memory key,
        uint128 liquidity,
        address[] memory payoutWallets,
        uint16[] memory payoutBps
    ) internal {
        hook.registerPool(key, msg.sender, TICK_LOWER, TICK_UPPER, liquidity, graduationEth);
        if (payoutWallets.length > 0) {
            hook.setInitialPayout(key.toId(), payoutWallets, payoutBps);
        }
    }

    /// @notice PoolManager unlock callback: executes the dev buy (exact-input ETH→token) and forwards the
    /// bought tokens to the creator. The hook skims its 1% fee during this swap like any other.
    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        (PoolKey memory key, address creator, uint256 devBuyEth) = abi.decode(data, (PoolKey, address, uint256));

        BalanceDelta delta = poolManager.swap(
            key,
            SwapParams({
                zeroForOne: true,
                amountSpecified: -int256(devBuyEth), // exact input ETH
                sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
            }),
            ""
        );

        // Settle the ETH owed (currency0 debit) and forward the token bought (currency1 credit) to creator.
        if (delta.amount0() < 0) {
            key.currency0.settle(poolManager, address(this), uint256(uint128(-delta.amount0())), false);
        }
        if (delta.amount1() > 0) {
            poolManager.take(key.currency1, creator, uint256(uint128(delta.amount1())));
        }
        return "";
    }

    /// @dev Clones the token impl at `salt` and one-shot-initializes it (name/symbol + the full on-chain
    /// metadata: logo/description/website/twitter/telegram), minting the full supply to this factory.
    /// Isolated in its own frame to keep `launch` under the stack limit.
    function _cloneAndInit(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        LaunchpadTokenV4.TokenMeta calldata meta,
        bytes32 salt
    ) internal returns (address token) {
        // Deploy each coin as an EIP-1167 minimal-proxy CLONE of the (verified) `tokenImpl`, CREATE2 so the
        // address is deterministic. WHY a clone, not a full contract: Blockscout auto-recognizes the 45-byte
        // EIP-1167 proxy and links it to the implementation, so a clone of a VERIFIED impl is marked verified
        // INSTANTLY at deploy — no per-coin verification, no compile, no propagation gap. That gap was the
        // whole problem: a full contract is only twin-matched at first (which Serialized rejects as "closed
        // source"), so terminals read it as unsafe in the seconds after launch and CACHE that verdict. A clone
        // is born verified, so Serialized's first read is verified+safe (proven: the clean RH launchpad coins
        // are eip1167 clones of a verified impl). tokenURI()/logo() still work — the clone delegatecalls to the
        // impl, reading the clone's OWN storage — so the on-chain metadata getters are unaffected.
        token = Clones.cloneDeterministic(tokenImpl, salt);
        // The launch metadataURI is stored on the token and served by `tokenURI()` (terminal image
        // convention); it is the SAME pointer emitted in the `Launched` event.
        LaunchpadTokenV4(token).initialize(name, symbol, address(this), meta, metadataURI);
    }

    // ─────────────────────────── graduation ───────────────────────────

    /// @notice Permissionless once the pool's accumulated real ETH reaches `graduationEth`. Atomically:
    /// (1) flips the hook's `graduated` flag — which switches the trade fee to the post-grad schedule
    /// (zero hook fee; the pool's own 30-bps LP fee then applies) AND lifts the outside-LP block — then
    /// (2) tells the locker to RE-RANGE the single-sided curve position into a full-range, deep position
    /// that stays locked forever. Exactly-once: the hook's `markGraduated` and the locker's re-range each
    /// enforce it independently. No bounty is paid — all trade ETH is inside the locked position (there is
    /// no free ETH pot to draw one from), and the call is cheap enough for the app/keeper to trigger.
    ///
    /// @dev Ordering matters: `markGraduated` runs FIRST so that when the locker's re-range adds the
    /// full-range liquidity, the (now-lifted) outside-LP block permits it.
    function graduate(address token) external returns (uint256 ethToLp, uint256 tokensToLp) {
        if (!isLaunchToken[token]) revert NotGraduatable();
        PoolKey memory key = _poolKey(token);
        PoolId poolId = key.toId();
        if (!hook.graduationReady(poolId)) revert NotGraduatable();

        hook.markGraduated(poolId); // flips fee schedule + lifts LP block (exactly-once in the hook)
        (ethToLp, tokensToLp) = locker.graduateRerange(token, FULL_TICK_LOWER, FULL_TICK_UPPER);

        emit Graduated(poolId, token, ethToLp, tokensToLp);
    }

    // ─────────────────────────── views / helpers ───────────────────────────

    function _poolKey(address token) internal view returns (PoolKey memory) {
        return PoolKey({
            currency0: CurrencyLibrary.ADDRESS_ZERO, // native ETH (address(0) < any token)
            currency1: Currency.wrap(token),
            fee: LP_FEE,
            tickSpacing: TICK_SPACING,
            hooks: IHooks(address(hook))
        });
    }

    function poolKeyOf(address token) external view returns (PoolKey memory) {
        return _poolKey(token);
    }

    function computeTokenAddress(bytes32 salt) public view returns (address) {
        // Must match the salted `Clones.cloneDeterministic(tokenImpl, salt)` above: CREATE2 over the EIP-1167
        // proxy bytecode for `tokenImpl`, deployed by this factory.
        return Clones.predictDeterministicAddress(tokenImpl, salt, address(this));
    }

    function launchesCount() external view returns (uint256) {
        return launches.length;
    }

    /// @notice OG registry read for the app: the FIRST (OG) token ever launched with `symbol`
    /// (case-insensitive), or address(0) if the ticker has never launched. Later duplicate launches of the
    /// same symbol do NOT change this — it always resolves to the original.
    function symbolToToken(string calldata symbol) external view returns (address) {
        return _ogTokenByKey[_symbolKey(symbol)];
    }

    /// @notice True iff `token` is the OG (first-ever) launch of its ticker (case-insensitive). Later
    /// duplicate launches of the same symbol return false. Uses the SAME symbol normalization as the
    /// registry (via `_symbolKey`), so OG determination is consistent with `symbolToToken`.
    function isOgTicker(address token) external view returns (bool) {
        if (!isLaunchToken[token]) return false;
        return _ogTokenByKey[_symbolKey(LaunchpadTokenV4(token).symbol())] == token;
    }

    /// @notice Whether a ticker has EVER launched (an OG exists). A duplicate ticker is allowed to launch
    /// regardless; this only reports whether the registry entry is already claimed.
    function isSymbolTaken(string calldata symbol) external view returns (bool) {
        return _ogTokenByKey[_symbolKey(symbol)] != address(0);
    }

    /// @dev Case-insensitive ticker key: ASCII a-z uppercased, then hashed. Rejects empty and >32-byte
    /// symbols (identical to LaunchFactoryV3). Takes `memory` so both calldata callers (launch/views) and
    /// the on-chain `token.symbol()` read in `isOgTicker` share the EXACT same normalization.
    function _symbolKey(string memory symbol) internal pure returns (bytes32) {
        bytes memory b = bytes(symbol);
        uint256 len = b.length;
        if (len == 0 || len > 32) revert BadSymbol();
        for (uint256 i = 0; i < len; i++) {
            if (b[i] >= 0x61 && b[i] <= 0x7a) b[i] = bytes1(uint8(b[i]) - 32);
        }
        return keccak256(b);
    }
}
Chain explorer4714msChain node83ms