LaunchFactory
0xa24d48d50fd7985c6de816eaf77c1a17d3593bbe
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
21,742 bytes
ABI entries
67
15 read · 14 write
License
none
Contract information
- Address
- 0xa24d48d50fd7985c6de816eaf77c1a17d3593bbe
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x407Fe47FA0…AB006322d8
- Creation tx
- 0xef86f678a2…4a6582de81
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (15)
ctoFund() → address
ctoVaultImplementation() → address
ctoVaultOf(address) → address
dexConfigCount() → uint256
getDexConfig(uint256) → tuple
getLaunchConfig(uint256) → tuple
getLaunchedToken(address) → tuple
isLaunchedToken(address) → bool
launchConfigCount() → uint256
launchEnabled() → bool
launchFee() → uint256
locker() → address
owner() → address
poolOf(address) → address
whitelistedLaunchers(address) → bool
Events (14)
CTOFeeVaultCreatedCTOFundConfiguredDexConfigAddedDexStatusUpdatedFeeRecipientRestrictionExemptedLaunchConfigAddedLaunchConfigUpdatedLaunchEnabledUpdatedLaunchFeeUpdatedOwnershipTransferredTokenDeployedTokenLaunchedTokenMetadataWhitelistedLauncherUpdated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "locker_",
"type": "address"
},
{
"internalType": "uint256",
"name": "launchFee_",
"type": "uint256"
},
{
"internalType": "bool",
"name": "launchEnabled_",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "CTOFundAlreadyConfigured",
"type": "error"
},
{
"inputs": [],
"name": "CTOFundNotConfigured",
"type": "error"
},
{
"inputs": [],
"name": "CreateFailed",
"type": "error"
},
{
"inputs": [],
"name": "DexDisabled",
"type": "error"
},
{
"inputs": [],
"name": "EmptyNameOrSymbol",
"type": "error"
},
{
"inputs": [],
"name": "FeeTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientFee",
"type": "error"
},
{
"inputs": [],
"name": "InvalidBasisPoints",
"type": "error"
},
{
"inputs": [],
"name": "InvalidCTOFund",
"type": "error"
},
{
"inputs": [],
"name": "InvalidDexConfig",
"type": "error"
},
{
"inputs": [],
"name": "InvalidInitialBuyAmount",
"type": "error"
},
{
"inputs": [],
"name": "InvalidLaunchConfig",
"type": "error"
},
{
"inputs": [],
"name": "LaunchDisabled",
"type": "error"
},
{
"inputs": [],
"name": "NotWhitelisted",
"type": "error"
},
{
"inputs": [],
"name": "OnlyCTOFund",
"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": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [],
"name": "TokenAlreadyLaunched",
"type": "error"
},
{
"inputs": [],
"name": "TokenTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "UnauthorizedCallback",
"type": "error"
},
{
"inputs": [],
"name": "UnknownToken",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "vault",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "initialLeader",
"type": "address"
}
],
"name": "CTOFeeVaultCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "ctoFund",
"type": "address"
}
],
"name": "CTOFundConfigured",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "dexId",
"type": "uint256"
}
],
"name": "DexConfigAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "dexId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "DexStatusUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "FeeRecipientRestrictionExempted",
"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": "launchFee",
"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": "dexFactory",
"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"
}
],
"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": "dexFactory",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "pairToken",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "pool",
"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": "restrictionsEndBlock",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "initialBuyAmount",
"type": "uint256"
}
],
"name": "TokenLaunched",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "logo",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "description",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "telegram",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "twitter",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "discord",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "website",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "farcaster",
"type": "string"
},
{
"indexed": false,
"internalType": "address",
"name": "devWallet",
"type": "address"
}
],
"name": "TokenMetadata",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "launcher",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "WhitelistedLauncherUpdated",
"type": "event"
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "dexFactory",
"type": "address"
},
{
"internalType": "address",
"name": "positionManager",
"type": "address"
},
{
"internalType": "address",
"name": "router",
"type": "address"
},
{
"internalType": "uint24",
"name": "poolFee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"internalType": "struct LauncherTypes.DexConfig",
"name": "config",
"type": "tuple"
}
],
"name": "addDexConfig",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "pairToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "dexId",
"type": "uint256"
},
{
"internalType": "int24",
"name": "initialTick",
"type": "int24"
},
{
"internalType": "uint256",
"name": "supply",
"type": "uint256"
},
{
"internalType": "uint16",
"name": "maxWalletBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "maxTxBps",
"type": "uint16"
},
{
"internalType": "uint32",
"name": "restrictionBlocks",
"type": "uint32"
},
{
"internalType": "uint24",
"name": "buyPairHopFee",
"type": "uint24"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
},
{
"internalType": "bool",
"name": "permissioned",
"type": "bool"
}
],
"internalType": "struct LauncherTypes.LaunchConfig",
"name": "config",
"type": "tuple"
}
],
"name": "addLaunchConfig",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "ctoFund",
"outputs": [
{
"internalType": "contract INoxaCTOFund",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "ctoSnapshot",
"outputs": [
{
"internalType": "uint256",
"name": "snapshotId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "ctoVaultImplementation",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "ctoVaultOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"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": "address",
"name": "dexFactory",
"type": "address"
},
{
"internalType": "address",
"name": "positionManager",
"type": "address"
},
{
"internalType": "address",
"name": "router",
"type": "address"
},
{
"internalType": "uint24",
"name": "poolFee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"internalType": "struct LauncherTypes.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": "dexId",
"type": "uint256"
},
{
"internalType": "int24",
"name": "initialTick",
"type": "int24"
},
{
"internalType": "uint256",
"name": "supply",
"type": "uint256"
},
{
"internalType": "uint16",
"name": "maxWalletBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "maxTxBps",
"type": "uint16"
},
{
"internalType": "uint32",
"name": "restrictionBlocks",
"type": "uint32"
},
{
"internalType": "uint24",
"name": "buyPairHopFee",
"type": "uint24"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
},
{
"internalType": "bool",
"name": "permissioned",
"type": "bool"
}
],
"internalType": "struct LauncherTypes.LaunchConfig",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "getLaunchedToken",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "deployer",
"type": "address"
},
{
"internalType": "address",
"name": "feeWallet",
"type": "address"
},
{
"internalType": "address",
"name": "pairToken",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint256",
"name": "dexId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "launchConfigId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "restrictionsEndBlock",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "initialBuyAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "createdAtBlock",
"type": "uint256"
},
{
"internalType": "bool",
"name": "isToken0",
"type": "bool"
}
],
"internalType": "struct LauncherTypes.LaunchedToken",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "isLaunchedToken",
"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": "logo",
"type": "string"
},
{
"internalType": "string",
"name": "description",
"type": "string"
},
{
"components": [
{
"internalType": "string",
"name": "telegram",
"type": "string"
},
{
"internalType": "string",
"name": "twitter",
"type": "string"
},
{
"internalType": "string",
"name": "discord",
"type": "string"
},
{
"internalType": "string",
"name": "website",
"type": "string"
},
{
"internalType": "string",
"name": "farcaster",
"type": "string"
}
],
"internalType": "struct LauncherTypes.Socials",
"name": "socials",
"type": "tuple"
},
{
"internalType": "address",
"name": "devWallet",
"type": "address"
}
],
"internalType": "struct LauncherTypes.LaunchParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "launchConfigId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "dexId",
"type": "uint256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"name": "launchToken",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "locker",
"outputs": [
{
"internalType": "contract LauncherLocker",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "poolOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "ctoFund_",
"type": "address"
}
],
"name": "setCTOFund",
"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": "allowed",
"type": "bool"
}
],
"name": "setWhitelistedLauncher",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "syncFeeRecipientExemptions",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "int256",
"name": "amount0Delta",
"type": "int256"
},
{
"internalType": "int256",
"name": "amount1Delta",
"type": "int256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "uniswapV3SwapCallback",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "configId",
"type": "uint256"
},
{
"components": [
{
"internalType": "address",
"name": "pairToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "dexId",
"type": "uint256"
},
{
"internalType": "int24",
"name": "initialTick",
"type": "int24"
},
{
"internalType": "uint256",
"name": "supply",
"type": "uint256"
},
{
"internalType": "uint16",
"name": "maxWalletBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "maxTxBps",
"type": "uint16"
},
{
"internalType": "uint32",
"name": "restrictionBlocks",
"type": "uint32"
},
{
"internalType": "uint24",
"name": "buyPairHopFee",
"type": "uint24"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
},
{
"internalType": "bool",
"name": "permissioned",
"type": "bool"
}
],
"internalType": "struct LauncherTypes.LaunchConfig",
"name": "config",
"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;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {LauncherTypes} from "./src/LauncherTypes.sol";
import {LaunchToken} from "./src/LaunchToken.sol";
import {LauncherLocker} from "./src/LauncherLocker.sol";
import {CTOFeeVault} from "./src/CTOFeeVault.sol";
import {INoxaCTOFund} from "./src/interfaces/INoxaCTO.sol";
import {Clones} from "./src/libraries/Clones.sol";
import {TickMath} from "./src/libraries/TickMath.sol";
import {
INonfungiblePositionManager,
IUniswapV3Pool,
IUniswapV3SwapCallback,
IWETH9
} from "./src/interfaces/IUniswapV3.sol";
/// @notice Deploys new tokens straight into a Uniswap V3 pool: the full supply is
/// minted as single-sided liquidity and the LP NFT is locked forever in the
/// LauncherLocker. Optionally executes the creator's initial buy in the same tx.
contract LaunchFactory is Ownable, ReentrancyGuard, IUniswapV3SwapCallback {
struct LaunchRuntime {
address pool;
address ctoVault;
uint256 positionId;
uint256 restrictionsEndBlock;
uint256 initialBuyAmount;
bool isToken0;
}
error LaunchDisabled();
error InvalidLaunchConfig();
error InvalidDexConfig();
error DexDisabled();
error NotWhitelisted();
error InsufficientFee();
error FeeTransferFailed();
error InvalidBasisPoints();
error UnauthorizedCallback();
error TokenAlreadyLaunched();
error EmptyNameOrSymbol();
error ZeroAddress();
error CTOFundNotConfigured();
error CTOFundAlreadyConfigured();
error InvalidCTOFund();
error OnlyCTOFund();
error UnknownToken();
error TokenTransferFailed();
error InvalidInitialBuyAmount();
event TokenDeployed(
address indexed token,
address indexed deployer,
address indexed dexFactory,
address pairToken,
uint256 dexId,
uint256 launchConfigId
);
event TokenLaunched(
address indexed token,
address indexed deployer,
address indexed dexFactory,
address pairToken,
address pool,
uint256 dexId,
uint256 launchConfigId,
uint256 positionId,
uint256 restrictionsEndBlock,
uint256 initialBuyAmount
);
event TokenMetadata(
address indexed token,
string name,
string symbol,
string logo,
string description,
string telegram,
string twitter,
string discord,
string website,
string farcaster,
address devWallet
);
event LaunchConfigAdded(uint256 indexed configId);
event LaunchConfigUpdated(uint256 indexed configId);
event DexConfigAdded(uint256 indexed dexId);
event DexStatusUpdated(uint256 indexed dexId, bool enabled);
event LaunchFeeUpdated(uint256 launchFee);
event LaunchEnabledUpdated(bool enabled);
event WhitelistedLauncherUpdated(address indexed launcher, bool allowed);
event CTOFundConfigured(address indexed ctoFund);
event CTOFeeVaultCreated(address indexed token, address indexed vault, address indexed initialLeader);
event FeeRecipientRestrictionExempted(address indexed token, address indexed recipient);
LauncherLocker public immutable locker;
address public immutable ctoVaultImplementation;
uint256 public launchFee;
bool public launchEnabled;
uint256 public launchConfigCount;
uint256 public dexConfigCount;
mapping(uint256 => LauncherTypes.LaunchConfig) internal launchConfigs;
mapping(uint256 => LauncherTypes.DexConfig) internal dexConfigs;
mapping(address => bool) public whitelistedLaunchers;
mapping(address => LauncherTypes.LaunchedToken) internal launchedTokens;
mapping(address token => address vault) internal ctoVaults;
/// @notice Stable election module used by every token launched after configuration.
/// @dev Wired once after deployment to avoid a circular Factory <-> CTOFund constructor dependency.
INoxaCTOFund public ctoFund;
/// @dev Pool allowed to invoke the swap callback for the duration of the initial buy.
address private _pendingCallbackPool;
/// @dev Position id minted by the current launch, passed between helpers.
uint256 private _lastPositionId;
constructor(address locker_, uint256 launchFee_, bool launchEnabled_) Ownable(msg.sender) {
if (locker_ == address(0)) revert ZeroAddress();
locker = LauncherLocker(locker_);
ctoVaultImplementation = address(new CTOFeeVault());
launchFee = launchFee_;
launchEnabled = launchEnabled_;
}
// ---------------------------------------------------------------- admin
function addLaunchConfig(LauncherTypes.LaunchConfig calldata config) external onlyOwner {
if (config.pairToken == address(0) || config.supply == 0) revert InvalidLaunchConfig();
if (config.maxWalletBps > 10_000 || config.maxTxBps > 10_000) revert InvalidBasisPoints();
launchConfigs[launchConfigCount] = config;
emit LaunchConfigAdded(launchConfigCount++);
}
function updateLaunchConfig(uint256 configId, LauncherTypes.LaunchConfig calldata config) external onlyOwner {
if (configId >= launchConfigCount) revert InvalidLaunchConfig();
if (config.pairToken == address(0) || config.supply == 0) revert InvalidLaunchConfig();
if (config.maxWalletBps > 10_000 || config.maxTxBps > 10_000) revert InvalidBasisPoints();
launchConfigs[configId] = config;
emit LaunchConfigUpdated(configId);
}
function addDexConfig(LauncherTypes.DexConfig calldata config) external onlyOwner {
if (config.dexFactory == address(0) || config.positionManager == address(0) || config.tickSpacing <= 0) {
revert InvalidDexConfig();
}
dexConfigs[dexConfigCount] = config;
emit DexConfigAdded(dexConfigCount++);
}
function setDexStatus(uint256 dexId, bool enabled) external onlyOwner {
if (dexId >= dexConfigCount) revert InvalidDexConfig();
dexConfigs[dexId].enabled = enabled;
emit DexStatusUpdated(dexId, enabled);
}
function setLaunchFee(uint256 fee) external onlyOwner {
launchFee = fee;
emit LaunchFeeUpdated(fee);
}
function setLaunchEnabled(bool enabled) external onlyOwner {
launchEnabled = enabled;
emit LaunchEnabledUpdated(enabled);
}
function setWhitelistedLauncher(address launcher, bool allowed) external onlyOwner {
whitelistedLaunchers[launcher] = allowed;
emit WhitelistedLauncherUpdated(launcher, allowed);
}
/// @notice One-time wiring of the CTO election module.
function setCTOFund(address ctoFund_) external onlyOwner {
if (ctoFund_ == address(0)) revert ZeroAddress();
if (address(ctoFund) != address(0)) revert CTOFundAlreadyConfigured();
if (INoxaCTOFund(ctoFund_).factory() != address(this)) revert InvalidCTOFund();
ctoFund = INoxaCTOFund(ctoFund_);
emit CTOFundConfigured(ctoFund_);
}
// ---------------------------------------------------------------- views
function getLaunchConfig(uint256 configId) external view returns (LauncherTypes.LaunchConfig memory) {
if (configId >= launchConfigCount) revert InvalidLaunchConfig();
return launchConfigs[configId];
}
function getDexConfig(uint256 dexId) external view returns (LauncherTypes.DexConfig memory) {
if (dexId >= dexConfigCount) revert InvalidDexConfig();
return dexConfigs[dexId];
}
function getLaunchedToken(address token) external view returns (LauncherTypes.LaunchedToken memory) {
return launchedTokens[token];
}
function isLaunchedToken(address token) external view returns (bool) {
return launchedTokens[token].token != address(0);
}
function poolOf(address token) external view returns (address) {
return launchedTokens[token].pool;
}
function ctoVaultOf(address token) external view returns (address) {
return ctoVaults[token];
}
/// @notice Creates a holder snapshot after validating the caller and synchronizing protocol inventory.
function ctoSnapshot(address token) external returns (uint256 snapshotId) {
if (msg.sender != address(ctoFund)) revert OnlyCTOFund();
if (launchedTokens[token].token == address(0)) revert UnknownToken();
LaunchToken launchTokenContract = LaunchToken(token);
_syncFeeRecipients(launchTokenContract, true);
snapshotId = launchTokenContract.snapshot();
}
/// @notice Refreshes anti-snipe treatment after either fixed FeeRouter recipient changes.
/// @dev Permissionless and fixed-destination: callers cannot choose which account becomes exempt.
/// Voting exclusion is applied atomically at the next CTO snapshot, never halfway through a round.
function syncFeeRecipientExemptions(address token) external {
if (launchedTokens[token].token == address(0)) revert UnknownToken();
_syncFeeRecipients(LaunchToken(token), false);
}
// ---------------------------------------------------------------- launch
function launchToken(
LauncherTypes.LaunchParams calldata params,
uint256 launchConfigId,
uint256 dexId,
bytes32 salt
) external payable nonReentrant returns (address token, uint256 positionId) {
if (!launchEnabled) revert LaunchDisabled();
if (address(ctoFund) == address(0)) revert CTOFundNotConfigured();
if (launchConfigId >= launchConfigCount) revert InvalidLaunchConfig();
if (bytes(params.name).length == 0 || bytes(params.symbol).length == 0) revert EmptyNameOrSymbol();
if (params.devWallet == address(0)) revert ZeroAddress();
LauncherTypes.LaunchConfig memory cfg = launchConfigs[launchConfigId];
if (!cfg.enabled) revert InvalidLaunchConfig();
if (cfg.permissioned && !whitelistedLaunchers[msg.sender]) revert NotWhitelisted();
if (dexId >= dexConfigCount) revert InvalidDexConfig();
LauncherTypes.DexConfig memory dex = dexConfigs[dexId];
if (!dex.enabled) revert DexDisabled();
if (msg.value < launchFee) revert InsufficientFee();
LaunchRuntime memory runtime;
runtime.initialBuyAmount = msg.value - launchFee;
runtime.restrictionsEndBlock = block.number + cfg.restrictionBlocks;
token = _deployToken(params, cfg, runtime.restrictionsEndBlock, salt);
runtime.ctoVault = Clones.clone(ctoVaultImplementation);
CTOFeeVault(payable(runtime.ctoVault)).initialize(token, cfg.pairToken, address(ctoFund));
LaunchToken(token).setVotingExcluded(runtime.ctoVault);
(runtime.pool, runtime.isToken0) = _createPoolAndLockLiquidity(token, cfg, dex, runtime.ctoVault);
runtime.positionId = _lastPositionId;
positionId = runtime.positionId;
launchedTokens[token] = LauncherTypes.LaunchedToken({
token: token,
deployer: msg.sender,
// Preserve the legacy field's meaning for indexers/UIs. The actual LP-fee recipient is the
// immutable vault stored in `ctoVaults` and registered in LauncherLocker below.
feeWallet: params.devWallet,
pairToken: cfg.pairToken,
pool: runtime.pool,
dexId: dexId,
launchConfigId: launchConfigId,
positionId: runtime.positionId,
restrictionsEndBlock: runtime.restrictionsEndBlock,
initialBuyAmount: runtime.initialBuyAmount,
createdAtBlock: block.number,
isToken0: runtime.isToken0
});
ctoVaults[token] = runtime.ctoVault;
ctoFund.onCreate(token, params.devWallet);
emit CTOFeeVaultCreated(token, runtime.ctoVault, params.devWallet);
emit TokenDeployed(token, msg.sender, dex.dexFactory, cfg.pairToken, dexId, launchConfigId);
_emitTokenMetadata(token, params);
if (launchFee > 0) {
// The anti-spam launch fee remains separate from the LP-fee split,
// but all payouts stay ERC20: wrap it and fund the burner in WETH.
IWETH9(cfg.pairToken).deposit{value: launchFee}();
if (!IWETH9(cfg.pairToken).transfer(locker.feeRouter().burnerRecipient(), launchFee)) {
revert FeeTransferFailed();
}
}
if (runtime.initialBuyAmount > 0) {
_executeInitialBuy(runtime.pool, cfg.pairToken, runtime.isToken0, runtime.initialBuyAmount);
}
emit TokenLaunched(
token,
msg.sender,
dex.dexFactory,
cfg.pairToken,
runtime.pool,
dexId,
launchConfigId,
_lastPositionId,
runtime.restrictionsEndBlock,
runtime.initialBuyAmount
);
_lastPositionId = 0;
}
function _emitTokenMetadata(address token, LauncherTypes.LaunchParams calldata params) private {
emit TokenMetadata(
token,
params.name,
params.symbol,
params.logo,
params.description,
params.socials.telegram,
params.socials.twitter,
params.socials.discord,
params.socials.website,
params.socials.farcaster,
params.devWallet
);
}
function _deployToken(
LauncherTypes.LaunchParams calldata params,
LauncherTypes.LaunchConfig memory cfg,
uint256 restrictionsEndBlock,
bytes32 salt
) private returns (address token) {
bytes32 finalSalt = keccak256(abi.encode(msg.sender, salt));
token = address(
new LaunchToken{salt: finalSalt}(
params.name, params.symbol, cfg.supply, cfg.maxWalletBps, cfg.maxTxBps, restrictionsEndBlock
)
);
if (launchedTokens[token].token != address(0)) revert TokenAlreadyLaunched();
}
function _createPoolAndLockLiquidity(
address token,
LauncherTypes.LaunchConfig memory cfg,
LauncherTypes.DexConfig memory dex,
address feeWallet
) private returns (address pool, bool isToken0) {
isToken0 = token < cfg.pairToken;
int24 initialTick = isToken0 ? cfg.initialTick : -cfg.initialTick;
(address token0, address token1) = isToken0 ? (token, cfg.pairToken) : (cfg.pairToken, token);
INonfungiblePositionManager pm = INonfungiblePositionManager(dex.positionManager);
pool = pm.createAndInitializePoolIfNecessary(
token0, token1, dex.poolFee, TickMath.getSqrtRatioAtTick(initialTick)
);
LaunchToken lt = LaunchToken(token);
lt.setVotingExcluded(pool);
lt.setVotingExcluded(address(locker.feeRouter()));
lt.setRestrictionExempt(pool);
lt.setRestrictionExempt(address(locker));
lt.setRestrictionExempt(dex.positionManager);
if (dex.router != address(0)) lt.setRestrictionExempt(dex.router);
// During the anti-snipe window, max-wallet checks also apply to protocol recipients. Exempt every
// hop in the permissionless LP-fee path so fee collection cannot be blocked by those launch limits.
lt.setRestrictionExempt(feeWallet);
lt.configureFeeVault(feeWallet, address(locker.feeRouter()));
lt.setRestrictionExempt(address(locker.feeRouter()));
_syncFeeRecipients(lt, true);
lt.approve(dex.positionManager, cfg.supply);
(int24 tickLower, int24 tickUpper) = isToken0
? (initialTick, TickMath.maxUsableTick(dex.tickSpacing))
: (TickMath.minUsableTick(dex.tickSpacing), initialTick);
(uint256 positionId,,,) = pm.mint(
INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: dex.poolFee,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: isToken0 ? cfg.supply : 0,
amount1Desired: isToken0 ? 0 : cfg.supply,
amount0Min: 0,
amount1Min: 0,
recipient: address(locker),
deadline: block.timestamp
})
);
_lastPositionId = positionId;
locker.registerPosition(token, positionId, dex.positionManager, cfg.pairToken, feeWallet, isToken0);
// Rounding can leave dust behind. Move it to the permanently excluded dead address.
uint256 dust = lt.balanceOf(address(this));
if (dust > 0 && !lt.transfer(0x000000000000000000000000000000000000dEaD, dust)) {
revert TokenTransferFailed();
}
}
function _syncFeeRecipients(LaunchToken token, bool excludeFromVoting) private {
address protocolRecipient = locker.feeRouter().protocolRecipient();
address burnerRecipient = locker.feeRouter().burnerRecipient();
if (protocolRecipient == address(0) || burnerRecipient == address(0)) revert ZeroAddress();
token.setRestrictionExempt(protocolRecipient);
emit FeeRecipientRestrictionExempted(address(token), protocolRecipient);
if (burnerRecipient != protocolRecipient) {
token.setRestrictionExempt(burnerRecipient);
emit FeeRecipientRestrictionExempted(address(token), burnerRecipient);
}
if (excludeFromVoting) {
token.setVotingExcluded(protocolRecipient);
if (burnerRecipient != protocolRecipient) token.setVotingExcluded(burnerRecipient);
}
}
function _executeInitialBuy(address pool, address pairToken, bool isToken0, uint256 amountIn) private {
if (amountIn > uint256(type(int256).max)) revert InvalidInitialBuyAmount();
// The preceding bound makes this conversion exact.
// forge-lint: disable-next-line(unsafe-typecast)
int256 signedAmountIn = int256(amountIn);
IWETH9(pairToken).deposit{value: amountIn}();
bool zeroForOne = !isToken0; // paying pairToken for token
_pendingCallbackPool = pool;
IUniswapV3Pool(pool)
.swap(
msg.sender,
zeroForOne,
signedAmountIn,
zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1,
abi.encode(pairToken)
);
_pendingCallbackPool = address(0);
}
/// @inheritdoc IUniswapV3SwapCallback
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
if (msg.sender != _pendingCallbackPool || _pendingCallbackPool == address(0)) revert UnauthorizedCallback();
address pairToken = abi.decode(data, (address));
uint256 owed = uint256(amount0Delta > 0 ? amount0Delta : amount1Delta);
if (!IWETH9(pairToken).transfer(msg.sender, owed)) revert TokenTransferFailed();
}
}
Chain explorer3689msChain node116ms