SocialhoodFactory

0x2416f1abaf4f784c5024057fab94a6ae011aff6a

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
20,823 bytes
ABI entries
62
34 read · 6 write
License
none

Contract information

Address
0x2416f1abaf4f784c5024057fab94a6ae011aff6a
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x1027577f3c…2e64562a9a

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (34)

AIRDROP_AMOUNT()uint256
AIRDROP_BPS()uint16
BPS_DENOM()uint16
ESCROW()address
INITIAL_BUY_BPS_MAX()uint16
LP_AMOUNT()uint256
LP_LOCKER()address
MAX_INITIAL_BUY()uint256
MAX_USABLE_TICK()int24
MIN_USABLE_TICK()int24
NFPM()address
OBS_CARDINALITY()uint16
POOL_FEE()uint24
RESTRICTION_AMOUNT()uint256
RESTRICTION_BLOCKS()uint32
RESTRICTION_BPS()uint16
SWAP_ROUTER()address
TICK_SPACING()int24
TOTAL_SUPPLY()uint256
WETH()address
allTokens(uint256)address
creatorNonce(address)uint256
creatorTokens(address, uint256)address
getCreatorTokens(address)address[]
getTokens(uint256, uint256)address[]
launchFee()uint256
owner()address
pendingOwner()address
predictTokenAddress(address, string, string, string)address
quotes(address)bool, int24, uint256
tokenSalt(address, uint256, string, string)bytes32
tokens(address)address, address, address, address, uint256, int24, uint64
tokensCount()uint256
treasury()address

Events (7)

