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
- Creator
- 0x73267feDc2…B208cfEaA4
- 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