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
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