RawbinInstantDEX

0x9ee7e27ac6f3028a93960dc53f30c9c905010999

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
14,291 bytes
ABI entries
47
27 read · 9 write
License
none

Contract information

Address
0x9ee7e27ac6f3028a93960dc53f30c9c905010999
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x2c42c158be…1ff3a7d74d

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (27)

FEE_TIMELOCK()uint256
GRADUATION_WETH()uint256
MAX_CREATION_FEE()uint256
PLATFORM()string
PLATFORM_URL()string
TICK_SPACING()int24
TOTAL_SUPPLY()uint256
V3_FEE()uint24
allTokens(uint256)address, address, uint256, address, string, uint256
allTokensLength()uint256
creationFee()uint256
feeRecipient()address
feeRecipientEta()uint256
graduated(address)bool
isCurve(address)bool
locker()address
npm()address
owner()address
pendingFeeRecipient()address
pendingOwner()address
poolOf(address)address
predictToken(address, string, string, bytes32)address
sqrtPriceStartX96()uint160
swapRouter()address
tickLower()int24
tickUpper()int24
weth()address

Events (8)

CreationFeeUpdatedFeeRecipientCancelledFeeRecipientProposedFeeRecipientUpdatedGraduatedOwnershipTransferStartedOwnershipTransferredTokenCreated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "feeRecipient_",
        "type": "address"
      },
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "npm_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "swapRouter_",
        "type": "address"
      },
      {
        "internalType": "uint160",
        "name": "sqrtPriceStartX96_",
        "type": "uint160"
      },
      {
        "internalType": "int24",
        "name": "tickLower_",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "tickUpper_",
        "type": "int24"
      },
      {
        "internalType": "uint256",
        "name": "creationFee_",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newFee",
        "type": "uint256"
      }
    ],
    "name": "CreationFeeUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "cancelled",
        "type": "address"
      }
    ],
    "name": "FeeRecipientCancelled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "next",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "eta",
        "type": "uint256"
      }
    ],
    "name": "FeeRecipientProposed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "prev",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "next",
        "type": "address"
      }
    ],
    "name": "FeeRecipientUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      }
    ],
    "name": "TokenCreated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "FEE_TIMELOCK",
    "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_CREATION_FEE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PLATFORM",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PLATFORM_URL",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "V3_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "allTokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "createdAt",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "allTokensLength",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "applyFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cancelFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "checkGraduation",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      }
    ],
    "name": "createToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "tokenAddr",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creationFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeRecipientEta",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "graduated",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "a",
        "type": "address"
      }
    ],
    "name": "isCurve",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "locker",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "npm",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingFeeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "poolOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "predictToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "tokenAddr",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "r",
        "type": "address"
      }
    ],
    "name": "proposeFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newFee",
        "type": "uint256"
      }
    ],
    "name": "setCreationFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "sqrtPriceStartX96",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "",
        "type": "uint160"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "swapRouter",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tickLower",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tickUpper",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./RawbinToken.sol";
import "./RawbinLocker.sol";
import "./interfaces/IUniswapV3.sol";

/// @dev Minimal Uniswap V3 SwapRouter02 subset for the optional same-tx dev buy.
interface ISwapRouter02Direct {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }
    function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
}

