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
- Creator
- 0x28e2936aD7…32438A498B
- 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