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
- Creator
- 0x4649D57d18…2e8c4682D6
- 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