YoinkLaunchV2

0x3ca1af694d8c1639679e52c75fa18ce9ee09bf3f

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
10,433 bytes
ABI entries
20
16 read · 1 write
License
mit

Contract information

Address
0x3ca1af694d8c1639679e52c75fa18ce9ee09bf3f
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xca02e6158f…6d78128e56

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (16)

INIT_SQRT_PRICE()uint160
LAUNCH_FEE()uint256
LOCKER()address
POOL_FEE()uint24
POS_MGR()address
POS_MGR_ADDR()address
ROUTER()address
ROUTER_ADDR()address
SUPPLY()uint256
TICK_LOWER()int24
TICK_UPPER()int24
TREASURY()address
V3_FACTORY()address
WETH()address
YOINK_ROUTER()address
launchCount()uint256

Events (2)

LaunchedTokenMeta

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "yoinkRouter",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "pairCreatorBps",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenCreatorBps",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenBurnBps",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "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": "uint256",
        "name": "initialBuyEth",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "initialBuyTokens",
        "type": "uint256"
      }
    ],
    "name": "Launched",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "logo",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "description",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string[3]",
        "name": "socials",
        "type": "string[3]"
      }
    ],
    "name": "TokenMeta",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "INIT_SQRT_PRICE",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "",
        "type": "uint160"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LAUNCH_FEE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LOCKER",
    "outputs": [
      {
        "internalType": "contract YoinkLpLockerV2",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POOL_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POS_MGR",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POS_MGR_ADDR",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROUTER",
    "outputs": [
      {
        "internalType": "contract ISwapRouter",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROUTER_ADDR",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_LOWER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_UPPER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TREASURY",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "V3_FACTORY",
    "outputs": [
      {
        "internalType": "contract IUniswapV3Factory",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "WETH",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "YOINK_ROUTER",
    "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": "logo",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "description",
            "type": "string"
          },
          {
            "internalType": "string[3]",
            "name": "socials",
            "type": "string[3]"
          },
          {
            "internalType": "uint256",
            "name": "minTokensOut",
            "type": "uint256"
          }
        ],
        "internalType": "struct YoinkLaunchV2.LaunchParams",
        "name": "p",
        "type": "tuple"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// YoinkLaunchV2 — v2 pool-born launch factory (fork of the audited YoinkLaunch).
//
// STATUS: BUILT 2026-07-11, NOT audited, NOT deployed. This file must pass the full
// adversarial audit gate before the founder deploys it. The deployed/audited v1
// (contracts/YoinkLaunch.sol @ 0xbcc67639cd2c3e9498714be53018949e3b17ea5e) is untouched.
//
// v2 bundles TWO features on top of the proven v1 launch flow (one audit, one deploy):
//
//  (1) CREATOR LP-FEE SPLIT — IMMUTABLE, per-leg configurable (set once at deploy).
//      v1's locker pays 100% of pool trading fees to the immutable TREASURY. v2's locker
//      splits every collect() between TREASURY, the token's CREATOR (recorded at launch,
//      immutable per tokenId), and optionally a token-side BURN — by three bps values
//      fixed FOREVER in the locker's constructor. The parameterization mirrors the
//      verified Noxa mechanism (docs/NOXA_GRADUATION_RESEARCH.md). VALUES FOUNDER-LOCKED
//      2026-07-11 ("match Noxa") — the deploy script MUST pass exactly these:
//        PAIR_CREATOR_BPS  = 3500 — WETH side: 35% creator, 65% treasury
//        TOKEN_CREATOR_BPS = 0    — token side: creator gets nothing
//        TOKEN_BURN_BPS    = 8000 — token side: 80% burned to 0x…dEaD, 20% treasury
//      (kept as constructor args, not hardcoded constants, so the audited bytecode
//      survives a value change without a re-audit; post-deploy verification must read
//      the three getters back on-chain before any site wiring)
//      IMMUTABILITY IS THE FEATURE: Noxa's splits are owner-mutable (their admin changed
//      them once, on-chain-verified); ours cannot be changed by anyone after deploy.
//      Creator payout is best-effort: if it fails, that share joins the TREASURY batch —
//      a broken creator can never brick collection (mirrors YoinkRouterV4._payFee's
//      rejected-referrer fallback). Anyone may trigger collect; destinations are fixed;
//      the locker stays ownerless/keyless (no admin, no setters beyond the factory's
//      one-shot per-tokenId register at launch).
//
//  (2) LAUNCH PROTECTIONS in the token: 2% max-wallet (Noxa parity) + 369-block
//      anti-snipe window. Noxa's exact on-chain mechanism is not fully public; Noxa's
//      numbers (2%, 366 blocks) were Grok-verified from noxa.fun/launch 2026-07-11.
//      We keep the 2% and FOUNDER-LOCKED 2026-07-11 the window at 369 blocks: chain
//      block time measured live at ~100ms/block (1000-block sample, 2026-07-11), so
//      369 blocks is about 37 seconds, matching Noxa's 366. The relaxing curve is OUR
//      design (linear ramp, documented at snipeCap()). Enforced in the token's
//      transfer path with a carefully scoped exemption set (see _t()).
//
// Everything else is the v1 flow verbatim (audited 2026-07-05, docs/AUDIT_YOINKLAUNCH.md):
// one signed tx deploys the token, seeds a 1% Uniswap V3 pool with 100% of supply at
// tick 204200, locks the LP NFT forever, takes the flat launch fee, optional creator
// initial buy.
//
// DEPLOY PARAMETERS (also listed in docs/SPEC_V2_LAUNCH_FACTORY.md):
//   - PAIR_CREATOR_BPS / TOKEN_CREATOR_BPS / TOKEN_BURN_BPS = 3500 / 0 / 8000
//     [FOUNDER-LOCKED 2026-07-11 "match Noxa"] — passed at deploy, verified on-chain after.
//   - MAX_WALLET_BPS = 200 (2%), permanent (never sunsets).  [FOUNDER-LOCKED 2026-07-11]
//   - ANTI_SNIPE_BLOCKS = 369 (~37s at the measured ~100ms/block), SNIPE_FLOOR_BPS =
//     1000 (curve starts at 10% of max wallet = 0.2% of supply) — linear ramp shape.
//     [FOUNDER-LOCKED 2026-07-11: 369, supersedes 60]
//   - Creator initial buy: subject to max-wallet (2%), EXEMPT from the anti-snipe curve.
//   - YOINK_ROUTER constructor arg (our router at deploy time, e.g. V5) — exempt set.

interface IERC20 {
    function approve(address, uint256) external returns (bool);
    function balanceOf(address) external view returns (uint256);
    function transfer(address, uint256) external returns (bool);
}

interface IWETH is IERC20 {
    function deposit() external payable;
}

interface IUniswapV3Factory {
    function createPool(address, address, uint24) external returns (address);
    function getPool(address, address, uint24) external view returns (address);
}

interface IUniswapV3Pool {
    function initialize(uint160) external;
}

interface INonfungiblePositionManager {
    struct MintParams {
        address token0; address token1; uint24 fee;
        int24 tickLower; int24 tickUpper;
        uint256 amount0Desired; uint256 amount1Desired;
        uint256 amount0Min; uint256 amount1Min;
        address recipient; uint256 deadline;
    }
    struct CollectParams { uint256 tokenId; address recipient; uint128 amount0Max; uint128 amount1Max; }
    function mint(MintParams calldata) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
    function collect(CollectParams calldata) external payable returns (uint256 amount0, uint256 amount1);
    function positions(uint256 tokenId) external view returns (
        uint96 nonce, address operator, address token0, address token1, uint24 fee,
        int24 tickLower, int24 tickUpper, uint128 liquidity,
        uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,
        uint128 tokensOwed0, uint128 tokensOwed1
    );
}

interface ISwapRouter {
    struct ExactInputSingleParams {
        address tokenIn; address tokenOut; uint24 fee; address recipient;
        uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96;
    }
    function exactInputSingle(ExactInputSingleParams calldata) external payable returns (uint256);
}

// Read by YoinkTokenV2's constructor (msg.sender is always the factory).
interface IYoinkLaunchV2 {
    function LOCKER() external view returns (address);
    function POS_MGR_ADDR() external view returns (address);
    function ROUTER_ADDR() external view returns (address);
    function YOINK_ROUTER() external view returns (address);
    function TREASURY() external view returns (address);
}

// Fixed-supply ERC-20, 100% minted to the factory at construction. No owner, no mint,
// no pause, no blacklist. v2 adds two transfer-path protections (max-wallet + anti-snipe)
// with a fixed, derived exemption set — there is still no admin and no settable list.
//
// CODEHASH UNIFORMITY (deliberate): every per-launch value (creator, pool, launchBlock,
// name, symbol) lives in STORAGE, not immutables, so the runtime bytecode — and therefore
// EXTCODEHASH — is identical for every token launched by one factory. YoinkRouterV5
// tiers fees by codehash (LAUNCH_TOKEN_CODEHASH); the v2 token gets ONE new codehash for
// the router/indexer to recognize, not one per launch. Shared infra addresses (factory,
// locker, position manager, routers, treasury) are immutables: same values for every
// launch from a given factory, so uniformity holds.
contract YoinkTokenV2 {
    string public name;
    string public symbol;
    uint8 public constant decimals = 18;
    uint256 public immutable totalSupply;

    // ---- launch protections (2% max-wallet Noxa parity; window + curve are ours) ----
    uint256 public constant MAX_WALLET_BPS = 200;      // 2% of supply, permanent  [FOUNDER-LOCKED 2026-07-11]
    uint256 public constant ANTI_SNIPE_BLOCKS = 369;   // window length in blocks ≈ 37s: chain block time
                                                       // measured live 2026-07-11 at ~100ms/block
                                                       // (1000-block sample); Noxa parity is 366
                                                       // [FOUNDER-LOCKED 2026-07-11: 369, supersedes 60]
    uint256 public constant SNIPE_FLOOR_BPS = 1000;    // curve start = 10% of maxWallet = 0.2% of supply  [FOUNDER-LOCKED 2026-07-11]
    uint256 public immutable maxWallet;                // SUPPLY * MAX_WALLET_BPS / 10000 (2e25) — same for all launches

    // ---- shared infra, exempt from wallet caps (same for every launch → immutables) ----
    address public immutable FACTORY;      // holds only mint dust; receives nothing post-launch
    address public immutable LOCKER;       // receives LP fees from the pool on collect
    address public immutable POS_MGR;      // moves supply factory→pool at launch (via callback)
    address public immutable V3_ROUTER;    // canonical SwapRouter; may transiently hold tokens
    address public immutable YOINK_ROUTER; // our router; sell() custodies tokens mid-trade (V5:177)
    address public immutable TREASURY;     // locker fallback sink; must never be cap-blocked

    // ---- per-launch state (storage, NOT immutables — keeps codehash uniform) ----
    address public pool;        // set once by the factory right after createPool
    address public creator;     // launch msg.sender; anti-snipe-exempt, max-wallet-subject
    uint64  public launchBlock; // anti-snipe window start
    mapping(address => uint256) public acquiredInWindow; // cumulative inbound during the window

    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // msg.sender is always YoinkLaunchV2; supply mints to it for pool seeding.
    constructor(string memory n, string memory s, uint256 supply, address creator_) {
        IYoinkLaunchV2 f = IYoinkLaunchV2(msg.sender);
        name = n; symbol = s; totalSupply = supply;
        maxWallet = (supply * MAX_WALLET_BPS) / 10000;
        FACTORY = msg.sender;
        LOCKER = f.LOCKER();
        POS_MGR = f.POS_MGR_ADDR();
        V3_ROUTER = f.ROUTER_ADDR();
        YOINK_ROUTER = f.YOINK_ROUTER();
        TREASURY = f.TREASURY();
        creator = creator_;
        launchBlock = uint64(block.number);
        balanceOf[msg.sender] = supply;
        emit Transfer(address(0), msg.sender, supply);
    }

    // One-shot, factory-only, inside the launch tx (deploy → createPool → setPool → mint).
    // No transfer can happen before it: the factory holds 100% of supply until mint.
    function setPool(address p) external {
        require(msg.sender == FACTORY, "factory");
        require(pool == address(0) && p != address(0), "pool set");
        pool = p;
    }

    function transfer(address to, uint256 v) external returns (bool) { _t(msg.sender, to, v); return true; }

    function approve(address sp, uint256 v) external returns (bool) {
        allowance[msg.sender][sp] = v; emit Approval(msg.sender, sp, v); return true;
    }

    function transferFrom(address f, address to, uint256 v) external returns (bool) {
        uint256 a = allowance[f][msg.sender];
        require(a >= v, "allowance");
        if (a != type(uint256).max) allowance[f][msg.sender] = a - v;
        _t(f, to, v); return true;
    }

    // Recipients exempt from max-wallet + anti-snipe. Every entry either (a) must hold
    // >2% for the system to function, or (b) only ever forwards via transfer() so the
    // END recipient is still checked — parking tokens on an exempt address gains nothing:
    //   pool         — holds ~100% of supply as locked liquidity; buys/sells/mint/collect
    //                  all move tokens to or from it.
    //   POS_MGR      — mints the LP position (pays factory→pool via callback); belt and
    //                  suspenders, it never holds the token in normal flow.
    //   LOCKER       — receives token-side LP fees on collect before forwarding.
    //   FACTORY      — holds mint dust (<1 token); receives nothing post-launch.
    //   V3_ROUTER    — canonical SwapRouter; recipient-of-swap patterns (sweepToken) may
    //                  park tokens transiently; any sweep out lands on a checked wallet.
    //   YOINK_ROUTER — our router; sell() pulls the full sell amount into itself before
    //                  swapping (YoinkRouterV5.sol:177). Without this exemption, cumulative
    //                  sells during the anti-snipe window would lock our own sell path.
    //   TREASURY     — the locker's split/fallback sink; a cap here bricks collection.
    //   address(0) + 0x…dEaD — burn paths (the locker's TOKEN_BURN_BPS leg accrues on
    //                  0x…dEaD without bound; v1 allowed transfer-to-zero; capping burns
    //                  helps nobody — burning only ever reduces the sender's balance).
    function _exempt(address to) internal view returns (bool) {
        return to == pool || to == POS_MGR || to == LOCKER || to == FACTORY
            || to == V3_ROUTER || to == YOINK_ROUTER || to == TREASURY
            || to == address(0) || to == 0x000000000000000000000000000000000000dEaD;
    }

    // Anti-snipe per-wallet acquisition cap, linear ramp (OUR design, Noxa-parity numbers):
    //   cap(delta) = floor + (maxWallet - floor) * delta / ANTI_SNIPE_BLOCKS
    //   floor = maxWallet * SNIPE_FLOOR_BPS / 10000
    // delta 0   → 0.2% of supply (2M tokens)
    // delta 369 → window over, only the permanent 2% max-wallet applies (curve meets it).
    // Rationale: block-zero snipers are limited to 1/10th of max-wallet per wallet; filling
    // a wallet to 2% requires waiting out the window. Cumulative-inbound tracking (not
    // per-buy) means splitting buys across blocks doesn't reset the cap, and counting ALL
    // inbound transfers (not just pool buys) blocks the buy-on-N-wallets-then-consolidate
    // bypass. "Grief-by-filling-someone's-cap" costs the attacker real tokens the victim
    // keeps, so it is economically self-defeating. Multi-wallet sybil buying remains
    // possible — inherent to any per-wallet rule; the cap raises its cost 10x at block 0.
    function snipeCap(uint256 delta) public view returns (uint256) {
        uint256 floorAmt = (maxWallet * SNIPE_FLOOR_BPS) / 10000;
        if (delta >= ANTI_SNIPE_BLOCKS) return maxWallet;
        return floorAmt + ((maxWallet - floorAmt) * delta) / ANTI_SNIPE_BLOCKS;
    }

    // Current per-wallet acquisition cap (UI/handler helper).
    function buyCapNow() external view returns (uint256) {
        return snipeCap(block.number - launchBlock);
    }

    function _t(address f, address to, uint256 v) internal {
        require(balanceOf[f] >= v, "balance");
        // Sender exemption is the LOCKER ONLY: its outbound transfers are the LP-fee
        // payouts to fixed destinations (creator, or treasury fallback) — it exposes no
        // arbitrary-transfer surface. Consequence (documented, AUDIT-GATE FOCUS): a
        // creator's earned token-side fees can push them past maxWallet. That is earned
        // yield on their own launch, not a market buy; accepted by design.
        if (f != LOCKER && !_exempt(to)) {
            require(balanceOf[to] + v <= maxWallet, "max wallet");
            // Creator is exempt from the anti-snipe curve only (their declared initial
            // buy lands at launchBlock and is visible on the launch card) but stays
            // subject to the 2% max-wallet above. The exemption spans the WHOLE window
            // (all creator inbound, not just the initial buy) — audit finding S2-2,
            // accepted as-built.  [FOUNDER-LOCKED 2026-07-11]
            if (to != creator) {
                uint256 delta = block.number - launchBlock;
                if (delta < ANTI_SNIPE_BLOCKS) {
                    uint256 acc = acquiredInWindow[to] + v;
                    require(acc <= snipeCap(delta), "anti-snipe");
                    acquiredInWindow[to] = acc;
                }
            }
        }
        unchecked { balanceOf[f] -= v; balanceOf[to] += v; }
        emit Transfer(f, to, v);
    }
}

// Holds LP NFTs forever. The only action is collect(): claim accrued pool trading fees
// and split them treasury / creator / burn by bps values FIXED FOREVER at construction.
// Anyone may trigger; destinations are fixed; there is no owner, no transfer, no
// decreaseLiquidity — liquidity can never be pulled and the split can never be changed.
// (Noxa's locker splits are owner-mutable and their admin has changed them on-chain —
// docs/NOXA_GRADUATION_RESEARCH.md. Our immutability is a deliberate trust feature.)
//
// Leg convention: for every position this locker is minted (factory launches), token0 is
// ALWAYS WETH and token1 the launched token (the factory's CREATE2 salt search guarantees
// token > WETH). PAIR_* bps apply to token0/amount0, TOKEN_* bps to token1/amount1.
// Donated foreign NFTs (never registered) have no creator: 100% of both legs → treasury,
// no burn — v1 behavior.
//
// Creator payout is best-effort: fees are collected to the locker first, then forwarded
// with a non-reverting transfer. If the creator payout fails, that share joins the
// treasury batch — mirrors YoinkRouterV4._payFee's rejected-referrer fallback. Collection
// can never be bricked by a hostile creator or token.
contract YoinkLpLockerV2 {
    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;

    INonfungiblePositionManager public immutable POS_MGR;
    address public immutable TREASURY;
    address public immutable FACTORY;   // the only address that can register creators

    // ---- the split, immutable forever [FOUNDER-LOCKED 2026-07-11: 3500 / 0 / 8000] ----
    uint256 public immutable PAIR_CREATOR_BPS;  // WETH-side → creator; remainder → treasury
    uint256 public immutable TOKEN_CREATOR_BPS; // token-side → creator
    uint256 public immutable TOKEN_BURN_BPS;    // token-side → 0x…dEaD; remainder → treasury

    mapping(uint256 => address) public creatorOf; // tokenId → launch creator, one-shot

    uint256 private _lock = 1;
    modifier nonReentrant() { require(_lock == 1, "reentrant"); _lock = 2; _; _lock = 1; }

    event Collected(uint256 indexed tokenId, address indexed creator, uint256 amount0, uint256 amount1);
    event PayoutFellBack(uint256 indexed tokenId, address token, uint256 amount); // creator payout failed → treasury

    constructor(address posMgr, address treasury, uint256 pairCreatorBps, uint256 tokenCreatorBps, uint256 tokenBurnBps) {
        require(pairCreatorBps <= 10000 && tokenCreatorBps + tokenBurnBps <= 10000, "bps");
        POS_MGR = INonfungiblePositionManager(posMgr);
        TREASURY = treasury;
        FACTORY = msg.sender;
        PAIR_CREATOR_BPS = pairCreatorBps;
        TOKEN_CREATOR_BPS = tokenCreatorBps;
        TOKEN_BURN_BPS = tokenBurnBps;
    }

    // One-shot per tokenId, factory-only, called inside launch() right after mint.
    function register(uint256 tokenId, address creator) external {
        require(msg.sender == FACTORY, "factory");
        require(creator != address(0), "creator");
        require(creatorOf[tokenId] == address(0), "registered");
        creatorOf[tokenId] = creator;
    }

    // Collect to self first, then forward: paying the pool's collect output straight to
    // the creator could revert on the token-side leg (max-wallet), bricking collection.
    // The token exempts transfers FROM this locker, so forwarding always succeeds for
    // our tokens; a failing creator share falls back to treasury.
    function collect(uint256 tokenId) external nonReentrant returns (uint256 amount0, uint256 amount1) {
        (amount0, amount1) = POS_MGR.collect(INonfungiblePositionManager.CollectParams({
            tokenId: tokenId, recipient: address(this),
            amount0Max: type(uint128).max, amount1Max: type(uint128).max
        }));
        (,, address token0, address token1,,,,,,,,) = POS_MGR.positions(tokenId);
        address creator = creatorOf[tokenId];
        _split(tokenId, token0, amount0, creator, PAIR_CREATOR_BPS, 0);
        _split(tokenId, token1, amount1, creator, TOKEN_CREATOR_BPS, TOKEN_BURN_BPS);
        emit Collected(tokenId, creator, amount0, amount1);
    }

    // Shares round DOWN; all rounding dust lands on the treasury (never the creator or
    // the burn). If the creator transfer fails, their share joins the treasury batch.
    // If the burn transfer fails, that share also joins the treasury batch. If the
    // TREASURY transfer fails, revert loudly: amounts come from this call's collect()
    // return (never balanceOf), so nothing can be double-counted on retry, but funds
    // would strand in the locker — surfacing the failure is safer than swallowing it.
    function _split(uint256 tokenId, address token, uint256 amount, address creator, uint256 creatorBps, uint256 burnBps) internal {
        if (amount == 0) return;
        uint256 toTreasury = amount;
        if (creator != address(0)) {
            uint256 cShare = (amount * creatorBps) / 10000;
            if (cShare > 0) {
                if (_pay(token, creator, cShare)) toTreasury -= cShare;
                else emit PayoutFellBack(tokenId, token, cShare);
            }
            uint256 bShare = (amount * burnBps) / 10000;
            if (bShare > 0 && _pay(token, DEAD, bShare)) toTreasury -= bShare;
        }
        if (toTreasury > 0) require(_pay(token, TREASURY, toTreasury), "treasury pay");
    }

    // Non-reverting ERC20 transfer; tolerates no-bool-return tokens (USDT-style).
    function _pay(address token, address to, uint256 v) internal returns (bool) {
        (bool ok, bytes memory ret) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, v));
        return ok && (ret.length == 0 || abi.decode(ret, (bool)));
    }

    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
        return this.onERC721Received.selector;
    }
}

contract YoinkLaunchV2 {
    // ---- Robinhood Chain constants (docs/CHAIN.md, verified on-chain 2026-07-05) ----
    address public constant WETH = 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73;
    IUniswapV3Factory public constant V3_FACTORY = IUniswapV3Factory(0x1f7d7550B1b028f7571E69A784071F0205FD2EfA);
    INonfungiblePositionManager public constant POS_MGR = INonfungiblePositionManager(0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3);
    ISwapRouter public constant ROUTER = ISwapRouter(0xCaf681a66D020601342297493863E78C959E5cb2);
    address public constant TREASURY = 0xF7bB7EfD70dD131dd1E7ec7C32F725fDb6686403;

    uint256 public constant SUPPLY = 1_000_000_000e18; // 1B, 100% to the pool, fair launch
    uint256 public constant LAUNCH_FEE = 0.0005 ether;  // flat, to TREASURY (v1-verified value)
    uint24  public constant POOL_FEE = 10000;           // 1% pool
    // Token is always token1 (salt search below), so one tick path only:
    int24   public constant TICK_LOWER = -887200;       // full-range floor for spacing 200
    int24   public constant TICK_UPPER = 204200;        // start tick: position begins 100% token
    uint160 public constant INIT_SQRT_PRICE = 2151813121295408910812139624586144; // tick 204200

    YoinkLpLockerV2 public immutable LOCKER; // deployed by this constructor, collect-only
    address public immutable YOINK_ROUTER;   // our router (e.g. V5) — token exempt set  [FOUNDER-TO-CONFIRM at deploy]

    uint256 public launchCount;
    uint256 private _entered; // reentrancy guard (defense in depth; every external callee is canonical infra)

    event Launched(
        address indexed token, address indexed creator, address pool, uint256 lpTokenId,
        uint256 initialBuyEth, uint256 initialBuyTokens
    );
    event TokenMeta(
        address indexed token,
        string name, string symbol, string logo, string description, string[3] socials
    );

    // yoinkRouter: the YOINK router to exempt from the token's wallet caps (its sell()
    // custodies tokens mid-trade). address(0) is allowed (no extra exemption) but means
    // launched tokens CANNOT be sold through a token-custodying router during high-volume
    // anti-snipe windows — pass the live router.  [FOUNDER-TO-CONFIRM at deploy]
    // pairCreatorBps / tokenCreatorBps / tokenBurnBps: the immutable LP-fee split
    // (see YoinkLpLockerV2). FOUNDER-LOCKED 2026-07-11: pass exactly 3500 / 0 / 8000.
    constructor(address yoinkRouter, uint256 pairCreatorBps, uint256 tokenCreatorBps, uint256 tokenBurnBps) {
        LOCKER = new YoinkLpLockerV2(address(POS_MGR), TREASURY, pairCreatorBps, tokenCreatorBps, tokenBurnBps);
        YOINK_ROUTER = yoinkRouter;
    }

    // ---- getters read by YoinkTokenV2's constructor (interface IYoinkLaunchV2) ----
    // (LOCKER/YOINK_ROUTER/TREASURY getters are auto-generated above.)
    function POS_MGR_ADDR() external pure returns (address) { return address(POS_MGR); }
    function ROUTER_ADDR() external pure returns (address) { return address(ROUTER); }

    // socials = [x, telegram, website], any may be empty. minTokensOut guards the initial
    // buy only (0 is acceptable when initial buy is 0). Metadata rides the event; the
    // token contract itself stays minimal.
    struct LaunchParams {
        string name; string symbol; string logo; string description;
        string[3] socials; uint256 minTokensOut;
    }

    // msg.value = LAUNCH_FEE + optional creator initial buy.
    // v2 note for the launch handler: the initial buy recipient is the creator, who is
    // subject to the 2% max-wallet — the UI quote must clamp the buy so tokens out
    // <= token.maxWallet() (2e25) or the whole launch reverts. The creator is exempt
    // from the anti-snipe curve, so maxWallet is the only bound.
    function launch(LaunchParams calldata p) external payable returns (address token, address pool) {
        require(_entered == 0, "reentrancy");
        _entered = 1;
        require(msg.value >= LAUNCH_FEE, "fee");
        require(bytes(p.name).length != 0 && bytes(p.symbol).length != 0, "name");
        // an initial buy must carry a real quote; UI passes the QuoterV2 result minus slippage
        require(msg.value == LAUNCH_FEE || p.minTokensOut > 0, "minOut");

        // ---- deploy the token at a CREATE2 address above WETH so it is always token1 ----
        token = _deployToken(p.name, p.symbol);
        emit TokenMeta(token, p.name, p.symbol, p.logo, p.description, p.socials);

        // ---- create + initialize the pool at the launch price ----
        pool = V3_FACTORY.createPool(WETH, token, POOL_FEE);
        IUniswapV3Pool(pool).initialize(INIT_SQRT_PRICE);
        // wire the pool into the token's exemption set BEFORE any token moves
        YoinkTokenV2(token).setPool(pool);

        // ---- seed 100% of supply as locked liquidity, LP NFT straight to the locker ----
        IERC20(token).approve(address(POS_MGR), SUPPLY);
        (uint256 lpTokenId,,,) = POS_MGR.mint(INonfungiblePositionManager.MintParams({
            token0: WETH, token1: token, fee: POOL_FEE,
            tickLower: TICK_LOWER, tickUpper: TICK_UPPER,
            amount0Desired: 0, amount1Desired: SUPPLY,
            amount0Min: 0, amount1Min: SUPPLY - 1e18, // tolerate liquidity-math dust only
            recipient: address(LOCKER), deadline: block.timestamp
        }));
        // Rounding dust (observed ~1e-10% on real launches) stays in this contract,
        // which has no way to move it — effectively burned.

        // ---- record the creator for LP-fee payouts (one-shot, immutable per tokenId) ----
        LOCKER.register(lpTokenId, msg.sender);

        // ---- optional creator initial buy with everything above the fee ----
        uint256 buyEth = msg.value - LAUNCH_FEE;
        uint256 bought;
        if (buyEth > 0) {
            IWETH(WETH).deposit{value: buyEth}();
            IWETH(WETH).approve(address(ROUTER), buyEth);
            bought = ROUTER.exactInputSingle(ISwapRouter.ExactInputSingleParams({
                tokenIn: WETH, tokenOut: token, fee: POOL_FEE, recipient: msg.sender,
                amountIn: buyEth, amountOutMinimum: p.minTokensOut, sqrtPriceLimitX96: 0
            }));
        }

        // ---- launch fee to treasury last (no external unknowns before this point) ----
        (bool ok,) = TREASURY.call{value: LAUNCH_FEE}("");
        require(ok, "fee transfer");

        emit Launched(token, msg.sender, pool, lpTokenId, buyEth, bought);
        _entered = 0;
    }

    // CREATE2 salt search: find a salt whose predicted token address sorts above WETH
    // (so the token is always token1) and is unused. Expected ~1.05 iterations; the
    // launchCount/sender mix keeps identical name+symbol launches from colliding.
    function _deployToken(string calldata name_, string calldata symbol_) internal returns (address token) {
        bytes32 initCodeHash = keccak256(abi.encodePacked(
            type(YoinkTokenV2).creationCode, abi.encode(name_, symbol_, SUPPLY, msg.sender)
        ));
        uint256 n = launchCount++;
        bytes32 salt;
        address predicted;
        for (uint256 i; ; i++) {
            require(i < 256, "salt"); // expected ~1.05 tries; cap is belt-and-suspenders
            salt = keccak256(abi.encodePacked(n, msg.sender, i));
            predicted = address(uint160(uint256(keccak256(abi.encodePacked(
                hex"ff", address(this), salt, initCodeHash
            )))));
            if (predicted > WETH && predicted.code.length == 0) break;
        }
        token = address(new YoinkTokenV2{salt: salt}(name_, symbol_, SUPPLY, msg.sender));
        require(token == predicted, "create2");
    }
}
Chain explorer1228msChain node150ms