InitialBuyLaunchFeeUpdatedOwnershipTransferStartedOwnershipTransferredQuoteConfigSetTokenLaunchedTreasuryUpdated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_nfpm",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_swapRouter",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_weth",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_lpLocker",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_escrow",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_treasury",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_totalSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint16",
        "name": "_airdropBps",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "_obsCardinality",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "_restrictionBps",
        "type": "uint16"
      },
      {
        "internalType": "uint32",
        "name": "_restrictionBlocks",
        "type": "uint32"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "target",
        "type": "address"
      }
    ],
    "name": "AddressEmptyCode",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "AddressInsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DeployFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedInnerCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InitialBuyExceedsCap",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientLaunchFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidBuyPath",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidLaunchConfig",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolAlreadyInitialized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "QuoteNotEnabled",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Unauthorized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "name": "InitialBuy",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "launchFee",
        "type": "uint256"
      }
    ],
    "name": "LaunchFeeUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "launchTickToken0",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "gradTargetMcap",
        "type": "uint256"
      }
    ],
    "name": "QuoteConfigSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "airdropRoot",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "startingTick",
        "type": "int24"
      }
    ],
    "name": "TokenLaunched",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldTreasury",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newTreasury",
        "type": "address"
      }
    ],
    "name": "TreasuryUpdated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "AIRDROP_AMOUNT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "AIRDROP_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "BPS_DENOM",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ESCROW",
    "outputs": [
      {
        "internalType": "contract AirdropEscrow",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "INITIAL_BUY_BPS_MAX",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LP_AMOUNT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LP_LOCKER",
    "outputs": [
      {
        "internalType": "contract HoodLPLocker",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_INITIAL_BUY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_USABLE_TICK",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_USABLE_TICK",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "NFPM",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "OBS_CARDINALITY",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POOL_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "RESTRICTION_AMOUNT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "RESTRICTION_BLOCKS",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "RESTRICTION_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SWAP_ROUTER",
    "outputs": [
      {
        "internalType": "contract IV3SwapRouter",
        "name": "",
        "type": "address"
      }
    ],
    "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": "WETH",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "allTokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "creatorNonce",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "creatorTokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "getCreatorTokens",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "",
        "type": "address[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "offset",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "limit",
        "type": "uint256"
      }
    ],
    "name": "getTokens",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "list",
        "type": "address[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name_",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol_",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI_",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "airdropRoot",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "quoteToken",
        "type": "address"
      },
      {
        "internalType": "bytes",
        "name": "buyPathToQuote",
        "type": "bytes"
      }
    ],
    "name": "launchToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "name_",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol_",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI_",
        "type": "string"
      }
    ],
    "name": "predictTokenAddress",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "quotes",
    "outputs": [
      {
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      },
      {
        "internalType": "int24",
        "name": "launchTickToken0",
        "type": "int24"
      },
      {
        "internalType": "uint256",
        "name": "gradTargetMcap",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newFee",
        "type": "uint256"
      }
    ],
    "name": "setLaunchFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      },
      {
        "internalType": "int24",
        "name": "launchTickToken0",
        "type": "int24"
      },
      {
        "internalType": "uint256",
        "name": "gradTargetMcap",
        "type": "uint256"
      }
    ],
    "name": "setQuoteConfig",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newTreasury",
        "type": "address"
      }
    ],
    "name": "setTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "nonce",
        "type": "uint256"
      },
      {
        "internalType": "string",
        "name": "name_",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol_",
        "type": "string"
      }
    ],
    "name": "tokenSalt",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "tokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      },
      {
        "internalType": "int24",
        "name": "startingTick",
        "type": "int24"
      },
      {
        "internalType": "uint64",
        "name": "deployedAt",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokensCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";

import {HoodToken} from "./HoodToken.sol";
import {HoodLPLocker} from "./HoodLPLocker.sol";
import {AirdropEscrow} from "./AirdropEscrow.sol";
import {INonfungiblePositionManager, IUniswapV3Pool, IV3SwapRouter} from "./interfaces/IUniswapV3.sol";
import {IWETH} from "./interfaces/IWETH.sol";

/// @title SocialhoodFactory (Robinhood Chain)
/// @notice Fair-launch launcher satu-tx utk Socialhood Terminal (§14.1-14.2),
///         port pattern sendr — Uniswap V3 SAJA (satu DEX = likuiditas &
///         perhatian terkonsentrasi; keputusan owner 2026-07-24).
///
///         PAIR PILIHAN (2026-07-24): tiap launch memilih QUOTE dari daftar
///         yang di-enable owner — WETH / USDG / stock token (AAPL, NVDA, ...).
///         Tick harga launch + target mcap graduasi dikonfigurasi PER QUOTE
///         oleh owner dan HANYA berlaku utk launch berikutnya; launch yang
///         sudah jadi terkunci selamanya di escrow.
///
///         Alur (satu tx): CREATE2-deploy HoodToken → escrow AIRDROP_BPS
///         supply + merkle root + quote + target → create+initialize pool V3
///         1% token/quote di tick per-quote → naikkan observation cardinality
///         (bekal TWAP graduasi) → mint LP single-sided sisa supply ke
///         HoodLPLocker (locked) → optional initial buy creator via msg.value
///         (ETH di-route lewat `initialBuyPath` apa pun quote-nya), HARD CAP
///         5% supply — jaminan fair-launch, tidak bisa dikonfigurasi.
contract SocialhoodFactory is ReentrancyGuard {
    using SafeERC20 for IERC20;

    // ─── Konstanta pool (fee tier 1%) ──────────────────────────────
    uint24 public constant POOL_FEE = 10_000;
    int24 public constant TICK_SPACING = 200;
    int24 public constant MAX_USABLE_TICK = 887_200;
    int24 public constant MIN_USABLE_TICK = -887_200;

    // ─── Ekonomi launch (immutable, §14.6) ─────────────────────────
    uint16 public constant BPS_DENOM = 10_000;
    /// @dev Cap HARD initial buy deployer — 500 bps = 5% supply (pattern
    ///      sendr, permintaan owner). Tidak bisa dikonfigurasi per launch.
    uint16 public constant INITIAL_BUY_BPS_MAX = 500;

    uint256 public immutable TOTAL_SUPPLY;
    /// @dev Porsi supply ke AirdropEscrow utk key holder creator (mis. 250 = 2.5%).
    uint16 public immutable AIRDROP_BPS;
    uint256 public immutable AIRDROP_AMOUNT;
    /// @dev Sisa supply → posisi LP single-sided (mis. 97.5%).
    uint256 public immutable LP_AMOUNT;
    uint256 public immutable MAX_INITIAL_BUY;
    /// @dev Ring buffer observasi pool — bekal window TWAP graduasi.
    uint16 public immutable OBS_CARDINALITY;

    // ─── Anti-sniper (adopsi PONS 2026-07-24, tanpa admin di token) ─
    /// @dev Selama RESTRICTION_BLOCKS blok pertama tiap token: maxTx DAN
    ///      maxWallet = RESTRICTION_BPS dari supply (default 500 = 5%, SAMA
    ///      dgn cap initial buy creator — tidak ada yang bisa pegang lebih,
    ///      termasuk creator). 0 blok = pembatasan mati.
    uint16 public immutable RESTRICTION_BPS;
    uint32 public immutable RESTRICTION_BLOCKS;
    uint256 public immutable RESTRICTION_AMOUNT;

    // ─── Immutables eksternal ──────────────────────────────────────
    INonfungiblePositionManager public immutable NFPM;
    IV3SwapRouter public immutable SWAP_ROUTER;
    address public immutable WETH;
    HoodLPLocker public immutable LP_LOCKER;
    AirdropEscrow public immutable ESCROW;

    // ─── State ─────────────────────────────────────────────────────
    address public owner;
    address public pendingOwner; // 2-step ownership (pattern PONS/OZ)
    address public treasury;
    /// @notice Fee flat per launch (wei) → treasury. 0 = gratis. Anti-spam +
    ///         revenue; initial buy pakai sisa msg.value setelah fee.
    uint256 public launchFee;

    /// @dev Konfigurasi per quote token. `launchTickToken0` = tick harga
    ///      launch saat TOKEN BARU adalah token0 (selalu negatif — harga
    ///      token baru sangat kecil vs quote); sign dibalik otomatis bila
    ///      quote sort sebagai token0. `gradTargetMcap` dalam raw unit quote.
    struct QuoteConfig {
        bool enabled;
        int24 launchTickToken0;
        uint256 gradTargetMcap;
    }

    mapping(address => QuoteConfig) public quotes;

    struct TokenInfo {
        address creator;
        address feeRecipient;
        address pool;
        address quote;
        uint256 positionId;
        int24 startingTick;
        uint64 deployedAt;
    }

    mapping(address => TokenInfo) public tokens;
    address[] public allTokens;
    mapping(address => address[]) public creatorTokens;
    mapping(address => uint256) public creatorNonce;

    // ─── Events (shape dikunci — indexer §14 step 4 bergantung ini) ─
    /// @dev Sengaja ramping (via-ir stack limit): feeRecipient/positionId/
    ///      deployedAt via getter tokens(); metadataURI via token.tokenURI().
    event TokenLaunched(
        address indexed token,
        address indexed creator,
        address indexed quote,
        string name,
        string symbol,
        address pool,
        bytes32 airdropRoot,
        int24 startingTick
    );
    event InitialBuy(address indexed token, address indexed buyer, uint256 ethIn, uint256 tokensOut);
    event QuoteConfigSet(address indexed quote, bool enabled, int24 launchTickToken0, uint256 gradTargetMcap);
    event LaunchFeeUpdated(uint256 launchFee);
    event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury);
    event OwnershipTransferStarted(address indexed oldOwner, address indexed newOwner);
    event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);

    // ─── Errors ────────────────────────────────────────────────────
    error Unauthorized();
    error ZeroAddress();
    error DeployFailed();
    error InvalidLaunchConfig();
    error QuoteNotEnabled();
    error PoolAlreadyInitialized();
    error InitialBuyExceedsCap();
    error InvalidBuyPath();
    error InsufficientLaunchFee();

    constructor(
        address _nfpm,
        address _swapRouter,
        address _weth,
        address _lpLocker,
        address _escrow,
        address _treasury,
        uint256 _totalSupply,
        uint16 _airdropBps,
        uint16 _obsCardinality,
        uint16 _restrictionBps,
        uint32 _restrictionBlocks
    ) {
        if (
            _nfpm == address(0) || _swapRouter == address(0) || _weth == address(0)
                || _lpLocker == address(0) || _escrow == address(0) || _treasury == address(0)
        ) revert ZeroAddress();
        if (_totalSupply == 0 || _airdropBps == 0 || _airdropBps >= BPS_DENOM) {
            revert InvalidLaunchConfig();
        }
        if (_obsCardinality == 0) revert InvalidLaunchConfig();

        NFPM = INonfungiblePositionManager(_nfpm);
        SWAP_ROUTER = IV3SwapRouter(_swapRouter);
        WETH = _weth;
        LP_LOCKER = HoodLPLocker(_lpLocker);
        ESCROW = AirdropEscrow(_escrow);
        treasury = _treasury;
        owner = msg.sender;
        TOTAL_SUPPLY = _totalSupply;
        AIRDROP_BPS = _airdropBps;
        AIRDROP_AMOUNT = (_totalSupply * _airdropBps) / BPS_DENOM;
        LP_AMOUNT = _totalSupply - AIRDROP_AMOUNT;
        MAX_INITIAL_BUY = (_totalSupply * INITIAL_BUY_BPS_MAX) / BPS_DENOM;
        OBS_CARDINALITY = _obsCardinality;
        if (_restrictionBps == 0 || _restrictionBps > BPS_DENOM) revert InvalidLaunchConfig();
        RESTRICTION_BPS = _restrictionBps;
        RESTRICTION_BLOCKS = _restrictionBlocks;
        RESTRICTION_AMOUNT = (_totalSupply * _restrictionBps) / BPS_DENOM;
    }

    // ──────────────────────────────────────────────────────────────
    // Launch (1 tx: token + escrow airdrop + pool + locked LP + buy)
    // ──────────────────────────────────────────────────────────────

    /// @notice Launch token Socialhood dengan quote pilihan (WETH/USDG/stock).
    /// @param airdropRoot merkle root snapshot key holder creator (dihitung
    ///        backend dari tabel Holding, diverifikasi via endpoint publik).
    /// @param quoteToken pair pool — harus di-enable owner via setQuoteConfig.
    /// @param buyPathToQuote path V3 dari WETH SAMPAI quote utk initial buy
    ///        (hop terakhir quote→token ditambahkan kontrak). Utk quote WETH
    ///        cukup 20 byte alamat WETH. Kosongkan bila msg.value == 0.
    function launchToken(
        string calldata name_,
        string calldata symbol_,
        string calldata metadataURI_,
        address feeRecipient,
        bytes32 airdropRoot,
        address quoteToken,
        bytes calldata buyPathToQuote
    ) external payable nonReentrant returns (address token) {
        if (!quotes[quoteToken].enabled) revert QuoteNotEnabled();
        if (msg.value < launchFee) revert InsufficientLaunchFee();
        address effectiveRecipient = feeRecipient == address(0) ? msg.sender : feeRecipient;

        // 1) CREATE2 deterministic HoodToken — supply mint ke launcher ini.
        bytes32 salt = keccak256(abi.encode(msg.sender, creatorNonce[msg.sender]++, name_, symbol_));
        token = address(
            new HoodToken{salt: salt}(
                name_,
                symbol_,
                TOTAL_SUPPLY,
                metadataURI_,
                RESTRICTION_AMOUNT,
                RESTRICTION_AMOUNT,
                RESTRICTION_BLOCKS
            )
        );
        if (token == address(0)) revert DeployFailed();

        // 2-5) Pool + anti-grief + escrow + LP locked + registry
        //      (dipecah ke helper — via-ir stack limit).
        _setupMarket(token, quoteToken, effectiveRecipient, airdropRoot);

        emit TokenLaunched(
            token,
            msg.sender,
            quoteToken,
            name_,
            symbol_,
            tokens[token].pool,
            airdropRoot,
            tokens[token].startingTick
        );

        // 6) Launch fee (bila di-set) → treasury; sisanya jadi initial buy.
        if (launchFee > 0) {
            (bool feeOk,) = treasury.call{value: launchFee}("");
            if (!feeOk) revert InvalidLaunchConfig();
        }
        uint256 buyValue = msg.value - launchFee;

        // 7) Optional initial buy creator — atomik, sebelum sniper mana pun.
        //    ETH di-route: buyPathToQuote (WETH→...→quote, divalidasi ujung-
        //    ujungnya) + hop terakhir quote→token yang di-APPEND kontrak
        //    (selalu pool baru fee 1%). Hop tengah urusan frontend — eksekusi
        //    jelek cuma rugiin pembeli sendiri. HARD CAP 5% supply.
        if (buyValue > 0) {
            if (buyPathToQuote.length < 20 || (buyPathToQuote.length - 20) % 23 != 0) {
                revert InvalidBuyPath();
            }
            if (
                address(bytes20(buyPathToQuote[0:20])) != WETH
                    || address(bytes20(buyPathToQuote[buyPathToQuote.length - 20:])) != quoteToken
            ) revert InvalidBuyPath();

            bytes memory fullPath = bytes.concat(buyPathToQuote, bytes3(POOL_FEE), bytes20(token));

            IWETH(WETH).deposit{value: buyValue}();
            IERC20(WETH).forceApprove(address(SWAP_ROUTER), buyValue);
            uint256 tokensOut = SWAP_ROUTER.exactInput(
                IV3SwapRouter.ExactInputParams({
                    path: fullPath,
                    recipient: msg.sender,
                    amountIn: buyValue,
                    // Pool token lahir di tx ini; hop perantara pakai pool
                    // publik — proteksi harga = cap 5% di bawah.
                    amountOutMinimum: 0
                })
            );
            if (tokensOut > MAX_INITIAL_BUY) revert InitialBuyExceedsCap();
            emit InitialBuy(token, msg.sender, buyValue, tokensOut);
        }
    }

    /// @dev Pool + anti-grief + cardinality + escrow fund + LP locked +
    ///      registry. Dipecah dari launchToken (stack limit via-ir).
    function _setupMarket(address token, address quoteToken, address effectiveRecipient, bytes32 airdropRoot)
        internal
    {
        QuoteConfig memory qc = quotes[quoteToken];
        bool tokenIsToken0 = token < quoteToken;
        (address token0, address token1) = tokenIsToken0 ? (token, quoteToken) : (quoteToken, token);
        int24 priceTick = tokenIsToken0 ? qc.launchTickToken0 : -qc.launchTickToken0;

        address pool = NFPM.createAndInitializePoolIfNecessary(
            token0, token1, POOL_FEE, TickMath.getSqrtPriceAtTick(priceTick)
        );

        // Anti-grief: alamat token CREATE2-deterministik → pool bisa di-
        // front-run + pre-initialize di harga jahat; wajib persis di launch
        // tick kita atau launch revert bersih.
        (, int24 initTick,,,,,) = IUniswapV3Pool(pool).slot0();
        if (initTick != priceTick) revert PoolAlreadyInitialized();

        // Bekal TWAP graduasi (§14.3).
        IUniswapV3Pool(pool).increaseObservationCardinalityNext(OBS_CARDINALITY);

        // Pool wajib exempt dari restriction window (menerima sisi jual).
        HoodToken(token).setRestrictionExempt(pool);

        // Escrow airdrop key holder — quote + target dikunci di sini.
        IERC20(token).forceApprove(address(ESCROW), AIRDROP_AMOUNT);
        ESCROW.fund(token, pool, airdropRoot, AIRDROP_AMOUNT, quoteToken, qc.gradTargetMcap);

        // Posisi LP single-sided sisa supply, mint langsung ke locker.
        (int24 mintLower, int24 mintUpper, uint256 amt0, uint256 amt1) = tokenIsToken0
            ? (priceTick, MAX_USABLE_TICK, LP_AMOUNT, uint256(0))
            : (MIN_USABLE_TICK, priceTick, uint256(0), LP_AMOUNT);

        IERC20(token).forceApprove(address(NFPM), LP_AMOUNT);
        (uint256 positionId,,,) = NFPM.mint(
            INonfungiblePositionManager.MintParams({
                token0: token0,
                token1: token1,
                fee: POOL_FEE,
                tickLower: mintLower,
                tickUpper: mintUpper,
                amount0Desired: amt0,
                amount1Desired: amt1,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(LP_LOCKER),
                deadline: block.timestamp
            })
        );
        LP_LOCKER.registerPosition(token, positionId, effectiveRecipient, treasury);

        tokens[token] = TokenInfo({
            creator: msg.sender,
            feeRecipient: effectiveRecipient,
            pool: pool,
            quote: quoteToken,
            positionId: positionId,
            startingTick: priceTick,
            deployedAt: uint64(block.timestamp)
        });
        allTokens.push(token);
        creatorTokens[msg.sender].push(token);
    }

    // ──────────────────────────────────────────────────────────────
    // Views
    // ──────────────────────────────────────────────────────────────

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

    /// @notice Slice paginasi registry token — frontend baca list dalam SATU
    ///         call tanpa scan event log.
    function getTokens(uint256 offset, uint256 limit) external view returns (address[] memory list) {
        uint256 n = allTokens.length;
        if (offset >= n) return new address[](0);
        uint256 end = offset + limit;
        if (end < offset || end > n) end = n; // clamp + overflow-guard
        list = new address[](end - offset);
        for (uint256 i = offset; i < end; i++) {
            list[i - offset] = allTokens[i];
        }
    }

    function getCreatorTokens(address creator) external view returns (address[] memory) {
        return creatorTokens[creator];
    }

    /// @notice Salt CREATE2 utk prediksi alamat client-side.
    function tokenSalt(address creator, uint256 nonce, string calldata name_, string calldata symbol_)
        external
        pure
        returns (bytes32)
    {
        return keccak256(abi.encode(creator, nonce, name_, symbol_));
    }

    /// @notice Prediksi alamat token utk launch BERIKUTNYA creator (pattern
    ///         PONS) — frontend bisa tampilkan alamat sebelum tx dikirim.
    function predictTokenAddress(
        address creator,
        string calldata name_,
        string calldata symbol_,
        string calldata metadataURI_
    ) external view returns (address) {
        bytes32 salt = keccak256(abi.encode(creator, creatorNonce[creator], name_, symbol_));
        bytes32 initHash = keccak256(
            abi.encodePacked(
                type(HoodToken).creationCode,
                abi.encode(
                    name_,
                    symbol_,
                    TOTAL_SUPPLY,
                    metadataURI_,
                    RESTRICTION_AMOUNT,
                    RESTRICTION_AMOUNT,
                    RESTRICTION_BLOCKS
                )
            )
        );
        return address(uint160(uint256(keccak256(abi.encodePacked(hex"ff", address(this), salt, initHash)))));
    }

    // ──────────────────────────────────────────────────────────────
    // Admin — konfigurasi quote (hanya utk launch BERIKUTNYA; launch
    // yang sudah jadi terkunci di escrow), treasury, ownership.
    // ──────────────────────────────────────────────────────────────

    modifier onlyOwner() {
        if (msg.sender != owner) revert Unauthorized();
        _;
    }

    /// @notice Enable/update quote token. Tick & target perlu disesuaikan
    ///         berkala (harga ETH/saham drift) — angka dari config off-chain.
    function setQuoteConfig(address quote, bool enabled, int24 launchTickToken0, uint256 gradTargetMcap)
        external
        onlyOwner
    {
        if (quote == address(0)) revert ZeroAddress();
        if (enabled) {
            if (launchTickToken0 >= 0 || launchTickToken0 < MIN_USABLE_TICK) revert InvalidLaunchConfig();
            if (launchTickToken0 % TICK_SPACING != 0) revert InvalidLaunchConfig();
            if (gradTargetMcap == 0) revert InvalidLaunchConfig();
        }
        quotes[quote] = QuoteConfig({
            enabled: enabled,
            launchTickToken0: launchTickToken0,
            gradTargetMcap: gradTargetMcap
        });
        emit QuoteConfigSet(quote, enabled, launchTickToken0, gradTargetMcap);
    }

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

    /// @notice Fee flat per launch → treasury. 0 = gratis (default).
    function setLaunchFee(uint256 newFee) external onlyOwner {
        launchFee = newFee;
        emit LaunchFeeUpdated(newFee);
    }

    /// @dev 2-step (pattern PONS/OZ): salah alamat tidak langsung fatal —
    ///      owner baru harus acceptOwnership() dulu.
    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert ZeroAddress();
        pendingOwner = newOwner;
        emit OwnershipTransferStarted(owner, newOwner);
    }

    function acceptOwnership() external {
        if (msg.sender != pendingOwner) revert Unauthorized();
        emit OwnershipTransferred(owner, msg.sender);
        owner = msg.sender;
        pendingOwner = address(0);
    }
}
Chain explorer4978msChain node91ms