RunnerV3

0x3e2b5a5975ea2be330589d50b2df42e638350567

Verification
Verified
v0.8.30+commit.73712a01
Type
Contract
13,350 bytes
ABI entries
61
21 read · 11 write
License
none

Contract information

Address
0x3e2b5a5975ea2be330589d50b2df42e638350567
Chain
Robinhood Chain (4663)
Compiler
v0.8.30+commit.73712a01
Optimization
Enabled
Creation tx
0x0d53da48fd…adb3d19fc1

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (21)

BPS()uint256
DEAD()address
POOL_FEE()uint24
TOTAL_SUPPLY()uint256
accruedFees()uint256
allTokens(uint256)address
bonded(address)bool
creationFee()uint256
creationPaused()bool
creatorFees(address)uint256
creatorShareBps()uint16
getLaunch(address)tuple
launches(address)address, address, uint256, uint16, bool, bool
owner()address
pendingOwner()address
poolState(address)address, address, bool, uint160, int24, uint128
positionManager()address
renounceOwnership()
tokenCount()uint256
treasury()address
weth()address

Events (10)

CreationFeeUpdatedCreationPausedSetCreatorFeesClaimedCreatorShareUpdatedFeesCollectedLpFeesCollectedOwnershipTransferStartedOwnershipTransferredTokenCreatedTreasuryUpdated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "creationFee_",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "uint16",
        "name": "creatorShareBps_",
        "type": "uint16"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "CannotRenounce",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CreationFeeTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CreationIsPaused",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CreatorShareTooLow",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientCreationFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidShare",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoDirectEth",
    "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": "PoolPriceOutOfRange",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Unauthorized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "CreationFeeUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "name": "CreationPausedSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "CreatorFeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "creatorShareBps",
        "type": "uint16"
      }
    ],
    "name": "CreatorShareUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethToCreator",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethToTreasury",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokToCreator",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokToTreasury",
        "type": "uint256"
      }
    ],
    "name": "LpFeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "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": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "name": "TokenCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      }
    ],
    "name": "TreasuryUpdated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEAD",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POOL_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "accruedFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "allTokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "bonded",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimCreatorFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "collectFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "collectLpFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "salt",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maxCreationFee",
        "type": "uint256"
      },
      {
        "internalType": "uint16",
        "name": "minCreatorShareBps",
        "type": "uint16"
      }
    ],
    "name": "createToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creationFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creationPaused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "creatorFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creatorShareBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "getLaunch",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "creator",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "pool",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "lpTokenId",
            "type": "uint256"
          },
          {
            "internalType": "uint16",
            "name": "creatorLpShareBps",
            "type": "uint16"
          },
          {
            "internalType": "bool",
            "name": "wethIsToken0",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "exists",
            "type": "bool"
          }
        ],
        "internalType": "struct RunnerV3.Launch",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "launches",
    "outputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "lpTokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint16",
        "name": "creatorLpShareBps",
        "type": "uint16"
      },
      {
        "internalType": "bool",
        "name": "wethIsToken0",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "exists",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "poolState",
    "outputs": [
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "isBonded",
        "type": "bool"
      },
      {
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      },
      {
        "internalType": "int24",
        "name": "tick",
        "type": "int24"
      },
      {
        "internalType": "uint128",
        "name": "liquidity",
        "type": "uint128"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "setCreationFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "name": "setCreationPaused",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "bps",
        "type": "uint16"
      }
    ],
    "name": "setCreatorShareBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "t",
        "type": "address"
      }
    ],
    "name": "setTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "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"
  },
  {
    "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": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {RunnerToken} from "./RunnerToken.sol";
import {IWETH, IUniswapV3Pool, INonfungiblePositionManager} from "./interfaces/IUniswapV3.sol";

/// @title RunnerV3 — launchpad that lists tokens on Uniswap V3 FROM BLOCK ONE
///
/// PROTOTYPE / not deployed. Design spec: docs/SINGLE_SIDED_V3_LAUNCHPAD.md.
///
/// Motivation
/// ----------
/// The live `Runner.sol` trades against a virtual-reserve curve and only mints a
/// real Uniswap pool at graduation, so pre-bond tokens are invisible to
/// DexScreener / DEX trackers ("nobody can find them"). RunnerV3 instead launches
/// each token straight into a real Uniswap V3 pool, so it is discoverable
/// immediately — while still capturing fees and staying rug-proof.
///
/// How it works
/// ------------
/// 1. `createToken` deploys the token (fixed 1B supply, minted here), creates the
///    (token, WETH, 1%) pool, and mints ONE **single-sided** position holding the
///    entire supply as tokens (no ETH). Because a token-only V3 range can only
///    SELL tokens as price rises, that position *is* the bonding curve — expressed
///    as real Uniswap liquidity. Buyers swap ETH in on Uniswap; the price walks up
///    the range exactly like the old curve (same launch/"bond" endpoints).
/// 2. **Liquidity is locked from block one.** This contract holds the position NFT
///    and there is NO `decreaseLiquidity`/withdraw path anywhere — principal can
///    never be pulled (rug-proof), the same guarantee the live contract only
///    reaches at graduation. Nothing to migrate, no second market.
/// 3. **Fees:** every swap pays the pool's 1% fee. This contract is the sole LP
///    *by default*, so that fee accrues to our position; `collectLpFees` sweeps
///    it and splits creator/treasury like the live contract's post-grad logic.
///    NOTE: sole-LP status is NOT enforceable — anyone may add liquidity (incl.
///    just-in-time) to the same canonical pool and take a pro-rata share of
///    FUTURE fees. That dilutes revenue but is not theft (Uniswap's per-position
///    fee accounting means outsiders can't touch our already-accrued fees, and
///    principal stays locked). A V4 hook would make capture enforceable if/when
///    V4 ships on this chain. (Affiliate rewards stay off-chain, unchanged.)
/// 4. `bonded(token)` is a read-only milestone (price crossed the ~48-ETH-FDV
///    tick) for the UI/indexers — it changes no liquidity.
///
/// FRONT-RUN DEFENSE: identical to `Runner.sol`. A griefer can pre-create the
/// deterministic (token, WETH, 1%) pool at a bad price. `_syncPoolPrice` re-reads
/// the live price and nudges it toward target with a DUST-BOUNDED (1-wei
/// exact-input) corrective swap before seeding; a zero-liquidity pre-init walks to
/// target for ~free, a funded mis-price is either within tolerance or reverts
/// (griefing DoS, never theft — the 1-wei input structurally caps our spend and
/// the callback is locked to the pool being synced).
///
/// Single-sided tick constants are the SAME for every token per address ordering
/// (all tokens share params), so they are precomputed off-chain and hardcoded
/// below rather than derived on-chain — see docs + the fork test that asserts the
/// position lands single-sided at the intended price.
contract RunnerV3 is Ownable2Step, ReentrancyGuard {
    // ------------------------------------------------------------- constants

    uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
    uint256 public constant BPS = 10_000;
    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;

    /// 1% fee tier (memecoin standard; tick spacing 200).
    uint24 public constant POOL_FEE = 10_000;

    // Single-sided range, precomputed for the token's ETH price walking from
    // P0 = 3.24e-9 (launch, FDV ~3.24 ETH) up to P_TOP = 2.12e-4 (FDV ~212k ETH),
    // with the ~48-ETH-FDV "bond" milestone at P1 = 4.82e-8 in between. Two
    // orderings because the token/WETH pair orders by address:
    //
    // Ordering A — WETH < token  (token0 = WETH, token1 = token):
    //   pool price = token/WETH, so the token APPRECIATES as the tick FALLS.
    //   Single-sided token (=token1) ⇒ init at the UPPER tick (position all token1).
    int24 internal constant A_TICK_LOWER = 84_400;
    int24 internal constant A_TICK_UPPER = 195_600;
    uint160 internal constant A_INIT_SQRT = 1_399_804_099_006_039_538_398_973_723_506_460; // sqrtRatio(195600)
    int24 internal constant A_BOND_TICK = 168_400; // bonded once tick <= this

    // Ordering B — token < WETH  (token0 = token, token1 = WETH):
    //   pool price = WETH/token, so the token APPRECIATES as the tick RISES.
    //   Single-sided token (=token0) ⇒ init at the LOWER tick (position all token0).
    int24 internal constant B_TICK_LOWER = -195_600;
    int24 internal constant B_TICK_UPPER = -84_400;
    uint160 internal constant B_INIT_SQRT = 4_484_271_577_604_230_056_945_578; // sqrtRatio(-195600)
    int24 internal constant B_BOND_TICK = -168_400; // bonded once tick >= this

    /// Backstop tolerance between the live pool price and our target after the
    /// corrective swap (bps).
    uint256 internal constant INIT_PRICE_TOLERANCE_BPS = 100; // 1%
    /// Hard cap on what the 1-wei corrective swap may consume of either token.
    uint256 internal constant CORRECTIVE_SWAP_MAX_SPEND = 100; // wei

    // ----------------------------------------------------------------- types

    struct Launch {
        address creator;
        address pool;
        uint256 lpTokenId; // locked position NFT — never decreased
        uint16 creatorLpShareBps; // creator's fee share, snapshotted at creation
        bool wethIsToken0;
        bool exists;
    }

    // ----------------------------------------------------------------- state

    INonfungiblePositionManager public immutable positionManager;
    address public immutable weth;

    address public treasury;
    uint256 public creationFee;
    uint16 public creatorShareBps;
    bool public creationPaused;

    uint256 public accruedFees; // treasury pull balance (ETH)
    mapping(address => uint256) public creatorFees; // creator pull balances (ETH)
    mapping(address => Launch) public launches;
    address[] public allTokens;

    address private _pendingPool; // swap-callback guard

    // ---------------------------------------------------------------- events

    event TokenCreated(
        address indexed token, address indexed creator, string name, string symbol, string metadataURI, address pool
    );
    event LpFeesCollected(
        address indexed token, uint256 ethToCreator, uint256 ethToTreasury, uint256 tokToCreator, uint256 tokToTreasury
    );
    event CreatorFeesClaimed(address indexed creator, uint256 amount);
    event FeesCollected(address indexed treasury, uint256 amount);
    event CreationFeeUpdated(uint256 fee);
    event TreasuryUpdated(address treasury);
    event CreatorShareUpdated(uint16 creatorShareBps);
    event CreationPausedSet(bool paused);

    // ---------------------------------------------------------------- errors

    error ZeroAddress();
    error InvalidShare();
    error CreationIsPaused();
    error InsufficientCreationFee();
    error CreationFeeTooHigh();
    error CreatorShareTooLow();
    error CannotRenounce();
    error UnknownToken();
    error ZeroAmount();
    error EthTransferFailed();
    error TokenTransferFailed();
    error PoolPriceOutOfRange();
    error Unauthorized();
    error NoDirectEth();

    // ----------------------------------------------------------- constructor

    constructor(
        address treasury_,
        uint256 creationFee_,
        address positionManager_,
        address weth_,
        uint16 creatorShareBps_
    ) Ownable(msg.sender) {
        if (treasury_ == address(0) || positionManager_ == address(0) || weth_ == address(0)) revert ZeroAddress();
        if (creatorShareBps_ > BPS) revert InvalidShare();
        treasury = treasury_;
        creationFee = creationFee_;
        positionManager = INonfungiblePositionManager(positionManager_);
        weth = weth_;
        creatorShareBps = creatorShareBps_;
    }

    /// Only WETH (on unwrap) may send ETH directly.
    receive() external payable {
        if (msg.sender != weth) revert NoDirectEth();
    }

    // ------------------------------------------------------------- create

    /// @notice Launch a token straight onto Uniswap. `msg.value` must cover
    /// `creationFee` (any surplus is refunded — dev-buys happen on the pool).
    /// `salt` makes the token address per-(sender,salt) — retry with a fresh salt
    /// if a launch reverts (see the C-1 note below). `maxCreationFee` /
    /// `minCreatorShareBps` are caller-supplied guards against the owner
    /// front-running a worse fee/share (pass type(uint256).max / 0 to waive).
    function createToken(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        uint256 salt,
        uint256 maxCreationFee,
        uint16 minCreatorShareBps
    ) external payable nonReentrant returns (address token) {
        if (creationPaused) revert CreationIsPaused();
        if (msg.value < creationFee) revert InsufficientCreationFee();
        if (creationFee > maxCreationFee) revert CreationFeeTooHigh();
        if (creatorShareBps < minCreatorShareBps) revert CreatorShareTooLow();

        // C-1 FIX — deploy via CREATE2 at a per-(sender,salt) address instead of
        // the sequential CREATE nonce. The old nonce-derived address was globally
        // predictable, so a griefer could pre-seed the deterministic NEXT pool
        // with single-sided WETH liquidity (Uniswap only checks balanceOf on the
        // non-zero side, so a WETH-only range never touches the not-yet-deployed
        // token). That funded mis-price made createToken revert; the revert rolled
        // back the nonce; the SAME address recurred forever → the whole launchpad
        // was permanently bricked from block one. A per-(sender,salt) address
        // isolates creators from each other and lets a griefed creator escape by
        // retrying with a fresh salt, reducing the attack to self-healing mempool
        // griefing (the attacker pays gas + WETH each attempt and it stops the
        // moment they do).
        token = address(new RunnerToken{salt: keccak256(abi.encode(msg.sender, salt))}(name, symbol, TOTAL_SUPPLY));
        bool wethIsToken0 = weth < token;

        // Order the pair and pick the single-sided config for this ordering.
        (address token0, address token1) = wethIsToken0 ? (weth, token) : (token, weth);
        (int24 tickLower, int24 tickUpper, uint160 initSqrt) = wethIsToken0
            ? (A_TICK_LOWER, A_TICK_UPPER, A_INIT_SQRT)
            : (B_TICK_LOWER, B_TICK_UPPER, B_INIT_SQRT);

        // Create + initialize the pool at the launch price, then defend against a
        // front-run pre-init by nudging the live price back to target.
        address pool = positionManager.createAndInitializePoolIfNecessary(token0, token1, POOL_FEE, initSqrt);
        _syncPoolPrice(pool, token0, token1, initSqrt);

        // Mint ONE single-sided position holding the entire supply as tokens
        // (no ETH side). This contract keeps the NFT; there is no path to remove
        // the liquidity, so it is locked from this block forward.
        if (!RunnerToken(token).approve(address(positionManager), TOTAL_SUPPLY)) revert TokenTransferFailed();
        (uint256 amount0Desired, uint256 amount1Desired) =
            wethIsToken0 ? (uint256(0), TOTAL_SUPPLY) : (TOTAL_SUPPLY, uint256(0));

        (uint256 tokenId,,,) = positionManager.mint(
            INonfungiblePositionManager.MintParams({
                token0: token0,
                token1: token1,
                fee: POOL_FEE,
                tickLower: tickLower,
                tickUpper: tickUpper,
                amount0Desired: amount0Desired,
                amount1Desired: amount1Desired,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(this),
                deadline: block.timestamp
            })
        );

        RunnerToken(token).approve(address(positionManager), 0);

        launches[token] = Launch({
            creator: msg.sender,
            pool: pool,
            lpTokenId: tokenId,
            creatorLpShareBps: creatorShareBps,
            wethIsToken0: wethIsToken0,
            exists: true
        });
        allTokens.push(token);

        // Any token dust the single-sided mint didn't consume is burned (fair
        // launch — the launchpad keeps no token bag).
        uint256 leftover = RunnerToken(token).balanceOf(address(this));
        if (leftover > 0 && !RunnerToken(token).transfer(DEAD, leftover)) revert TokenTransferFailed();

        if (creationFee > 0) accruedFees += creationFee;
        uint256 refund = msg.value - creationFee;
        if (refund > 0) _sendEth(msg.sender, refund);

        emit TokenCreated(token, msg.sender, name, symbol, metadataURI, pool);
    }

    // --------------------------------------------------------------- fees

    /// @notice Collect the pool's accrued 1% swap fees (this contract is the sole
    /// LP) and split them creator/treasury using the share snapshotted at
    /// creation. ETH side accrues to pull balances; token side is pushed out.
    /// Callable by anyone.
    function collectLpFees(address token) external nonReentrant {
        Launch storage L = launches[token];
        if (!L.exists) revert UnknownToken();
        address creator = L.creator;
        uint256 share = L.creatorLpShareBps;

        (uint256 amount0, uint256 amount1) = positionManager.collect(
            INonfungiblePositionManager.CollectParams({
                tokenId: L.lpTokenId,
                recipient: address(this),
                amount0Max: type(uint128).max,
                amount1Max: type(uint128).max
            })
        );

        (uint256 wethAmt, uint256 tokAmt) = L.wethIsToken0 ? (amount0, amount1) : (amount1, amount0);

        uint256 ethToCreator;
        uint256 ethToTreasury;
        if (wethAmt > 0) {
            IWETH(weth).withdraw(wethAmt);
            ethToCreator = (wethAmt * share) / BPS;
            ethToTreasury = wethAmt - ethToCreator;
            if (ethToCreator > 0) creatorFees[creator] += ethToCreator;
            accruedFees += ethToTreasury;
        }

        uint256 tokToCreator;
        uint256 tokToTreasury;
        if (tokAmt > 0) {
            tokToCreator = (tokAmt * share) / BPS;
            tokToTreasury = tokAmt - tokToCreator;
            if (tokToCreator > 0 && !RunnerToken(token).transfer(creator, tokToCreator)) revert TokenTransferFailed();
            if (tokToTreasury > 0 && !RunnerToken(token).transfer(treasury, tokToTreasury)) revert TokenTransferFailed();
        }

        emit LpFeesCollected(token, ethToCreator, ethToTreasury, tokToCreator, tokToTreasury);
    }

    /// @notice Claim accrued creator fees (ETH) for msg.sender.
    function claimCreatorFees() external nonReentrant {
        uint256 amount = creatorFees[msg.sender];
        if (amount == 0) revert ZeroAmount();
        creatorFees[msg.sender] = 0;
        emit CreatorFeesClaimed(msg.sender, amount);
        _sendEth(msg.sender, amount);
    }

    /// @notice Push accrued protocol fees (creation fees + treasury LP share) to
    /// the treasury. Callable by anyone.
    function collectFees() external nonReentrant {
        uint256 amount = accruedFees;
        if (amount == 0) revert ZeroAmount();
        accruedFees = 0;
        emit FeesCollected(treasury, amount);
        _sendEth(treasury, amount);
    }

    // ------------------------------------------------ front-run defense

    /// If the pool's live price differs from `target`, walk it back with a
    /// dust-bounded 1-wei exact-input swap, then require it landed within
    /// tolerance. Zero-liquidity pre-init walks to target for ~free; a funded
    /// mis-price either sits within tolerance or reverts (griefing DoS, not theft
    /// — exact-input of 1 wei caps our spend and the callback is pool-locked).
    function _syncPoolPrice(address pool, address token0, address token1, uint160 target) internal {
        (uint160 cur,,,,,,) = IUniswapV3Pool(pool).slot0();
        if (cur == target) return;

        uint256 bal0Before = IERC20(token0).balanceOf(address(this));
        uint256 bal1Before = IERC20(token1).balanceOf(address(this));

        _pendingPool = pool;
        IUniswapV3Pool(pool).swap(address(this), cur > target, int256(1), target, abi.encode(token0, token1));
        _pendingPool = address(0);

        uint256 bal0After = IERC20(token0).balanceOf(address(this));
        uint256 bal1After = IERC20(token1).balanceOf(address(this));
        if (bal0Before > bal0After + CORRECTIVE_SWAP_MAX_SPEND) revert PoolPriceOutOfRange();
        if (bal1Before > bal1After + CORRECTIVE_SWAP_MAX_SPEND) revert PoolPriceOutOfRange();

        (uint160 landed,,,,,,) = IUniswapV3Pool(pool).slot0();
        uint256 diff = landed > target ? landed - target : target - landed;
        if (diff * BPS > uint256(target) * INIT_PRICE_TOLERANCE_BPS) revert PoolPriceOutOfRange();
    }

    /// Uniswap V3 swap callback — pays the pool what the 1-wei corrective swap
    /// owes. Locked to the pool being synced during an active createToken.
    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
        if (_pendingPool == address(0) || msg.sender != _pendingPool) revert Unauthorized();
        (address token0, address token1) = abi.decode(data, (address, address));
        if (amount0Delta > 0) _payPool(token0, uint256(amount0Delta));
        if (amount1Delta > 0) _payPool(token1, uint256(amount1Delta));
    }

    function _payPool(address t, uint256 amount) internal {
        bool ok = t == weth ? IWETH(weth).transfer(msg.sender, amount) : RunnerToken(t).transfer(msg.sender, amount);
        if (!ok) revert TokenTransferFailed();
    }

    function _sendEth(address to, uint256 amount) internal {
        (bool ok,) = to.call{value: amount}("");
        if (!ok) revert EthTransferFailed();
    }

    // ---------------------------------------------------------------- views

    /// Live (instantaneous) bond check from the current tick. Token appreciates as
    /// the tick falls (ordering A) or rises (ordering B).
    function _liveBonded(Launch storage L) internal view returns (bool) {
        (, int24 tick,,,,,) = IUniswapV3Pool(L.pool).slot0();
        return L.wethIsToken0 ? (tick <= A_BOND_TICK) : (tick >= B_BOND_TICK);
    }

    /// @notice Is the token's CURRENT spot price past the ~48-ETH-FDV bond tick?
    /// LIVE and instantaneous — deliberately NOT monotonic and NOT a
    /// safety/legitimacy signal. There is no on-chain latch (round-2 audit: a
    /// permanent latch on a spot read could be set true forever by a single
    /// flash-loaned round-trip across the tick), so a transient cross can never
    /// stick. Bonding here is a soft discovery/UI marker that moves no liquidity;
    /// integrators MUST derive any legitimacy/pricing decision from a TWAP, never
    /// from this spot read.
    function bonded(address token) public view returns (bool) {
        Launch storage L = launches[token];
        if (!L.exists) revert UnknownToken();
        return _liveBonded(L);
    }

    /// @notice Current pool spot state for a launched token. `isBonded` /
    /// `sqrtPriceX96` / `tick` / `liquidity` are ALL live spot reads — do not
    /// trust mid-swap; price off a TWAP for anything that matters.
    function poolState(address token)
        external
        view
        returns (address pool, address creator, bool isBonded, uint160 sqrtPriceX96, int24 tick, uint128 liquidity)
    {
        Launch storage L = launches[token];
        if (!L.exists) revert UnknownToken();
        pool = L.pool;
        creator = L.creator;
        (sqrtPriceX96, tick,,,,,) = IUniswapV3Pool(pool).slot0();
        liquidity = IUniswapV3Pool(pool).liquidity();
        isBonded = _liveBonded(L);
    }

    function getLaunch(address token) external view returns (Launch memory) {
        return launches[token];
    }

    function tokenCount() external view returns (uint256) {
        return allTokens.length;
    }

    // ---------------------------------------------------------------- admin

    function setCreationFee(uint256 fee) external onlyOwner {
        creationFee = fee;
        emit CreationFeeUpdated(fee);
    }

    function setTreasury(address t) external onlyOwner {
        if (t == address(0)) revert ZeroAddress();
        treasury = t;
        emit TreasuryUpdated(t);
    }

    /// Applies to tokens launched AFTER the change (each token snapshots its share
    /// at creation, so live creators are never diluted).
    function setCreatorShareBps(uint16 bps) external onlyOwner {
        if (bps > BPS) revert InvalidShare();
        creatorShareBps = bps;
        emit CreatorShareUpdated(bps);
    }

    function setCreationPaused(bool paused) external onlyOwner {
        creationPaused = paused;
        emit CreationPausedSet(paused);
    }

    /// Disabled: renouncing would freeze treasury/fee/pause controls forever and
    /// could permanently strand protocol fees if `treasury` ever can't receive
    /// ETH. Hand ownership to a timelock/Safe via transferOwnership instead.
    function renounceOwnership() public view override onlyOwner {
        revert CannotRenounce();
    }
}
Chain explorer785msChain node80ms