LaunchFactory
0x9fafa3b9ffcc7b3b9d118085797024be6eaf83ea
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
9,470 bytes
ABI entries
21
8 read · 6 write
License
none
Contract information
- Address
- 0x9fafa3b9ffcc7b3b9d118085797024be6eaf83ea
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x90A016771D…d23D6851c6
- Creation tx
- 0x5a79a692c3…a961cf931a
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (8)
POOL_FEE() → uint24
antiSnipeSeconds() → uint256
creationFee() → uint256
creatorFeeBps() → uint16
locker() → address
owner() → address
paused() → bool
treasury() → address
Events (2)
OwnershipTransferredTokenLaunched
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "uint256",
"name": "antiSnipeSeconds_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "FeeTooLow",
"type": "error"
},
{
"inputs": [],
"name": "LaunchesPaused",
"type": "error"
},
{
"inputs": [],
"name": "NotOwner",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "lpTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
}
],
"name": "TokenLaunched",
"type": "event"
},
{
"inputs": [],
"name": "POOL_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "antiSnipeSeconds",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "creationFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "creatorFeeBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"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": "struct LaunchFactory.TokenMeta",
"name": "meta",
"type": "tuple"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"name": "launchToken",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint256",
"name": "lpTokenId",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "locker",
"outputs": [
{
"internalType": "contract LpLocker",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "paused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "v",
"type": "uint256"
}
],
"name": "setCreationFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "v",
"type": "uint16"
}
],
"name": "setCreatorFeeBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "v",
"type": "bool"
}
],
"name": "setPaused",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "v",
"type": "address"
}
],
"name": "setTreasury",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {LaunchToken} from "./LaunchToken.sol";
import {LpLocker} from "./LpLocker.sol";
import {RobinhoodChain} from "./RobinhoodChain.sol";
import {
IUniswapV3Factory,
IUniswapV3Pool,
INonfungiblePositionManager,
IWETH9,
ISwapRouter02
} from "./interfaces/IUniswapV3.sol";
/// @title LaunchFactory
/// @notice One-transaction token launch, reproducing Noxa Fun on Uniswap V3.
///
/// Observed Noxa entrypoint (decoded on-chain):
/// launchToken((name, symbol, metadataURI, ..., socials, address), configId, dexId, salt)
/// msg.value = creationFee (~0.0005 ETH) + optional dev-buy
///
/// The launch does, atomically:
/// 1. take the flat creation fee
/// 2. CREATE2-deploy the LaunchToken (full 1B supply minted to this factory)
/// 3. create + initialize the 1% V3 pool at the opening price
/// 4. mint the ENTIRE supply as a single-sided position (the "bonding curve")
/// 5. transfer the LP NFT to the LpLocker (permanent, rug-proof)
/// 6. optionally execute the creator's dev-buy in the same tx
///
/// @dev Steps 3–4 (pool init sqrtPrice + single-sided tick range) are the only
/// non-trivial mechanics; the exact tick math is finalized in Phase 3 against
/// a mainnet fork and by decoding one live Noxa launch. Everything else here
/// is the real control flow.
contract LaunchFactory {
struct TokenMeta {
string name;
string symbol;
string metadataURI; // ipfs://…
}
uint256 public creationFee = RobinhoodChain.CREATION_FEE;
uint24 public constant POOL_FEE = RobinhoodChain.POOL_FEE; // 1%
// Our split: 60% creator / 40% platform (Noxa gives creators only 35%, so this
// is the lever to pull launches over). Change any time via setCreatorFeeBps.
uint16 public creatorFeeBps = RobinhoodChain.CREATOR_FEE_BPS;
uint256 public antiSnipeSeconds; // anti-snipe window (~1h) — set in constructor
bool public paused; // owner kill switch for NEW launches only — never trading
address public owner;
address public treasury;
LpLocker public immutable locker;
IUniswapV3Factory constant V3_FACTORY = IUniswapV3Factory(RobinhoodChain.UNIV3_FACTORY);
INonfungiblePositionManager constant NPM =
INonfungiblePositionManager(RobinhoodChain.NONFUNGIBLE_POSITION_MANAGER);
address constant WETH = RobinhoodChain.WETH;
// ── Noxa launch template — read off every live Noxa LP position ──────────
// fee 10000, tickLower -887200, tickUpper 204200, single-sided (all token).
// Opening FDV ≈ 1.356 ETH (opening price ~1.36e-9 ETH/token × 1B supply).
// The token side is single-sided, so the opening tick is the bound nearest
// spot; ticks/price mirror depending on token↔WETH address ordering.
int24 constant NOXA_TICK = 204200; // |opening tick|
int24 constant FULL_TICK = 887200; // full-range bound (aligned to spacing 200)
// sqrtPriceX96 at the opening tick (precomputed via TickMath):
uint160 constant SQRT_OPEN_TOKEN1 = 2151813121295408910812139624586144; // tick 204200
uint160 constant SQRT_OPEN_TOKEN0 = 2917122157712197017744680; // tick -204200
event TokenLaunched(
address indexed token,
address indexed creator,
address pool,
uint256 lpTokenId,
string name,
string symbol
);
event OwnershipTransferred(address indexed from, address indexed to);
error FeeTooLow();
error NotOwner();
error ZeroAddress();
error LaunchesPaused();
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
constructor(address treasury_, uint256 antiSnipeSeconds_) {
owner = msg.sender;
treasury = treasury_;
antiSnipeSeconds = antiSnipeSeconds_;
locker = new LpLocker(RobinhoodChain.NONFUNGIBLE_POSITION_MANAGER, treasury_, WETH);
}
/// @notice Launch a token. `msg.value` = creationFee + optional dev-buy.
function launchToken(TokenMeta calldata meta, bytes32 salt)
external
payable
returns (address token, address pool, uint256 lpTokenId)
{
if (paused) revert LaunchesPaused();
if (msg.value < creationFee) revert FeeTooLow();
uint256 devBuy = msg.value - creationFee;
// 1. creation fee → treasury
(bool ok,) = treasury.call{value: creationFee}("");
require(ok, "fee xfer failed");
// 2. deploy token — full supply minted to this factory. Pool/router are
// NOT exempt, so buyers stay capped to 2% during the window; the pool
// is registered below so sells (wallet → pool) clear the wallet cap.
token = address(
new LaunchToken{salt: salt}(
meta.name, meta.symbol, msg.sender, _exemptList(), antiSnipeSeconds, 0
)
);
uint256 supply = LaunchToken(token).SUPPLY();
// Noxa mines the salt so the token address sorts ABOVE WETH, making the
// token always token1 and the template deterministic. Enforce the same;
// the deployer mines a qualifying salt off-chain (WETH is low, so most do).
// 3. create + initialize the 1% pool at Noxa's exact opening price.
// Ticks, opening sqrtPrice and the seeded side all mirror on the
// token↔WETH address ordering — but the opening FDV (≈1.356 ETH) is
// identical either way, so no salt-mining is needed (any salt works).
bool tokenIsToken0 = uint160(token) < uint160(WETH);
(address t0, address t1) = tokenIsToken0 ? (token, WETH) : (WETH, token);
int24 tickLower;
int24 tickUpper;
uint160 sqrtOpen;
uint256 amt0;
uint256 amt1;
if (tokenIsToken0) {
// token = token0: seed all token0, opened at the lower bound.
tickLower = -NOXA_TICK; // -204200
tickUpper = FULL_TICK; // 887200
sqrtOpen = SQRT_OPEN_TOKEN0;
amt0 = supply;
} else {
// token = token1 (Noxa's usual case): seed all token1, opened at 204200.
tickLower = -FULL_TICK; // -887200
tickUpper = NOXA_TICK; // 204200
sqrtOpen = SQRT_OPEN_TOKEN1;
amt1 = supply;
}
pool = V3_FACTORY.getPool(token, WETH, POOL_FEE);
if (pool == address(0)) pool = V3_FACTORY.createPool(token, WETH, POOL_FEE);
IUniswapV3Pool(pool).initialize(sqrtOpen);
// Register the pool so it's excused from the max-wallet cap as recipient
// (the sell path). It stays non-exempt otherwise, keeping buys capped.
LaunchToken(token).setPool(pool);
// 4. single-sided mint of the entire supply (the "bonding curve").
IERC20(token).approve(address(NPM), supply);
(lpTokenId,,,) = NPM.mint(
INonfungiblePositionManager.MintParams({
token0: t0,
token1: t1,
fee: POOL_FEE,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: amt0,
amount1Desired: amt1,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp
})
);
// 5. lock the LP NFT permanently, carrying the creator/platform split.
NPM.safeTransferFrom(
address(this), address(locker), lpTokenId, abi.encode(msg.sender, creatorFeeBps)
);
// 6. optional dev-buy in the same tx: any ETH beyond the creation fee is
// swapped WETH→token via SwapRouter02 straight to the creator. The
// creator is anti-snipe-exempt, so the dev allocation isn't capped.
if (devBuy > 0) {
IWETH9(WETH).deposit{value: devBuy}();
IWETH9(WETH).approve(RobinhoodChain.SWAP_ROUTER_02, devBuy);
ISwapRouter02(RobinhoodChain.SWAP_ROUTER_02).exactInputSingle(
ISwapRouter02.ExactInputSingleParams({
tokenIn: WETH,
tokenOut: token,
fee: POOL_FEE,
recipient: msg.sender,
amountIn: devBuy,
// Atomic with pool creation at the known opening price — nobody
// can sandwich between init and this buy, so 0 is safe here.
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
})
);
}
emit TokenLaunched(token, msg.sender, pool, lpTokenId, meta.name, meta.symbol);
}
/// @dev Anti-snipe exemptions: the NPM (seeds the pool), the locker
/// (receives the token side of fees, which can exceed the cap), and
/// the dead address (fee burns must never revert). In a helper to
/// keep launchToken's stack depth in check.
function _exemptList() internal view returns (address[] memory exempt) {
exempt = new address[](3);
exempt[0] = address(NPM);
exempt[1] = address(locker);
exempt[2] = 0x000000000000000000000000000000000000dEaD;
}
// ── admin ───────────────────────────────────────────────────────────────
function setCreationFee(uint256 v) external onlyOwner {
creationFee = v;
}
/// @notice Stop/resume NEW launches. Has no power over existing tokens,
/// pools, locked LP, or fee claims — those are unstoppable.
function setPaused(bool v) external onlyOwner {
paused = v;
}
function setCreatorFeeBps(uint16 v) external onlyOwner {
require(v <= 10_000, "bad bps");
creatorFeeBps = v;
}
function setTreasury(address v) external onlyOwner {
if (v == address(0)) revert ZeroAddress();
treasury = v;
}
/// @notice Hand admin (fee/treasury control) to a more secure wallet later —
/// e.g. move off the deployer key onto a hardware wallet or multisig.
function transferOwnership(address newOwner) external onlyOwner {
if (newOwner == address(0)) revert ZeroAddress();
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
Chain explorer2428msChain node85ms