KaitenFactory

0x50a4c73931988de48258f8372511edeafc0ec3a4

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
12,430 bytes
ABI entries
38
19 read · 6 write
License
none

Contract information

Address
0x50a4c73931988de48258f8372511edeafc0ec3a4
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x797b460ef8…0d8f3a17c7

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (19)

FEE()uint24
GRADUATION_MIN_HOLD()uint256
GRADUATION_WETH()uint256
MAX_LAUNCH_ATTEMPTS()uint256
POSITION_MANAGER()address
PRIMARY_AMOUNT()uint256
SECONDARY_AMOUNT()uint256
SWAP_ROUTER()address
TOTAL_SUPPLY()uint256
V3_FACTORY()address
WETH()address
allTokens(uint256)address
allTokensLength()uint256
graduationArmedAt(address)uint64
isGraduated(address)bool
launches(address)address, address, uint256, uint256, bool, bool
owner()address
treasury()address
wethInPrimary(address)uint256

Events (5)

FeesClaimedGraduatedLaunchedOwnershipTransferredTreasuryUpdated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadStartPrice",
    "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": "PoolPreInitialized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownToken",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "wethToCreator",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenToCreator",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "wethToTreasury",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenToTreasury",
        "type": "uint256"
      }
    ],
    "name": "FeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "wethInPosition",
        "type": "uint256"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "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": "primaryPositionId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "continuationPositionId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "imageURI",
        "type": "string"
      }
    ],
    "name": "Launched",
    "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": "treasury",
        "type": "address"
      }
    ],
    "name": "TreasuryUpdated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRADUATION_MIN_HOLD",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRADUATION_WETH",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_LAUNCH_ATTEMPTS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POSITION_MANAGER",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PRIMARY_AMOUNT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SECONDARY_AMOUNT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SWAP_ROUTER",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "V3_FACTORY",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "WETH",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "allTokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "allTokensLength",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "checkGraduation",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "claimFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "graduationArmedAt",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "armedAt",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "isGraduated",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "imageURI",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "initialBuyMinOut",
        "type": "uint256"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "launches",
    "outputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "primaryPositionId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "continuationPositionId",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "tokenIsToken0",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "graduated",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newTreasury",
        "type": "address"
      }
    ],
    "name": "setTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "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": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "wethInPrimary",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import { KaitenToken } from "./KaitenToken.sol";
import { LiquidityAmounts } from "./libraries/LiquidityAmounts.sol";
import {
    INonfungiblePositionManager,
    ISwapRouter,
    IUniswapV3Factory,
    IUniswapV3Pool,
    IWETH9
} from "./interfaces/ISushiV3.sol";