/// @title RawbinInstantDEX
/// @author Rawbin — the #1 launchpad on Robinhood Chain
/// @custom:website https://rawbin.fun
/// @custom:mission We're building a transparent launchpad. Verified contracts, no
///  hidden owner over tokens, no token tax, liquidity you can never rug. Read the
///  code and verify everything yourself. Every launch is a drop. — rawbin
/// @notice "Instant DEX" launch mode. One call deploys a RawbinToken and opens a real
///         Uniswap V3 pool immediately, seeding it *single-sided* (token only, no ETH)
///         over a fixed price range. Buyers walk the price up the range exactly like a
///         bonding curve, but it's a genuine DEX pool from block 0 — indexed by
///         DEXScreener/GMGN instantly, holder shown as "Uniswap V3" (not an insider).
///         The position NFT is locked in the Locker forever; the 1% pool fee is
///         collectable and split 75% platform / 25% creator.
///
///         Design notes:
///         - The meme token is always Uniswap `token1` (its address sorts ABOVE WETH),
///           enforced on-chain + by the off-chain grinder. This gives one code path and
///           lets us bake the price/tick constants (see script/compute-direct-params).
///         - No custom curve contract: the single-sided V3 position *is* the curve.
///         - This factory implements IRawbinFactoryView so it can reuse RawbinLocker
///           unchanged (it authorizes itself via isCurve(address(this)) == true).
contract RawbinInstantDEX is Ownable2Step, IRawbinFactoryView {
    string public constant PLATFORM = "rawbin.fun";
    string public constant PLATFORM_URL = "https://rawbin.fun";

    uint256 public constant TOTAL_SUPPLY = 1_000_000_000 ether;

    /// @notice The Rawbin mark, enforced on-chain: every token address ends in 0x…999.
    uint160 internal constant VANITY_MASK = 0xFFF;
    uint160 internal constant VANITY_SUFFIX = 0x999;

    /// @notice Uniswap V3 pool fee tier (1%) and its tick spacing.
    uint24 public constant V3_FEE = 10000;
    int24 public constant TICK_SPACING = 200;

    /// @notice Amount floor for the single-sided seed mint (basis points of the
    ///         token supply). At the exact start price the mint consumes ~100% of
    ///         TOTAL_SUPPLY; anything materially below that means the pool wasn't at
    ///         our price, so we revert instead of seeding a half-empty pool. This is
    ///         the explicit `amount1Min` an auditor expects and matches the floor the
    ///         bonding curve already enforces at migration (parity with RB-01).
    uint256 internal constant MAX_SEED_SLIPPAGE_BPS = 200; // 2%

    INonfungiblePositionManager public immutable npm;
    address public immutable weth;
    /// @notice Uniswap V3 SwapRouter02 — used for the optional same-tx dev buy.
    address public immutable swapRouter;
    /// @notice Dedicated Locker instance for direct launches (deployed + wired here).
    address public immutable locker;

    // Single-sided launch geometry — precomputed off-chain, fixed at deploy.
    // Because the meme token is always token1 (addr > WETH), the position holds only
    // token1 when the current tick is at/above tickUpper. So we initialize the pool at
    // sqrtPriceStart == sqrtRatioAtTick(tickUpper) and mint the whole supply into
    // [tickLower, tickUpper]; buys push the tick down through the range, selling token
    // for WETH (price of the meme in WETH rises).
    uint160 public immutable sqrtPriceStartX96;
    int24 public immutable tickLower;
    int24 public immutable tickUpper;

    // Fee recipient — mutable behind a 24h timelock (propose -> apply), Ownable2Step.
    address public override feeRecipient;
    address public pendingFeeRecipient;
    uint256 public feeRecipientEta;
    uint256 public constant FEE_TIMELOCK = 24 hours;

    uint256 public creationFee;
    uint256 public constant MAX_CREATION_FEE = 0.005 ether;

    struct TokenInfo {
        address token;
        address pool;
        uint256 positionId;
        address creator;
        string metadataURI;
        uint256 createdAt;
    }
    TokenInfo[] public allTokens;

    /// @notice Graduation milestone: once the pool has collected this much WETH, the
    ///         token is flagged "graduated". Purely a status latch — it changes NOTHING
    ///         about the pool or the locked liquidity (no withdraw, no re-add); trading
    ///         continues exactly as before. It only powers the progress meter / status.
    uint256 public constant GRADUATION_WETH = 10 ether;
    mapping(address => address) public poolOf;   // token => its V3 pool
    mapping(address => bool) public graduated;   // token => milestone reached (latched)

    event Graduated(address indexed token, address indexed pool);

    event TokenCreated(
        address indexed token,
        address indexed pool,
        address indexed creator,
        uint256 positionId,
        string name,
        string symbol,
        string metadataURI
    );
    event CreationFeeUpdated(uint256 newFee);
    event FeeRecipientProposed(address indexed next, uint256 eta);
    event FeeRecipientUpdated(address indexed prev, address indexed next);
    event FeeRecipientCancelled(address indexed cancelled);

    constructor(
        address feeRecipient_,
        INonfungiblePositionManager npm_,
        address swapRouter_,
        uint160 sqrtPriceStartX96_,
        int24 tickLower_,
        int24 tickUpper_,
        uint256 creationFee_
    ) Ownable(feeRecipient_) {
        require(feeRecipient_ != address(0), "feeRecipient=0");
        require(swapRouter_ != address(0), "router=0");
        require(sqrtPriceStartX96_ > 0, "sqrtPrice=0");
        require(tickLower_ < tickUpper_, "ticks");
        require(tickLower_ % TICK_SPACING == 0 && tickUpper_ % TICK_SPACING == 0, "tick spacing");
        require(creationFee_ <= MAX_CREATION_FEE, "fee > cap");
        feeRecipient = feeRecipient_;
        npm = npm_;
        weth = npm_.WETH9();
        swapRouter = swapRouter_;
        sqrtPriceStartX96 = sqrtPriceStartX96_;
        tickLower = tickLower_;
        tickUpper = tickUpper_;
        creationFee = creationFee_;
        locker = address(new RawbinLocker(npm_, IRawbinFactoryView(address(this))));
    }

    // --------------------------------------------------- IRawbinFactoryView
    // The Locker authorizes lockPosition() via isCurve(msg.sender). For direct
    // launches the factory itself creates + locks the position, so it authorizes
    // itself. It never marks any other address as a curve.
    function isCurve(address a) external view override returns (bool) {
        return a == address(this);
    }
    // feeRecipient() is the public state var getter (satisfies the interface).

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

    function setCreationFee(uint256 newFee) external onlyOwner {
        require(newFee <= MAX_CREATION_FEE, "fee > cap");
        creationFee = newFee;
        emit CreationFeeUpdated(newFee);
    }

    function proposeFeeRecipient(address r) external onlyOwner {
        require(r != address(0), "feeRecipient=0");
        pendingFeeRecipient = r;
        feeRecipientEta = block.timestamp + FEE_TIMELOCK;
        emit FeeRecipientProposed(r, feeRecipientEta);
    }

    function applyFeeRecipient() external onlyOwner {
        // forge-lint: disable-next-line(block-timestamp)
        require(feeRecipientEta != 0 && block.timestamp >= feeRecipientEta, "timelock");
        emit FeeRecipientUpdated(feeRecipient, pendingFeeRecipient);
        feeRecipient = pendingFeeRecipient;
        pendingFeeRecipient = address(0);
        feeRecipientEta = 0;
    }

    function cancelFeeRecipient() external onlyOwner {
        emit FeeRecipientCancelled(pendingFeeRecipient);
        pendingFeeRecipient = address(0);
        feeRecipientEta = 0;
    }

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

    /// @param salt Vanity salt ground off-chain so the token address ends in 0x…999
    ///        AND sorts above WETH (so the meme is token1). Namespaced by msg.sender.
    /// @param minTokensOut Slippage bound for the optional same-tx dev buy — any ETH
    ///        above the creation fee buys tokens for the creator atomically at launch
    ///        (snipe-proof: the creator gets the first, cheapest tokens before bots).
    function createToken(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        bytes32 salt,
        uint256 minTokensOut
    ) external payable returns (address tokenAddr, address pool, uint256 positionId) {
        require(msg.value >= creationFee, "creation fee not paid");

        // Scope the token creation so `token` is freed before the rest (stack depth).
        {
            RawbinToken token =
                new RawbinToken{salt: keccak256(abi.encodePacked(msg.sender, salt))}(name, symbol, TOTAL_SUPPLY, address(this));
            tokenAddr = address(token);
            // Enforce the Rawbin mark AND the token1 ordering (meme addr > WETH addr).
            require(uint160(tokenAddr) & VANITY_MASK == VANITY_SUFFIX, "not a 0x...999 address");
            require(uint160(tokenAddr) > uint160(weth), "token must sort above WETH");
            token.initializeMetadata(metadataURI);
        }

        (pool, positionId) = _seedAndLock(tokenAddr, msg.sender);
        _forwardFeeAndDevBuy(tokenAddr, msg.sender, minTokensOut);
        _record(tokenAddr, pool, positionId, name, symbol, metadataURI);
    }

    /// Store + announce the launch (kept in its own frame to ease createToken's stack).
    function _record(
        address tokenAddr,
        address pool,
        uint256 positionId,
        string calldata name,
        string calldata symbol,
        string calldata metadataURI
    ) internal {
        poolOf[tokenAddr] = pool;
        allTokens.push(TokenInfo({
            token: tokenAddr,
            pool: pool,
            positionId: positionId,
            creator: msg.sender,
            metadataURI: metadataURI,
            createdAt: block.timestamp
        }));
        emit TokenCreated(tokenAddr, pool, msg.sender, positionId, name, symbol, metadataURI);
    }

    /// @notice Permissionless graduation latch: flags the token graduated once its pool
    ///         holds >= GRADUATION_WETH. Read-only w.r.t. liquidity — safe for anyone to
    ///         call; it never touches the pool or the locked position.
    function checkGraduation(address token) external returns (bool) {
        if (graduated[token]) return true;
        address p = poolOf[token];
        require(p != address(0), "unknown token");
        if (IERC20(weth).balanceOf(p) >= GRADUATION_WETH) {
            graduated[token] = true;
            emit Graduated(token, p);
            return true;
        }
        return false;
    }

    /// Open the pool, seed it single-sided (all supply as token1, no WETH), lock the NFT.
    function _seedAndLock(address tokenAddr, address creator) internal returns (address pool, uint256 positionId) {
        pool = npm.createAndInitializePoolIfNecessary(weth, tokenAddr, V3_FEE, sqrtPriceStartX96);

        // RB-02 / RB-06: the (WETH,token,1%) pool is deterministic and pre-createable.
        // Unlike the curve (whose fair price is derived from the raise), our start
        // price is a FIXED constant, so we can — and must — require EXACT equality.
        // A band would let an attacker pre-init at +band above tickUpper, seeding a
        // gapped pool that aggregators price above the liquidity (bad launch chart);
        // and a price below tickUpper makes the single-sided mint compute 0 liquidity
        // and revert anyway. Any off-price pool → revert; the creator re-grinds a
        // fresh salt (=> a different, unpoisoned pool).
        (uint160 poolSqrt, , , , , , ) = IUniswapV3Pool(pool).slot0();
        require(poolSqrt == sqrtPriceStartX96, "pool price manipulated");

        IERC20(tokenAddr).approve(address(npm), TOTAL_SUPPLY);
        (positionId, , , ) = npm.mint(
            INonfungiblePositionManager.MintParams({
                token0: weth,
                token1: tokenAddr,
                fee: V3_FEE,
                tickLower: tickLower,
                tickUpper: tickUpper,
                amount0Desired: 0,
                amount1Desired: TOTAL_SUPPLY,
                // amount0Min stays 0 on purpose: the seed is single-sided (token
                // only, no WETH), so a nonzero WETH floor would always revert.
                amount0Min: 0,
                // Real floor on the token side: at least (100% - 2%) of supply must
                // actually enter the pool, else the mint reverts (see RB-04 note).
                amount1Min: TOTAL_SUPPLY - (TOTAL_SUPPLY * MAX_SEED_SLIPPAGE_BPS) / 10_000,
                recipient: locker,
                deadline: block.timestamp
            })
        );
        // Lock it forever (Locker authorizes us via isCurve(address(this)) == true).
        RawbinLocker(payable(locker)).lockPosition(positionId, creator);

        // V3 single-sided mint leaves a few wei of dust (tick rounding). Sweep it to
        // the creator so the factory never accumulates token balances.
        uint256 dust = IERC20(tokenAddr).balanceOf(address(this));
        if (dust > 0) require(IERC20(tokenAddr).transfer(creator, dust), "dust transfer failed");
    }

    /// Forward the creation fee; spend any excess ETH on an atomic dev buy for the
    /// creator (snipe-proof first buy). No excess → nothing to do.
    function _forwardFeeAndDevBuy(address tokenAddr, address buyer, uint256 minTokensOut) internal {
        uint256 fee = creationFee;
        if (fee > 0) {
            (bool feeSent, ) = feeRecipient.call{value: fee}("");
            require(feeSent, "fee transfer failed");
        }
        uint256 buyEth = msg.value - fee;
        if (buyEth == 0) return;

        // RB-09: a V3 exact-input swap can consume LESS than amountIn if the dev buy
        // is large enough to exhaust the seeded range. The unspent WETH would then sit
        // in the factory with no withdraw path. Measure the balance around the swap and
        // refund any unspent WETH to the creator; reset the router allowance too.
        uint256 wethBefore = IERC20(weth).balanceOf(address(this));
        IWETH9(weth).deposit{value: buyEth}();
        IERC20(weth).approve(swapRouter, buyEth);
        ISwapRouter02Direct(swapRouter).exactInputSingle(
            ISwapRouter02Direct.ExactInputSingleParams({
                tokenIn: weth,
                tokenOut: tokenAddr,
                fee: V3_FEE,
                recipient: buyer,
                amountIn: buyEth,
                amountOutMinimum: minTokensOut,
                sqrtPriceLimitX96: 0
            })
        );

        // Reset the allowance and refund any WETH the swap didn't consume.
        IERC20(weth).approve(swapRouter, 0);
        uint256 wethAfter = IERC20(weth).balanceOf(address(this));
        if (wethAfter > wethBefore) {
            require(IERC20(weth).transfer(buyer, wethAfter - wethBefore), "dev-buy refund failed");
        }
    }

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

    /// @notice Predicts the CREATE2 token address for a creator + salt (grinder source
    ///         of truth). Supply is minted to this factory, so that's the ctor arg.
    function predictToken(
        address creator,
        string calldata name,
        string calldata symbol,
        bytes32 salt
    ) external view returns (address tokenAddr) {
        bytes32 nsSalt = keccak256(abi.encodePacked(creator, salt));
        bytes32 initHash = keccak256(
            abi.encodePacked(
                type(RawbinToken).creationCode,
                abi.encode(name, symbol, TOTAL_SUPPLY, address(this))
            )
        );
        tokenAddr = address(uint160(uint256(
            keccak256(abi.encodePacked(bytes1(0xff), address(this), nsSalt, initHash))
        )));
    }
}
Chain explorer2699msChain node88ms