LaunchHoodV3Factory

0x62b33a039d289cbda50ebeb72fe4261449e61bcf

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
15,739 bytes
ABI entries
50
14 read · 10 write
License
none

Contract information

Address
0x62b33a039d289cbda50ebeb72fe4261449e61bcf
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x42a2aba7e8…2016f8d436

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (14)

LOCKER()address
PROTOCOL_TREASURY()address
TOKEN_IMPL()address
dexConfigCount()uint256
getDexConfig(uint256)tuple
getLaunchConfig(uint256)tuple
getLaunchedToken(address)tuple
isLaunchHoodToken(address)bool
launchConfigCount()uint256
launchEnabled()bool
launchFee()uint256
owner()address
predictTokenAddress(address, bytes32)address
whitelistedLaunchers(address)bool

Events (10)

DexConfigAddedDexConfigStatusUpdatedLaunchConfigAddedLaunchConfigUpdatedLaunchEnabledUpdatedLaunchFeeUpdatedOwnershipTransferredTokenDeployedTokenLaunchedWhitelistedLauncherUpdated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenImpl_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "contract LaunchHoodV3Locker",
        "name": "locker_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "launchFee_",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "ConfigDisabled",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedDeployment",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeSendFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InitialBuyNeedsWethPair",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientLaunchFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidConfig",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LaunchDisabled",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolAlreadyInitializedAtWrongPrice",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TickMisaligned",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownConfig",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      }
    ],
    "name": "DexConfigAdded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      }
    ],
    "name": "DexConfigStatusUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "configId",
        "type": "uint256"
      }
    ],
    "name": "LaunchConfigAdded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "configId",
        "type": "uint256"
      }
    ],
    "name": "LaunchConfigUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      }
    ],
    "name": "LaunchEnabledUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "LaunchFeeUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "deployer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pairToken",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "launchConfigId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      }
    ],
    "name": "TokenDeployed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "deployer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "pairToken",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "launchConfigId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "restrictionEndBlock",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "initialBuyEth",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "initialBuyTokens",
        "type": "uint256"
      }
    ],
    "name": "TokenLaunched",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "launcher",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "whitelisted",
        "type": "bool"
      }
    ],
    "name": "WhitelistedLauncherUpdated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "LOCKER",
    "outputs": [
      {
        "internalType": "contract LaunchHoodV3Locker",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PROTOCOL_TREASURY",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOKEN_IMPL",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "contract IUniswapV3FactoryMinimal",
            "name": "dexFactory",
            "type": "address"
          },
          {
            "internalType": "contract INonfungiblePositionManagerMinimal",
            "name": "positionManager",
            "type": "address"
          },
          {
            "internalType": "contract ISwapRouter02Minimal",
            "name": "swapRouter",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "weth9",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "bool",
            "name": "enabled",
            "type": "bool"
          },
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          }
        ],
        "internalType": "struct LaunchHoodV3Factory.DexConfig",
        "name": "d",
        "type": "tuple"
      }
    ],
    "name": "addDexConfig",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "pairToken",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "totalSupply",
            "type": "uint256"
          },
          {
            "internalType": "int24",
            "name": "initialTick",
            "type": "int24"
          },
          {
            "internalType": "uint16",
            "name": "maxWalletBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "maxTxBps",
            "type": "uint16"
          },
          {
            "internalType": "uint32",
            "name": "restrictionBlocks",
            "type": "uint32"
          },
          {
            "internalType": "bool",
            "name": "enabled",
            "type": "bool"
          }
        ],
        "internalType": "struct LaunchHoodV3Factory.LaunchConfig",
        "name": "c",
        "type": "tuple"
      }
    ],
    "name": "addLaunchConfig",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "configId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "dexConfigCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      }
    ],
    "name": "getDexConfig",
    "outputs": [
      {
        "components": [
          {
            "internalType": "contract IUniswapV3FactoryMinimal",
            "name": "dexFactory",
            "type": "address"
          },
          {
            "internalType": "contract INonfungiblePositionManagerMinimal",
            "name": "positionManager",
            "type": "address"
          },
          {
            "internalType": "contract ISwapRouter02Minimal",
            "name": "swapRouter",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "weth9",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "bool",
            "name": "enabled",
            "type": "bool"
          },
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          }
        ],
        "internalType": "struct LaunchHoodV3Factory.DexConfig",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "configId",
        "type": "uint256"
      }
    ],
    "name": "getLaunchConfig",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "pairToken",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "totalSupply",
            "type": "uint256"
          },
          {
            "internalType": "int24",
            "name": "initialTick",
            "type": "int24"
          },
          {
            "internalType": "uint16",
            "name": "maxWalletBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "maxTxBps",
            "type": "uint16"
          },
          {
            "internalType": "uint32",
            "name": "restrictionBlocks",
            "type": "uint32"
          },
          {
            "internalType": "bool",
            "name": "enabled",
            "type": "bool"
          }
        ],
        "internalType": "struct LaunchHoodV3Factory.LaunchConfig",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "getLaunchedToken",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "deployer",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "pool",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "pairToken",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "positionId",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "launchConfigId",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "dexId",
            "type": "uint256"
          }
        ],
        "internalType": "struct LaunchHoodV3Factory.LaunchedToken",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "isLaunchHoodToken",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchConfigCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchEnabled",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "metadataURI",
            "type": "string"
          },
          {
            "internalType": "address",
            "name": "rewardRecipient",
            "type": "address"
          }
        ],
        "internalType": "struct LaunchHoodV3Factory.TokenParams",
        "name": "p",
        "type": "tuple"
      },
      {
        "internalType": "uint256",
        "name": "configId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "userSalt",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      }
    ],
    "name": "launchToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "userSalt",
        "type": "bytes32"
      }
    ],
    "name": "predictTokenAddress",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      }
    ],
    "name": "setDexStatus",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      }
    ],
    "name": "setLaunchEnabled",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "setLaunchFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "launcher",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "whitelisted",
        "type": "bool"
      }
    ],
    "name": "setWhitelistedLauncher",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "configId",
        "type": "uint256"
      },
      {
        "components": [
          {
            "internalType": "address",
            "name": "pairToken",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "totalSupply",
            "type": "uint256"
          },
          {
            "internalType": "int24",
            "name": "initialTick",
            "type": "int24"
          },
          {
            "internalType": "uint16",
            "name": "maxWalletBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "maxTxBps",
            "type": "uint16"
          },
          {
            "internalType": "uint32",
            "name": "restrictionBlocks",
            "type": "uint32"
          },
          {
            "internalType": "bool",
            "name": "enabled",
            "type": "bool"
          }
        ],
        "internalType": "struct LaunchHoodV3Factory.LaunchConfig",
        "name": "c",
        "type": "tuple"
      }
    ],
    "name": "updateLaunchConfig",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "whitelistedLaunchers",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

