StoxesFunV3LaunchFactory
0xf612b37d684b3eca3583121172764ab0ef8417fc
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
13,093 bytes
ABI entries
31
20 read · 1 write
License
none
Contract information
- Address
- 0xf612b37d684b3eca3583121172764ab0ef8417fc
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x9Cf1e0bff5…CcA354c20B
- Creation tx
- 0x03fd32c7b7…0e7142d868
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (20)
MAX_STARTING_MCAP() → uint256
MIN_STARTING_MCAP() → uint256
POOL_FEE() → uint24
TICK_SPACING() → int24
TOTAL_SUPPLY() → uint256
VANITY_SUFFIX() → uint16
WETH() → address
allTokens(uint256) → address
allTokensLength() → uint256
computeSqrtPriceX96(address, uint256) → uint160
computeTokenAddress(address, bytes32) → address
creatorOf(address) → address
launchParams() → string, string, string, address, address
locker() → address
poolOf(address) → address
positionIdOf(address) → uint256
positionManager() → address
startingMcapOf(address) → uint256
swapRouter() → address
tokenInitCodeHash() → bytes32
Events (1)
TokenLaunched
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "positionManager_",
"type": "address"
},
{
"internalType": "address",
"name": "swapRouter_",
"type": "address"
},
{
"internalType": "address",
"name": "weth_",
"type": "address"
},
{
"internalType": "address payable",
"name": "locker_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "InvalidParams",
"type": "error"
},
{
"inputs": [],
"name": "McapOutOfBounds",
"type": "error"
},
{
"inputs": [],
"name": "NoPendingLaunch",
"type": "error"
},
{
"inputs": [],
"name": "PoolPriceMismatch",
"type": "error"
},
{
"inputs": [],
"name": "Reentrancy",
"type": "error"
},
{
"inputs": [],
"name": "SqrtPriceOutOfRange",
"type": "error"
},
{
"inputs": [],
"name": "TransferFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "VanitySuffixMismatch",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "creatorWallet",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "startingMcapWei",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "devBuyWei",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "devBuyTokensOut",
"type": "uint256"
}
],
"name": "TokenLaunched",
"type": "event"
},
{
"inputs": [],
"name": "MAX_STARTING_MCAP",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MIN_STARTING_MCAP",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_SPACING",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOTAL_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VANITY_SUFFIX",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "WETH",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allTokens",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "allTokensLength",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "startingMcapWei",
"type": "uint256"
}
],
"name": "computeSqrtPriceX96",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "deployer",
"type": "address"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"name": "computeTokenAddress",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "creatorOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metaURI",
"type": "string"
},
{
"internalType": "uint256",
"name": "startingMcapWei",
"type": "uint256"
},
{
"internalType": "address",
"name": "creatorWallet",
"type": "address"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "devBuyMinOut",
"type": "uint256"
}
],
"internalType": "struct StoxesFunV3LaunchFactory.LaunchParams",
"name": "params",
"type": "tuple"
}
],
"name": "launch",
"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": "launchParams",
"outputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metaURI",
"type": "string"
},
{
"internalType": "address",
"name": "devBuyer",
"type": "address"
},
{
"internalType": "address",
"name": "weth",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "locker",
"outputs": [
{
"internalType": "contract StoxesFunV3Locker",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "poolOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "positionIdOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "startingMcapOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "swapRouter",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenInitCodeHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {ReentrancyGuardLite, SafeERC20Lite} from "./base/Common.sol";
import {FixedPointMathLib} from "./vendor/FixedPointMathLib.sol";
import {StoxesFunSimpleToken} from "./StoxesFunSimpleToken.sol";
import {StoxesFunV3Locker} from "./StoxesFunV3Locker.sol";
import {
INonfungiblePositionManager,
IUniswapV3PoolMinimal,
ISwapRouter02
} from "./interfaces/IUniswapV3.sol";
import {IWETH} from "./interfaces/IWETH.sol";
interface IERC20ApproveMinimal {
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
/// @title StoxesFunV3LaunchFactory — the stoxes.fun NON-taxed launch entry point
/// @author stoxes.fun
/// @notice One-transaction bow.fun-style launch: deploys an immutable, non-taxed
/// ERC-20 at a CREATE2 vanity address (suffix 0x5afe), creates + prices
/// its Uniswap V3 pool (1% fee tier) at the creator's chosen starting
/// market cap, mints the ENTIRE supply as a single-sided V3 position
/// straight into the permanent Locker, and optionally lands the
/// creator's dev buy — all atomically.
///
/// Key properties:
/// - The token constructor takes no args (deployer-callback pattern),
/// so the CREATE2 init-code hash is constant and salts can be mined
/// in the browser once per build, independent of name/metadata.
/// - The mined salt is bound to msg.sender (salt = keccak(sender,salt)),
/// so a mempool front-runner cannot steal a mined vanity address.
/// - The vanity suffix is ENFORCED on-chain: a launch whose resulting
/// address does not end in 0x5afe reverts.
/// - No LP is ever held by the factory or creator: the position NFT
/// is minted directly to the Locker, which has no exit path.
/// - Revenue is the pool's 1% swap fee only (17.5% creator / 17.5%
/// buyback-and-burn / 65% platform, split by the Locker's `collect`).
contract StoxesFunV3LaunchFactory is ReentrancyGuardLite {
using SafeERC20Lite for address;
/// @notice Fixed launch supply (mirrors the token's constant).
uint256 public constant TOTAL_SUPPLY = 1e9 ether;
/// @notice The 1% Uniswap V3 fee tier used for every launch.
uint24 public constant POOL_FEE = 10_000;
/// @notice Tick spacing of the 1% fee tier.
int24 public constant TICK_SPACING = 200;
/// @dev Max usable tick aligned down to the 1% tier's spacing (887272 -> 887200).
int24 internal constant MAX_ALIGNED_TICK = 887_200;
/// @dev Uniswap V3 TickMath bounds for sqrtPriceX96 sanity checks.
uint160 internal constant MIN_SQRT_RATIO = 4_295_128_739;
uint160 internal constant MAX_SQRT_RATIO =
1_461_446_703_485_210_103_287_273_052_203_988_822_378_723_970_342;
/// @notice Every launched token address must end in this suffix ("SAFE").
uint16 public constant VANITY_SUFFIX = 0x5afe;
/// @notice Starting market cap (FDV) bounds, in ETH wei. Keeps the price
/// math well inside V3's representable range.
uint256 public constant MIN_STARTING_MCAP = 0.1 ether;
uint256 public constant MAX_STARTING_MCAP = 1_000_000 ether;
/// @notice The existing verified Uniswap V3 periphery on Robinhood Chain.
address public immutable positionManager;
address public immutable swapRouter;
address public immutable WETH;
/// @notice The permanent liquidity locker (fee split lives there).
StoxesFunV3Locker public immutable locker;
/// @notice All launched non-taxed tokens, in launch order.
address[] public allTokens;
/// @notice token => V3 pool / creator / locked position id lookups.
mapping(address => address) public poolOf;
mapping(address => address) public creatorOf;
mapping(address => uint256) public positionIdOf;
mapping(address => uint256) public startingMcapOf;
struct LaunchParams {
string name;
string symbol;
string metaURI; // stoxes.fun-hosted metadata JSON (socials, logo)
uint256 startingMcapWei; // starting market cap (FDV) in ETH wei
address creatorWallet; // immutable 35% fee wallet, locked in the Locker
bytes32 salt; // browser-mined so the token address ends in 0x5afe
uint256 devBuyMinOut; // slippage floor for the optional dev buy (msg.value)
}
/// @dev Transient constructor params served to the token via `launchParams()`.
struct PendingLaunch {
string name;
string symbol;
string metaURI;
address devBuyer;
bool active;
}
PendingLaunch private _pending;
event TokenLaunched(
address indexed token,
address indexed creator,
address indexed pool,
address creatorWallet,
uint256 positionId,
uint256 startingMcapWei,
uint256 devBuyWei,
uint256 devBuyTokensOut
);
error InvalidParams();
error McapOutOfBounds();
error VanitySuffixMismatch(address token);
error NoPendingLaunch();
error SqrtPriceOutOfRange();
error PoolPriceMismatch();
constructor(address positionManager_, address swapRouter_, address weth_, address payable locker_) {
if (
positionManager_ == address(0) || swapRouter_ == address(0) || weth_ == address(0)
|| locker_ == address(0)
) revert InvalidParams();
positionManager = positionManager_;
swapRouter = swapRouter_;
WETH = weth_;
locker = StoxesFunV3Locker(locker_);
}
/// @notice Constructor-param callback read by the token being deployed.
/// Only meaningful during the launch transaction.
function launchParams()
external
view
returns (
string memory name,
string memory symbol,
string memory metaURI,
address devBuyer,
address weth
)
{
if (!_pending.active) revert NoPendingLaunch();
return (_pending.name, _pending.symbol, _pending.metaURI, _pending.devBuyer, WETH);
}
/// @notice Launches a non-taxed token. Fully self-serve; send ETH for an
/// optional atomic dev buy (exempt from the anti-snipe max wallet).
function launch(LaunchParams calldata params)
external
payable
nonReentrant
returns (address token, address pool, uint256 positionId)
{
if (bytes(params.name).length == 0 || bytes(params.symbol).length == 0) {
revert InvalidParams();
}
if (params.creatorWallet == address(0)) revert InvalidParams();
if (
params.startingMcapWei < MIN_STARTING_MCAP
|| params.startingMcapWei > MAX_STARTING_MCAP
) revert McapOutOfBounds();
// 1. CREATE2-deploy the token at the mined vanity address. The salt is
// bound to the caller so a front-runner cannot steal it.
_pending = PendingLaunch({
name: params.name,
symbol: params.symbol,
metaURI: params.metaURI,
devBuyer: msg.value > 0 ? msg.sender : address(0),
active: true
});
token = address(
new StoxesFunSimpleToken{salt: keccak256(abi.encode(msg.sender, params.salt))}()
);
delete _pending;
if (uint16(uint160(token)) != VANITY_SUFFIX) revert VanitySuffixMismatch(token);
// 2. Create + initialize the 1% pool at the chosen starting market cap.
// If the pool already existed (pre-created + initialized by a griefer
// at a hostile price), `createAndInitializePoolIfNecessary` silently
// keeps the existing price — verify the pool actually trades at the
// intended starting price and revert otherwise.
uint160 intendedSqrtPriceX96 = computeSqrtPriceX96(token, params.startingMcapWei);
pool = INonfungiblePositionManager(positionManager).createAndInitializePoolIfNecessary(
token < WETH ? token : WETH,
token < WETH ? WETH : token,
POOL_FEE,
intendedSqrtPriceX96
);
(uint160 realizedSqrtPriceX96,,,,,,) = IUniswapV3PoolMinimal(pool).slot0();
if (realizedSqrtPriceX96 != intendedSqrtPriceX96) revert PoolPriceMismatch();
// 3. Finish the token's one-shot wiring (exemptions, then pool last —
// setPool freezes the exemption list forever).
StoxesFunSimpleToken(token).setMaxWalletExempt(address(locker), true);
StoxesFunSimpleToken(token).setMaxWalletExempt(positionManager, true);
StoxesFunSimpleToken(token).setPool(pool);
// 4. Mint the ENTIRE supply as a single-sided position directly to the
// Locker, burn any rounding dust the mint left behind, and record
// the lock (fee wallet is immutable in the Locker from here on).
positionId = _lockSupply(token, pool);
locker.register(token, positionId, params.creatorWallet);
// 5. Optional atomic dev buy: first buy in the pool, exempt from the
// anti-snipe max wallet, delivered straight to the creator.
uint256 devBuyTokensOut =
msg.value > 0 ? _devBuy(token, params.devBuyMinOut) : 0;
allTokens.push(token);
poolOf[token] = pool;
creatorOf[token] = msg.sender;
positionIdOf[token] = positionId;
startingMcapOf[token] = params.startingMcapWei;
emit TokenLaunched(
token,
msg.sender,
pool,
params.creatorWallet,
positionId,
params.startingMcapWei,
msg.value,
devBuyTokensOut
);
}
/// @dev Mints the factory's ENTIRE token balance as a single-sided V3
/// position (token-only range from the starting price upwards)
/// directly to the Locker, then burns any rounding dust so the
/// factory ends the launch holding ZERO tokens.
function _lockSupply(address token, address pool) internal returns (uint256 tokenId) {
bool tokenIsToken0 = token < WETH;
IERC20ApproveMinimal(token).approve(positionManager, TOTAL_SUPPLY);
(int24 tickLower, int24 tickUpper) = _singleSidedTokenRange(pool, tokenIsToken0);
(tokenId,,,) = INonfungiblePositionManager(positionManager).mint(
INonfungiblePositionManager.MintParams({
token0: tokenIsToken0 ? token : WETH,
token1: tokenIsToken0 ? WETH : token,
fee: POOL_FEE,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: tokenIsToken0 ? TOTAL_SUPPLY : 0,
amount1Desired: tokenIsToken0 ? 0 : TOTAL_SUPPLY,
amount0Min: 0,
amount1Min: 0,
recipient: address(locker),
deadline: block.timestamp
})
);
uint256 dust = IERC20ApproveMinimal(token).balanceOf(address(this));
if (dust > 0) token.safeTransfer(address(0xdead), dust);
}
/// @dev Wraps the sent ETH and swaps it through the fresh pool to the
/// launching creator (msg.sender) — the pool's first buy.
function _devBuy(address token, uint256 minOut) internal returns (uint256 amountOut) {
IWETH(WETH).deposit{value: msg.value}();
IERC20ApproveMinimal(WETH).approve(swapRouter, msg.value);
amountOut = ISwapRouter02(swapRouter).exactInputSingle(
ISwapRouter02.ExactInputSingleParams({
tokenIn: WETH,
tokenOut: token,
fee: POOL_FEE,
recipient: msg.sender,
amountIn: msg.value,
amountOutMinimum: minOut,
sqrtPriceLimitX96: 0
})
);
}
// ------------------------------------------------------------------
// Price / range math
// ------------------------------------------------------------------
/// @notice The pool's initial sqrtPriceX96 pricing the FULL supply at
/// `startingMcapWei` (FDV in ETH wei). Handles both token orderings.
/// @dev price = token1-per-token0 in raw (18-dec) units:
/// token is token1: price = SUPPLY / mcap
/// token is token0: price = mcap / SUPPLY
/// sqrtPriceX96 = sqrt(price * 1e36) * 2^96 / 1e18 — the 1e36
/// scaling keeps ~1e-11 relative precision on the sqrt.
function computeSqrtPriceX96(address token, uint256 startingMcapWei)
public
view
returns (uint160)
{
(uint256 num, uint256 den) = token < WETH
? (startingMcapWei, TOTAL_SUPPLY) // token is token0: WETH per token
: (TOTAL_SUPPLY, startingMcapWei); // token is token1: token per WETH
uint256 ratioX36 = FixedPointMathLib.mulDiv(num, 1e36, den);
uint256 sqrtPrice = (FixedPointMathLib.sqrt(ratioX36) * (1 << 96)) / 1e18;
if (sqrtPrice <= MIN_SQRT_RATIO || sqrtPrice >= MAX_SQRT_RATIO) {
revert SqrtPriceOutOfRange();
}
return uint160(sqrtPrice);
}
/// @dev The single-sided token-only tick range for the launch position:
/// token is token0: [first aligned tick ABOVE current, +max]
/// token is token1: [-max, last aligned tick AT/BELOW current]
/// Reads the pool's own post-initialize tick (no TickMath port).
function _singleSidedTokenRange(address pool, bool tokenIsToken0)
internal
view
returns (int24 tickLower, int24 tickUpper)
{
(, int24 tick,,,,,) = IUniswapV3PoolMinimal(pool).slot0();
if (tokenIsToken0) {
tickLower = _alignUp(tick + 1);
tickUpper = MAX_ALIGNED_TICK;
} else {
tickLower = -MAX_ALIGNED_TICK;
tickUpper = _alignDown(tick);
}
if (tickLower >= tickUpper) revert SqrtPriceOutOfRange();
}
/// @dev Smallest multiple of TICK_SPACING >= t (handles negatives).
function _alignUp(int24 t) internal pure returns (int24) {
int24 aligned = (t / TICK_SPACING) * TICK_SPACING;
if (aligned < t) aligned += TICK_SPACING;
return aligned;
}
/// @dev Largest multiple of TICK_SPACING <= t (handles negatives).
function _alignDown(int24 t) internal pure returns (int24) {
int24 aligned = (t / TICK_SPACING) * TICK_SPACING;
if (aligned > t) aligned -= TICK_SPACING;
return aligned;
}
// ------------------------------------------------------------------
// Views
// ------------------------------------------------------------------
function allTokensLength() external view returns (uint256) {
return allTokens.length;
}
/// @notice The CREATE2 address `salt` would produce for `deployer` — the
/// browser miner's ground truth (staticCall to sanity-check a salt
/// before submitting).
function computeTokenAddress(address deployer, bytes32 salt) external view returns (address) {
bytes32 innerSalt = keccak256(abi.encode(deployer, salt));
bytes32 initCodeHash = keccak256(type(StoxesFunSimpleToken).creationCode);
return address(
uint160(
uint256(
keccak256(
abi.encodePacked(bytes1(0xff), address(this), innerSalt, initCodeHash)
)
)
)
);
}
/// @notice The constant init-code hash the in-browser salt miner needs.
function tokenInitCodeHash() external pure returns (bytes32) {
return keccak256(type(StoxesFunSimpleToken).creationCode);
}
}
Chain explorer2233msChain node102ms