/// @title KaitenFactory
/// @notice One-click token launchpad on Sushi V3 (Robinhood Chain). Each launch
///         deploys a fixed-supply KaitenToken, creates its WETH pool at ~2 ETH
///         FDV, and seeds two single-sided positions (80% engineered to raise
///         ~4 ETH, 20% continuation band). The LP NFTs are held by this factory
///         forever — there is NO code path to withdraw principal, only collect()
///         fees, split 50% creator / 50% treasury.
contract KaitenFactory is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    // ─────────────────────────── Sushi V3 on Robinhood Chain (verified on-chain) ──
    address public constant WETH = 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73;
    address public constant V3_FACTORY = 0xE51960f1B45f1C9FB6D166E6a884F866fC70433B;
    address public constant POSITION_MANAGER = 0x51d0e5188afe12d502e29D982d20C190e7816107;
    address public constant SWAP_ROUTER = 0xB2d8eD81e79eb64A0751352459eC215FbAFad669;
    uint24 public constant FEE = 10000; // 1% tier, tickSpacing 200

    // ─────────────────────────── Tokenomics ──
    uint256 public constant TOTAL_SUPPLY = 100_000_000e18;
    uint256 public constant PRIMARY_AMOUNT = 80_000_000e18; // engineered ~4 ETH raise
    uint256 public constant SECONDARY_AMOUNT = 20_000_000e18; // continuation band
    uint256 public constant GRADUATION_WETH = 4 ether;

    /// @notice the primary position's WETH side must hold >= GRADUATION_WETH for at
    ///         least this long before the `Graduated` latch fires. This is what makes
    ///         graduation resistant to a transient spot-price spike: a single-tx flash
    ///         buy-then-sell crosses the line for zero seconds and cannot span it.
    uint256 public constant GRADUATION_MIN_HOLD = 10 minutes;

    /// @notice max KaitenToken deploys per launch while routing around a griefer who
    ///         has pre-created hostile pools at our upcoming token addresses.
    uint256 public constant MAX_LAUNCH_ATTEMPTS = 8;

    // Range ticks (aligned to tickSpacing 200). See ANALYSIS.md for the derivation.
    // Orientation A — TOKEN is token0 (TOKEN address < WETH): price = WETH/TOKEN.
    int24 internal constant A_TICK_LOWER = -177200; // start FDV ~2 ETH
    int24 internal constant A_TICK_UPPER = -159000; // top of primary, FDV ~12.5 ETH
    int24 internal constant A_TICK_UPPER2 = -100000; // top of continuation band
    // Init AT the primary lower tick: single-sided all-token0 with no dust, and no
    // zero-liquidity gap between the start price and the position (so buys trade).
    uint160 internal constant A_START_SQRT = 11251822014334092487017933; // sqrtRatio(-177200)
    uint160 internal constant A_SQRT_LOWER = 11251822014334092487017933; // sqrtRatio(-177200)
    uint160 internal constant A_SQRT_UPPER = 27951883217009894848890166; // sqrtRatio(-159000)
    // Orientation B — TOKEN is token1 (TOKEN address > WETH): price = TOKEN/WETH (mirror).
    int24 internal constant B_TICK_LOWER = 159000;
    int24 internal constant B_TICK_UPPER = 177200;
    int24 internal constant B_TICK_LOWER2 = 100000;
    // Init AT the primary upper tick: single-sided all-token1 with no dust / no gap.
    uint160 internal constant B_START_SQRT = 557874247156599145362199702308705; // sqrtRatio(+177200)
    uint160 internal constant B_SQRT_LOWER = 224568115380748325923021391801200; // sqrtRatio(+159000)
    uint160 internal constant B_SQRT_UPPER = 557874247156599145362199702308705; // sqrtRatio(+177200)

    // ─────────────────────────── State ──
    struct Launch {
        address creator;
        address pool;
        uint256 primaryPositionId; // engineered for the 4 ETH graduation target
        uint256 continuationPositionId;
        bool tokenIsToken0;
        bool graduated;
    }

    /// @notice protocol treasury — receives 50% of fees. Owner-updatable (the ONLY owner power).
    address public treasury;
    mapping(address token => Launch) public launches;
    /// @notice timestamp at which a token's primary WETH side was first seen at/above
    ///         GRADUATION_WETH and has been continuously observed there since (0 = not
    ///         armed). Reset to 0 whenever a `checkGraduation` call observes it below.
    mapping(address token => uint64 armedAt) public graduationArmedAt;
    address[] public allTokens;

    // ─────────────────────────── Events ──
    event Launched(
        address indexed token,
        address indexed creator,
        address pool,
        uint256 primaryPositionId,
        uint256 continuationPositionId,
        string name,
        string symbol,
        string imageURI
    );
    event FeesClaimed(
        address indexed token,
        address indexed creator,
        uint256 wethToCreator,
        uint256 tokenToCreator,
        uint256 wethToTreasury,
        uint256 tokenToTreasury
    );
    event TreasuryUpdated(address indexed treasury);
    event Graduated(address indexed token, address indexed pool, uint256 wethInPosition);

    error UnknownToken();
    error PoolPreInitialized();
    error BadStartPrice();

    constructor(address treasury_) Ownable(msg.sender) {
        treasury = treasury_ == address(0) ? msg.sender : treasury_;
    }

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

    // ─────────────────────────── Launch ──

    /// @notice Deploy a token, create its Sushi V3 pool, seed both single-sided
    ///         positions, and (optionally) buy the first serving as the creator.
    /// @param initialBuyMinOut min tokens out for the optional initial buy (msg.value).
    function launch(
        string calldata name,
        string calldata symbol,
        string calldata imageURI,
        uint256 initialBuyMinOut
    ) external payable nonReentrant returns (address token) {
        // The token address is a deterministic CREATE, so an attacker watching the
        // mempool can pre-create+initialize its pool at a hostile price before this
        // tx lands (createAndInitializePoolIfNecessary would then silently return
        // that pool, and the single-sided mint traps or the initial buy is stolen).
        // Defense: never touch a pool we didn't just create at the engineered price.
        // If our next token address already has a pool at the wrong price, discard it
        // and deploy the next one — routing around targeted griefing.
        KaitenToken kt;
        bool isT0;
        address token0;
        address token1;
        uint160 startSqrt;
        address pool;
        uint256 attempts;
        while (true) {
            // Deploy via CREATE2 with a salt that varies by block, launcher, launch
            // index and attempt. This is what makes the griefing above non-permanent:
            // a reverting launch rolls back a plain CREATE nonce, so a griefer who
            // pre-creates pools at the next few nonce-derived addresses would brick
            // the factory forever. With block.timestamp in the salt, a retry in a
            // later block lands on fresh addresses the attacker couldn't pre-grief.
            bytes32 salt = keccak256(abi.encode(msg.sender, block.timestamp, allTokens.length, attempts));
            kt = new KaitenToken{ salt: salt }(name, symbol, imageURI, msg.sender);
            token = address(kt);
            isT0 = token < WETH;
            (token0, token1) = isT0 ? (token, WETH) : (WETH, token);
            startSqrt = isT0 ? A_START_SQRT : B_START_SQRT;

            address existing = IUniswapV3Factory(V3_FACTORY).getPool(token0, token1, FEE);
            if (existing == address(0)) {
                // No pool yet — create it ourselves at the engineered start price.
                pool = INonfungiblePositionManager(POSITION_MANAGER)
                    .createAndInitializePoolIfNecessary(token0, token1, FEE, startSqrt);
                break;
            }
            // A pool already exists. Trust it only if it sits at our exact engineered
            // price (a harmless pre-create); otherwise skip this token address.
            (uint160 sp,,,,,,) = IUniswapV3Pool(existing).slot0();
            if (sp == startSqrt) {
                pool = existing;
                break;
            }
            unchecked {
                ++attempts;
            }
            if (attempts >= MAX_LAUNCH_ATTEMPTS) revert PoolPreInitialized();
        }

        // Hard invariant: we only ever seed liquidity against the engineered price.
        (uint160 nowSqrt,,,,,,) = IUniswapV3Pool(pool).slot0();
        if (nowSqrt != startSqrt) revert BadStartPrice();

        kt.initializeAntiSnipe(pool); // starts the 5-minute cap window now
        IERC20(token).forceApprove(POSITION_MANAGER, TOTAL_SUPPLY);

        // Primary (engineered) + continuation single-sided positions.
        (int24 pL, int24 pU, int24 cL, int24 cU) = isT0
            ? (A_TICK_LOWER, A_TICK_UPPER, A_TICK_UPPER, A_TICK_UPPER2)
            : (B_TICK_LOWER, B_TICK_UPPER, B_TICK_LOWER2, B_TICK_LOWER);
        uint256 primaryId = _mintPosition(token0, token1, isT0, pL, pU, PRIMARY_AMOUNT);
        uint256 continuationId = _mintPosition(token0, token1, isT0, cL, cU, SECONDARY_AMOUNT);

        launches[token] = Launch({
            creator: msg.sender,
            pool: pool,
            primaryPositionId: primaryId,
            continuationPositionId: continuationId,
            tokenIsToken0: isT0,
            graduated: false
        });
        allTokens.push(token);
        emit Launched(token, msg.sender, pool, primaryId, continuationId, name, symbol, imageURI);

        // Optional creator initial buy — exempt from the anti-snipe cap (recipient == creator).
        if (msg.value > 0) {
            IWETH9(WETH).deposit{ value: msg.value }();
            IERC20(WETH).forceApprove(SWAP_ROUTER, msg.value);
            ISwapRouter(SWAP_ROUTER)
                .exactInputSingle(
                    ISwapRouter.ExactInputSingleParams({
                    tokenIn: WETH,
                    tokenOut: token,
                    fee: FEE,
                    recipient: msg.sender,
                    amountIn: msg.value,
                    amountOutMinimum: initialBuyMinOut,
                    sqrtPriceLimitX96: 0
                })
                );
        }
    }

    function _mintPosition(
        address token0,
        address token1,
        bool isT0,
        int24 tickLower,
        int24 tickUpper,
        uint256 amount
    ) internal returns (uint256 tokenId) {
        (uint256 amount0, uint256 amount1) = isT0 ? (amount, uint256(0)) : (uint256(0), amount);
        (tokenId,,,) = INonfungiblePositionManager(POSITION_MANAGER)
            .mint(
                INonfungiblePositionManager.MintParams({
                token0: token0,
                token1: token1,
                fee: FEE,
                tickLower: tickLower,
                tickUpper: tickUpper,
                amount0Desired: amount0,
                amount1Desired: amount1,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(this), // LP NFT locked in the factory forever
                deadline: block.timestamp
            })
            );
    }

    // ─────────────────────────── Fees (locked LP: collect only) ──

    /// @notice Collect trading fees from both positions and split 50/50 to the
    ///         token's creator and the treasury. Principal is never touched.
    ///         Permissionless: destinations are fixed (recorded creator + treasury),
    ///         so a non-creator caller can never redirect the creator's share.
    function claimFees(address token) external nonReentrant {
        Launch storage l = launches[token];
        if (l.pool == address(0)) revert UnknownToken();

        uint128 max = type(uint128).max;
        (uint256 p0, uint256 p1) = INonfungiblePositionManager(POSITION_MANAGER)
            .collect(INonfungiblePositionManager.CollectParams(l.primaryPositionId, address(this), max, max));
        (uint256 c0, uint256 c1) = INonfungiblePositionManager(POSITION_MANAGER)
            .collect(
                INonfungiblePositionManager.CollectParams(l.continuationPositionId, address(this), max, max)
            );
        uint256 fee0 = p0 + c0;
        uint256 fee1 = p1 + c1;
        (uint256 wethFee, uint256 tokenFee) = l.tokenIsToken0 ? (fee1, fee0) : (fee0, fee1);

        uint256 wethCreator = wethFee / 2;
        uint256 tokenCreator = tokenFee / 2;
        address creator = l.creator;
        if (wethFee > 0) {
            IERC20(WETH).safeTransfer(creator, wethCreator);
            IERC20(WETH).safeTransfer(treasury, wethFee - wethCreator);
        }
        if (tokenFee > 0) {
            IERC20(token).safeTransfer(creator, tokenCreator);
            IERC20(token).safeTransfer(treasury, tokenFee - tokenCreator);
        }
        emit FeesClaimed(
            token, creator, wethCreator, tokenCreator, wethFee - wethCreator, tokenFee - tokenCreator
        );
    }

    // ─────────────────────────── Treasury (only owner power) ──

    function setTreasury(address newTreasury) external onlyOwner {
        require(newTreasury != address(0), "zero treasury");
        treasury = newTreasury;
        emit TreasuryUpdated(newTreasury);
    }

    // ─────────────────────────── Graduation (indexing only) ──

    /// @notice WETH currently held by the primary (engineered) position.
    function wethInPrimary(address token) public view returns (uint256) {
        Launch storage l = launches[token];
        if (l.pool == address(0)) revert UnknownToken();
        (,,,,,,, uint128 liquidity,,,,) =
            INonfungiblePositionManager(POSITION_MANAGER).positions(l.primaryPositionId);
        (uint160 sqrtCurrent,,,,,,) = IUniswapV3Pool(l.pool).slot0();
        (uint160 sqrtLower, uint160 sqrtUpper) =
            l.tokenIsToken0 ? (A_SQRT_LOWER, A_SQRT_UPPER) : (B_SQRT_LOWER, B_SQRT_UPPER);
        (uint256 amount0, uint256 amount1) =
            LiquidityAmounts.getAmountsForLiquidity(sqrtCurrent, sqrtLower, sqrtUpper, liquidity);
        return l.tokenIsToken0 ? amount1 : amount0;
    }

    function isGraduated(address token) public view returns (bool) {
        return wethInPrimary(token) >= GRADUATION_WETH;
    }

    /// @notice Latch `Graduated` once — but only after the primary position's WETH
    ///         side has stayed at/above GRADUATION_WETH for at least
    ///         GRADUATION_MIN_HOLD. Requires two calls: the first *arms* the sustain
    ///         timer, a later one (past the hold, still at/above the line) *latches*.
    ///
    ///         Rationale (M-1): `wethInPrimary` reads the spot price, which a single
    ///         transaction can spike (flash buy → check → sell) to momentarily cross
    ///         4 ETH. Requiring the level to persist across wall-clock time means a
    ///         zero-duration spike can never latch — only genuine, sustained
    ///         accumulation does. A drop below the line disarms the timer.
    ///
    ///         Residual (accepted, signal-only): an attacker willing to hold >= 4 ETH
    ///         of real buy pressure parked in the pool across two spikes MIN_HOLD apart
    ///         could still latch it — but that is expensive, self-funded, and
    ///         indistinguishable from real demand; no funds are affected either way.
    function checkGraduation(address token) external {
        Launch storage l = launches[token];
        if (l.pool == address(0)) revert UnknownToken();
        if (l.graduated) return;

        uint256 weth = wethInPrimary(token);
        if (weth < GRADUATION_WETH) {
            // fell back below the line — disarm any pending sustain timer
            if (graduationArmedAt[token] != 0) graduationArmedAt[token] = 0;
            return;
        }

        uint64 armedAt = graduationArmedAt[token];
        if (armedAt == 0) {
            // first sighting at/above the line — arm the timer, do NOT latch yet
            graduationArmedAt[token] = uint64(block.timestamp);
            return;
        }
        if (block.timestamp >= uint256(armedAt) + GRADUATION_MIN_HOLD) {
            l.graduated = true;
            emit Graduated(token, l.pool, weth);
        }
    }
}
Chain explorer5771msChain node72ms