// =============================================================================
//   launchhood.fun — memecoin launchpad on Ethereum
//   https://launchhood.fun   ·   https://x.com/LaunchHoodfun
// =============================================================================

import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import {TickMath} from "v4-core/libraries/TickMath.sol";

import {LaunchHoodV3Token} from "./LaunchHoodV3Token.sol";
import {LaunchHoodV3Locker} from "./LaunchHoodV3Locker.sol";
import {
    IUniswapV3FactoryMinimal,
    IUniswapV3PoolMinimal,
    INonfungiblePositionManagerMinimal,
    ISwapRouter02Minimal
} from "./interfaces/IUniswapV3.sol";

/// @title LaunchHoodV3Factory
/// @notice Second-generation launchhood.fun factory: tokens launch DIRECTLY
///         into a Uniswap V3 pool — no bonding-curve phase, no graduation.
///
///         launchToken() in one tx:
///           1. clones the ERC20 (full supply minted to this factory),
///           2. creates + initializes the V3 pool at the configured tick,
///           3. mints a single-sided full-supply position (all tokens, zero
///              pair) straight to the permanent locker,
///           4. sends the flat launch fee to the treasury,
///           5. spends the remaining msg.value as the creator's initial buy,
///           6. activates the token's anti-snipe caps for the restriction
///              window.
///
///         Launch parameters live in owner-managed LaunchConfig / DexConfig
///         arrays, so pricing, caps, and DEX wiring are tunable without
///         redeploying.
contract LaunchHoodV3Factory is ReentrancyGuard, Ownable {
    using SafeERC20 for IERC20;

    address public immutable TOKEN_IMPL;
    address public immutable PROTOCOL_TREASURY;
    LaunchHoodV3Locker public immutable LOCKER;

    struct DexConfig {
        IUniswapV3FactoryMinimal dexFactory;
        INonfungiblePositionManagerMinimal positionManager;
        ISwapRouter02Minimal swapRouter;
        address weth9;
        uint24 fee;
        int24 tickSpacing;
        bool enabled;
        string name;
    }

    struct LaunchConfig {
        /// @dev Token the pool is quoted against. Must be the DEX's WETH9
        ///      when a launch includes an initial buy (the buy is paid in ETH).
        address pairToken;
        uint256 totalSupply;
        /// @dev Initial pool tick expressed as if the launched token were
        ///      token0 (i.e. negative for a sub-1:1 starting price, e.g.
        ///      -204200 ≈ 1.37 WETH FDV on a 1B supply). The factory flips
        ///      the sign automatically when the token sorts after pairToken.
        int24 initialTick;
        uint16 maxWalletBps;      // anti-snipe wallet cap; 0 or 10000 = none
        uint16 maxTxBps;          // anti-snipe per-tx cap; 0 or 10000 = none
        uint32 restrictionBlocks; // cap duration in blocks after launch
        bool enabled;
    }

    struct TokenParams {
        string name;
        string symbol;
        string metadataURI;
        /// @dev Optional address that earns the creator LP-fee share.
        ///      address(0) → msg.sender.
        address rewardRecipient;
    }

    struct LaunchedToken {
        address deployer;
        address pool;
        address pairToken;
        uint256 positionId;
        uint256 launchConfigId;
        uint256 dexId;
    }

    DexConfig[] internal _dexConfigs;
    LaunchConfig[] internal _launchConfigs;

    mapping(address token => LaunchedToken) internal _launchedTokens;

    /// @notice Flat per-launch fee (wei), paid out of msg.value to treasury.
    uint256 public launchFee;
    /// @notice Global kill-switch for new launches (whitelist bypasses it).
    bool public launchEnabled;
    mapping(address => bool) public whitelistedLaunchers;

    event LaunchEnabledUpdated(bool enabled);
    event LaunchFeeUpdated(uint256 fee);
    event WhitelistedLauncherUpdated(address indexed launcher, bool whitelisted);
    event DexConfigAdded(uint256 indexed dexId, string name);
    event DexConfigStatusUpdated(uint256 indexed dexId, bool enabled);
    event LaunchConfigAdded(uint256 indexed configId);
    event LaunchConfigUpdated(uint256 indexed configId);

    event TokenDeployed(
        address indexed token,
        address indexed deployer,
        address indexed pairToken,
        uint256 dexId,
        uint256 launchConfigId,
        string name,
        string symbol,
        string metadataURI
    );

    event TokenLaunched(
        address indexed token,
        address indexed deployer,
        address indexed pool,
        address pairToken,
        uint256 dexId,
        uint256 launchConfigId,
        uint256 positionId,
        uint256 restrictionEndBlock,
        uint256 initialBuyEth,
        uint256 initialBuyTokens
    );

    error LaunchDisabled();
    error UnknownConfig();
    error ConfigDisabled();
    error InsufficientLaunchFee();
    error TickMisaligned();
    error PoolAlreadyInitializedAtWrongPrice();
    error InvalidConfig();
    error InitialBuyNeedsWethPair();
    error FeeSendFailed();

    constructor(
        address tokenImpl_,
        address treasury_,
        LaunchHoodV3Locker locker_,
        uint256 launchFee_
    ) Ownable(msg.sender) {
        TOKEN_IMPL = tokenImpl_;
        PROTOCOL_TREASURY = treasury_;
        LOCKER = locker_;
        launchFee = launchFee_;
        launchEnabled = true;
    }

    // -------------------------------------------------------------------------
    // Admin
    // -------------------------------------------------------------------------

    function setLaunchEnabled(bool enabled) external onlyOwner {
        launchEnabled = enabled;
        emit LaunchEnabledUpdated(enabled);
    }

    function setLaunchFee(uint256 fee) external onlyOwner {
        launchFee = fee;
        emit LaunchFeeUpdated(fee);
    }

    function setWhitelistedLauncher(address launcher, bool whitelisted) external onlyOwner {
        whitelistedLaunchers[launcher] = whitelisted;
        emit WhitelistedLauncherUpdated(launcher, whitelisted);
    }

    function addDexConfig(DexConfig calldata d) external onlyOwner returns (uint256 dexId) {
        if (
            address(d.dexFactory) == address(0) || address(d.positionManager) == address(0)
                || address(d.swapRouter) == address(0) || d.weth9 == address(0)
        ) revert InvalidConfig();
        // The locker can only custody NFTs from its own position manager.
        if (address(d.positionManager) != address(LOCKER.POSITION_MANAGER())) revert InvalidConfig();
        // fee tier must exist on the DEX and match the configured spacing.
        if (d.dexFactory.feeAmountTickSpacing(d.fee) != d.tickSpacing || d.tickSpacing <= 0) {
            revert InvalidConfig();
        }
        dexId = _dexConfigs.length;
        _dexConfigs.push(d);
        emit DexConfigAdded(dexId, d.name);
    }

    function setDexStatus(uint256 dexId, bool enabled) external onlyOwner {
        if (dexId >= _dexConfigs.length) revert UnknownConfig();
        _dexConfigs[dexId].enabled = enabled;
        emit DexConfigStatusUpdated(dexId, enabled);
    }

    function addLaunchConfig(LaunchConfig calldata c) external onlyOwner returns (uint256 configId) {
        _validateLaunchConfig(c);
        configId = _launchConfigs.length;
        _launchConfigs.push(c);
        emit LaunchConfigAdded(configId);
    }

    function updateLaunchConfig(uint256 configId, LaunchConfig calldata c) external onlyOwner {
        if (configId >= _launchConfigs.length) revert UnknownConfig();
        _validateLaunchConfig(c);
        _launchConfigs[configId] = c;
        emit LaunchConfigUpdated(configId);
    }

    function _validateLaunchConfig(LaunchConfig calldata c) internal pure {
        if (c.pairToken == address(0) || c.totalSupply == 0) revert InvalidConfig();
        if (c.maxWalletBps > 10_000 || c.maxTxBps > 10_000) revert InvalidConfig();
        if (c.initialTick <= TickMath.MIN_TICK || c.initialTick >= TickMath.MAX_TICK) {
            revert InvalidConfig();
        }
    }

    // -------------------------------------------------------------------------
    // Views
    // -------------------------------------------------------------------------

    function dexConfigCount() external view returns (uint256) {
        return _dexConfigs.length;
    }

    function getDexConfig(uint256 dexId) external view returns (DexConfig memory) {
        if (dexId >= _dexConfigs.length) revert UnknownConfig();
        return _dexConfigs[dexId];
    }

    function launchConfigCount() external view returns (uint256) {
        return _launchConfigs.length;
    }

    function getLaunchConfig(uint256 configId) external view returns (LaunchConfig memory) {
        if (configId >= _launchConfigs.length) revert UnknownConfig();
        return _launchConfigs[configId];
    }

    function getLaunchedToken(address token) external view returns (LaunchedToken memory) {
        return _launchedTokens[token];
    }

    /// @notice True iff `token` was launched by this factory.
    function isLaunchHoodToken(address token) external view returns (bool) {
        return _launchedTokens[token].pool != address(0);
    }

    /// @param userSalt Combined with the creator for the CREATE2 salt so two
    ///                 creators passing the same value get distinct addresses.
    ///                 Frontend mines this for a 0xc0de vanity suffix; any
    ///                 value is accepted on-chain.
    function predictTokenAddress(address creator, bytes32 userSalt) public view returns (address) {
        bytes32 salt = keccak256(abi.encode(creator, userSalt));
        return Clones.predictDeterministicAddress(TOKEN_IMPL, salt, address(this));
    }

    // -------------------------------------------------------------------------
    // Launch
    // -------------------------------------------------------------------------

    function launchToken(
        TokenParams calldata p,
        uint256 configId,
        uint256 dexId,
        bytes32 userSalt,
        uint256 minTokensOut
    ) external payable nonReentrant returns (address token, address pool, uint256 positionId) {
        if (!launchEnabled && !whitelistedLaunchers[msg.sender]) revert LaunchDisabled();
        if (configId >= _launchConfigs.length || dexId >= _dexConfigs.length) revert UnknownConfig();

        LaunchConfig memory cfg = _launchConfigs[configId];
        DexConfig memory dex = _dexConfigs[dexId];
        if (!cfg.enabled || !dex.enabled) revert ConfigDisabled();
        if (msg.value < launchFee) revert InsufficientLaunchFee();

        // 1. Deploy + initialize the token (full supply minted to this factory).
        token = Clones.cloneDeterministic(TOKEN_IMPL, keccak256(abi.encode(msg.sender, userSalt)));
        LaunchHoodV3Token(token).initialize(
            p.name,
            p.symbol,
            p.metadataURI,
            msg.sender,
            address(LOCKER),
            cfg.totalSupply,
            cfg.maxWalletBps,
            cfg.maxTxBps,
            cfg.restrictionBlocks
        );

        emit TokenDeployed(
            token, msg.sender, cfg.pairToken, dexId, configId, p.name, p.symbol, p.metadataURI
        );

        // 2. Create + initialize the pool and place the full supply as
        //    single-sided liquidity, owned forever by the locker.
        uint256 initialBuyEth = msg.value - launchFee;
        positionId = _createPoolAndLock(token, cfg, dex);
        pool = _launchedPool(token, cfg.pairToken, dex);

        LaunchHoodV3Token(token).setPool(pool);

        // Fee split is snapshotted inside the locker from its admin-set
        // default — launches cannot pick their own split.
        LOCKER.register(
            positionId,
            p.rewardRecipient == address(0) ? msg.sender : p.rewardRecipient,
            token,
            cfg.pairToken
        );

        _launchedTokens[token] = LaunchedToken({
            deployer: msg.sender,
            pool: pool,
            pairToken: cfg.pairToken,
            positionId: positionId,
            launchConfigId: configId,
            dexId: dexId
        });

        // 3. Flat launch fee to the treasury.
        if (launchFee > 0) {
            (bool ok,) = PROTOCOL_TREASURY.call{value: launchFee}("");
            if (!ok) revert FeeSendFailed();
        }

        // 4. Creator's initial buy with the remaining value — the very first
        //    swap in the pool, executed before the anti-snipe caps activate.
        uint256 boughtTokens;
        if (initialBuyEth > 0) {
            if (cfg.pairToken != dex.weth9) revert InitialBuyNeedsWethPair();
            boughtTokens = dex.swapRouter.exactInputSingle{value: initialBuyEth}(
                ISwapRouter02Minimal.ExactInputSingleParams({
                    tokenIn: dex.weth9,
                    tokenOut: token,
                    fee: dex.fee,
                    recipient: msg.sender,
                    amountIn: initialBuyEth,
                    amountOutMinimum: minTokensOut,
                    sqrtPriceLimitX96: 0
                })
            );
        }

        // 5. Arm the anti-snipe caps for everyone after this tx.
        LaunchHoodV3Token(token).activateLimits();

        emit TokenLaunched(
            token,
            msg.sender,
            pool,
            cfg.pairToken,
            dexId,
            configId,
            positionId,
            LaunchHoodV3Token(token).restrictionEndBlock(),
            initialBuyEth,
            boughtTokens
        );
    }

    /// @dev Creates/initializes the pool at the configured tick and mints the
    ///      full token supply as a single-sided position straight to the
    ///      locker. Split out of launchToken to stay under the stack limit.
    function _createPoolAndLock(address token, LaunchConfig memory cfg, DexConfig memory dex)
        internal
        returns (uint256 positionId)
    {
        (address token0, address token1) =
            token < cfg.pairToken ? (token, cfg.pairToken) : (cfg.pairToken, token);
        bool tokenIsToken0 = token0 == token;

        // cfg.initialTick is quoted with the launched token as token0; flip
        // when sorting puts it at token1 so the starting PRICE is identical
        // either way.
        int24 initialTick = tokenIsToken0 ? cfg.initialTick : -cfg.initialTick;
        if (initialTick % dex.tickSpacing != 0) revert TickMisaligned();

        int24 maxUsable = (TickMath.MAX_TICK / dex.tickSpacing) * dex.tickSpacing;
        // Single-sided range: all liquidity is the launched token, sitting
        // between the starting price and the far end of the curve, so every
        // buy walks the price up from `initialTick`.
        (int24 tickLower, int24 tickUpper) =
            tokenIsToken0 ? (initialTick, maxUsable) : (-maxUsable, initialTick);

        dex.positionManager.createAndInitializePoolIfNecessary(
            token0, token1, dex.fee, TickMath.getSqrtPriceAtTick(initialTick)
        );

        // If someone pre-created the pool (the token address is predictable
        // from the salt) at a different price, refuse to launch into it —
        // the creator can relaunch with a fresh salt.
        address pool = dex.dexFactory.getPool(token0, token1, dex.fee);
        (, int24 poolTick,,,,,) = IUniswapV3PoolMinimal(pool).slot0();
        if (poolTick != initialTick) revert PoolAlreadyInitializedAtWrongPrice();

        IERC20(token).forceApprove(address(dex.positionManager), cfg.totalSupply);

        (uint256 amount0Desired, uint256 amount1Desired) =
            tokenIsToken0 ? (cfg.totalSupply, uint256(0)) : (uint256(0), cfg.totalSupply);

        // amountMins are 0: the pool price was just pinned to `initialTick`
        // above, so the only variance is sub-wei rounding dust, which stays
        // in the factory (economically nil — ~1e-20 of supply).
        (positionId,,,) = dex.positionManager.mint(
            INonfungiblePositionManagerMinimal.MintParams({
                token0: token0,
                token1: token1,
                fee: dex.fee,
                tickLower: tickLower,
                tickUpper: tickUpper,
                amount0Desired: amount0Desired,
                amount1Desired: amount1Desired,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(LOCKER),
                deadline: block.timestamp
            })
        );
    }

    function _launchedPool(address token, address pairToken, DexConfig memory dex)
        internal
        view
        returns (address)
    {
        (address token0, address token1) =
            token < pairToken ? (token, pairToken) : (pairToken, token);
        return dex.dexFactory.getPool(token0, token1, dex.fee);
    }
}
Chain explorer1711msChain node80ms