BondingLaunchFactoryV2

0xc917056e80322c12957c98df98361309960e0c05

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
22,323 bytes
ABI entries
57
26 read · 5 write
License
mit

Contract information

Address
0xc917056e80322c12957c98df98361309960e0c05
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0xacfd6de2b7…bb30b7682f

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (26)

LAUNCH_FEE()uint256
MAX_DECAY_SECONDS()uint32
MAX_GRADUATION_ETH()uint128
MAX_SELL_LOCK_SECONDS()uint32
MAX_START_FEE_BPS()uint16
MAX_START_TICK()int24
MAX_STATIC_FEE_BPS()uint16
MIN_GRADUATION_ETH()uint128
MIN_START_TICK()int24
MIN_STATIC_FEE_BPS()uint16
PROTOCOL_SHARE_BPS()uint16
TICK_SPACING()int24
TOKEN_DECIMALS()uint8
TOKEN_SUPPLY()uint256
graduationThresholdEth()uint128
hook()address
launchFeeRecipient()address
launchLiquidity()uint128
owner()address
permit2()address
poolManager()address
positionManager()address
protocol()address
protocolFeeRecipient()address
sqrtPriceStartX96()uint160
startTick()int24

Events (5)

BondingLaunchCreatedDevBuyExecutedLaunchFeePaidOwnershipTransferredProtocolFeeRecipientChanged

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "contract IPositionManager",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "contract IAllowanceTransfer",
        "name": "permit2_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "protocol_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "launchFeeRecipient_",
        "type": "address"
      },
      {
        "internalType": "int24",
        "name": "startTick_",
        "type": "int24"
      },
      {
        "internalType": "uint128",
        "name": "graduationThresholdEth_",
        "type": "uint128"
      },
      {
        "internalType": "bytes32",
        "name": "hookSalt",
        "type": "bytes32"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "startBps",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "endBps",
        "type": "uint16"
      }
    ],
    "name": "DecayGoesUp",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint32",
        "name": "requested",
        "type": "uint32"
      },
      {
        "internalType": "uint32",
        "name": "max",
        "type": "uint32"
      }
    ],
    "name": "DecayTooLong",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DevBuyReturnedNothing",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "requested",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "cap",
        "type": "uint16"
      }
    ],
    "name": "FeeAboveCap",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "requested",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "floor",
        "type": "uint16"
      }
    ],
    "name": "FeeBelowFloor",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint128",
        "name": "value",
        "type": "uint128"
      },
      {
        "internalType": "uint128",
        "name": "min",
        "type": "uint128"
      },
      {
        "internalType": "uint128",
        "name": "max",
        "type": "uint128"
      }
    ],
    "name": "GraduationThresholdOutOfRange",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "maxAbsorbable",
        "type": "uint256"
      },
      {
        "internalType": "uint128",
        "name": "threshold",
        "type": "uint128"
      }
    ],
    "name": "GraduationUnreachable",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "hookAddr",
        "type": "address"
      }
    ],
    "name": "HookFlagsWrong",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "sent",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "required",
        "type": "uint256"
      }
    ],
    "name": "InsufficientLaunchFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LaunchFeeTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyPoolManager",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RefundFailed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SellLockDisabled",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "int24",
        "name": "tick",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "spacing",
        "type": "int24"
      }
    ],
    "name": "StartTickNotAligned",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "int24",
        "name": "tick",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "min",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "max",
        "type": "int24"
      }
    ],
    "name": "StartTickOutOfBounds",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "locker",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "positionTokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "graduationThresholdEth",
        "type": "uint256"
      }
    ],
    "name": "BondingLaunchCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "name": "DevBuyExecuted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "payer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "LaunchFeePaid",
    "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": "previous",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "current",
        "type": "address"
      }
    ],
    "name": "ProtocolFeeRecipientChanged",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "LAUNCH_FEE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_DECAY_SECONDS",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_GRADUATION_ETH",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "",
        "type": "uint128"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SELL_LOCK_SECONDS",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_START_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_START_TICK",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_STATIC_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_GRADUATION_ETH",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "",
        "type": "uint128"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_START_TICK",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_STATIC_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PROTOCOL_SHARE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOKEN_DECIMALS",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOKEN_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "uint16",
            "name": "poolFeeBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "feeDecayStartBps",
            "type": "uint16"
          },
          {
            "internalType": "uint32",
            "name": "feeDecayDurationSec",
            "type": "uint32"
          },
          {
            "internalType": "uint32",
            "name": "sellLockSeconds",
            "type": "uint32"
          }
        ],
        "internalType": "struct BondingLaunchFactoryV2.BondingLaunchParams",
        "name": "p",
        "type": "tuple"
      }
    ],
    "name": "createBondingLaunch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "positionTokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "locker",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "graduationThresholdEth",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "",
        "type": "uint128"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "contract LittleJohnBondingHook",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchFeeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchLiquidity",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "",
        "type": "uint128"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "permit2",
    "outputs": [
      {
        "internalType": "contract IAllowanceTransfer",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "contract IPositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocol",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolFeeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "setProtocolFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "sqrtPriceStartX96",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "",
        "type": "uint160"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "startTick",
    "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": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

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

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 {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {SqrtPriceMath} from "@uniswap/v4-core/src/libraries/SqrtPriceMath.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {LiquidityAmounts} from "@uniswap/v4-periphery/src/libraries/LiquidityAmounts.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";

import {LittleJohnToken} from "./LittleJohnToken.sol";
import {LittleJohnBondingHook} from "./LittleJohnBondingHook.sol";
import {LpLockerV4} from "./LpLockerV4.sol";

/**
 * BondingLaunchFactoryV2 — the LittleJohn "Bonding Launch", rebuilt as a REAL
 * Uniswap v4 pool from block one (operator directive 2026-07-09, NOXA/Flaunch
 * pattern). SUPERSEDES BondingLaunchFactory + BondingCurve: the old standalone
 * curve was invisible to DexScreener/GeckoTerminal until graduation; this pool
 * is indexed from its first trade.
 *
 * ONE TRANSACTION fires a complete Bonding Launch (same mechanics as
 * LaunchFactory.createLaunch — the proven Instant flow — with the starting
 * price fixed by the factory instead of chosen by the creator):
 *
 *   1. deploy LittleJohnToken (fixed supply, no mint fn, no owner),
 *   2. register the pool's immutable fee/sell-lock config AND its graduation
 *      milestone on the bonding hook,
 *   3. initialize the v4 pool at the factory's immutable NEAR-ZERO `startTick`,
 *   4. mint ONE single-sided position holding the entire supply over
 *      [minUsableTick, startTick], owner = a fresh per-launch LpLockerV4 (the
 *      NFT is BORN inside the locker — locked from block one, forever),
 *   5. optional atomic dev-buy (msg.value), tokens to the creator.
 *
 * THE POOL IS THE CURVE: the full supply sits single-sided at the low starting
 * price, so buys walk the price up the range exactly like a bonding curve —
 * the rising price is the anti-snipe, and the (optional) fee-decay presets
 * stack on top like any Instant launch.
 *
 * FIXED START, NO CREATOR-CHOSEN MCAP (operator decision): every bonding
 * launch starts at the SAME price — `startTick` is a per-factory immutable,
 * like the old per-factory `virtualEth`. The deploy script derives it from the
 * live ETH/USD print so the first-trade market cap reads ~$4–6k equivalent
 * (pump.fun starts ~$4k, NOXA ~$5.5k); it floats with ETH thereafter, exactly
 * like pump.fun's $4k floats with SOL. The Instant path (LaunchFactory) stays
 * the only place a creator picks a starting mcap. The compile-time band
 * [MIN_START_TICK, MAX_START_TICK] is the anti-brick bound (the analog of the
 * old virtualEth band): it spans ~$475 to ~$191k equivalent at ETH ≈ $1,700 —
 * wide enough for any plausible ETH price, impossible to configure absurdly.
 *
 * GRADUATION = A MILESTONE, NOT A MIGRATION: when the pool has absorbed
 * >= `graduationThresholdEth` of ETH (a bounded per-factory immutable; ~4 ETH
 * on mainnet, NOXA-like; a tiny value on testnet so a proof fits a faucet
 * budget), the hook emits `BondingGraduated(poolId, ...)` — once, exactly at
 * the threshold, latched forever. NOTHING moves: no pool migration, no
 * liquidity event, trading continues in the same pool. The indexer/UI flip the
 * badge and hide the progress bar; that is the entire effect.
 *
 * RUG-PROOF INVARIANTS: identical to LaunchFactory (fee caps / decay bounds /
 * sell-lock bounds re-enforced by the hook; every LAUNCH immutable once
 * created; locker custody enforced by LpLockerV4's absence of code), plus the
 * milestone invariants documented in LittleJohnBondingHook. The factory is
 * Ownable with EXACTLY ONE owner power: setProtocolFeeRecipient, which
 * redirects where FUTURE launches' protocol fees accrue — each launch bakes
 * the value into its LpLockerV4 immutably at creation, so no existing launch
 * can ever be redirected, and the hook/milestone config has no setter, owner,
 * or upgrade path. The approved mainnet manifest deliberately separates the
 * operator owner from the protocol fee recipient.
 *
 * TIME: block.timestamp everywhere (Arbitrum Orbit/Nitro — block.number is an
 * L1 estimate and is never used).
 */
contract BondingLaunchFactoryV2 is IUnlockCallback, ReentrancyGuard, Ownable {
    using SafeERC20 for IERC20;

    // ---------- launch constants (mirror LaunchFactory) ----------

    /// Fixed supply per launch: 1,000,000,000 tokens, 18 decimals — the whole
    /// supply is the pool's single-sided liquidity from block one.
    uint256 public constant TOKEN_SUPPLY = 1_000_000_000e18;
    /// Flat anti-spam fee paid once per launch. Immutable by bytecode: no setter.
    uint256 public constant LAUNCH_FEE = 0.0045 ether;
    uint8 public constant TOKEN_DECIMALS = 18;
    int24 public constant TICK_SPACING = 200;
    /// Protocol's share of trading fees: 3333 bps = 1/3 (FEES.PROTOCOL_FEE_SHARE).
    uint16 public constant PROTOCOL_SHARE_BPS = 3333;

    // invariant bounds (duplicated from the hook for cheap pre-checks;
    // the hook re-enforces them — it does not trust this contract)
    uint16 public constant MAX_STATIC_FEE_BPS = 500; // 5% hard fee cap
    uint16 public constant MIN_STATIC_FEE_BPS = 100; // 1% operator revenue floor
    uint16 public constant MAX_START_FEE_BPS = 4000; // 40% global fee ceiling
    uint32 public constant MAX_DECAY_SECONDS = 3600;
    uint32 public constant MAX_SELL_LOCK_SECONDS = 0;

    /// Anti-brick band for the immutable `startTick` (tick = log1.0001(token
    /// per ETH); HIGHER tick = CHEAPER token). Derivation at ETH ≈ $1,700,
    /// supply fixed at 1e9: tick 160000 ⇒ ~$191k starting mcap (absurdly high
    /// start — cap), tick 220000 ⇒ ~$475 (absurdly low — floor). The deploy
    /// script targets ~$4–6k equivalent inside this band (tick ≈ 196400 at
    /// ETH $1,700); the band only forecloses configs ~40x off target either way.
    int24 public constant MIN_START_TICK = 160_000;
    int24 public constant MAX_START_TICK = 220_000;

    /// Sane bounds on the per-factory graduation threshold (the analog of the
    /// old BondingCurve virtualEth band, same rationale): below MIN the
    /// milestone fires on sub-microether dust and means nothing; above MAX it
    /// is unreachable and the badge could never flip. Mainnet target ~4 ETH
    /// (NOXA graduates at a fixed 4.2 ETH absorbed); testnet bakes a tiny
    /// value (e.g. 2e15 = 0.002 ETH) so a live proof fits a faucet budget.
    uint128 public constant MIN_GRADUATION_ETH = 1e12; // 1e-6 ETH
    uint128 public constant MAX_GRADUATION_ETH = 1e24; // 1e6 ETH

    // ---------- wiring (immutable, except the owner-settable protocol
    // recipient for FUTURE launches — see protocolFeeRecipient below) ----------

    IPoolManager public immutable poolManager;
    IPositionManager public immutable positionManager;
    IAllowanceTransfer public immutable permit2;
    LittleJohnBondingHook public immutable hook;
    /// Protocol fee recipient baked into the locker of every FUTURE launch.
    /// Owner-settable (Ownable, owner = deployer): the operator can redirect
    /// where NEW bonding launches' protocol fees accrue WITHOUT any power over
    /// existing launches — each locker bakes THIS value in immutably at
    /// creation. The legacy `protocol()` getter still returns the current value.
    address public protocolFeeRecipient;
    /// Dedicated launch-fee wallet, immutable for the lifetime of this factory.
    address public immutable launchFeeRecipient;
    /// The fixed starting tick of every bonding pool (see header).
    int24 public immutable startTick;
    /// sqrt price at `startTick` (cached; also the milestone's price anchor).
    uint160 public immutable sqrtPriceStartX96;
    /// Net ETH the pool must absorb for the graduation milestone (see header).
    uint128 public immutable graduationThresholdEth;
    /// The launch position's liquidity — identical for every launch (fixed
    /// supply, fixed range), computed once here and re-used for the mint and
    /// the hook's milestone math.
    uint128 public immutable launchLiquidity;

    struct BondingLaunchParams {
        string name;
        string symbol;
        /// Base pool trading fee in bps (settled fee after any decay window).
        uint16 poolFeeBps;
        /// Anti-snipe decay opening fee in bps; 0 = decay off.
        uint16 feeDecayStartBps;
        /// Decay window in seconds; 0 = decay off.
        uint32 feeDecayDurationSec;
        /// Reserved legacy field; must be zero for every new launch.
        uint32 sellLockSeconds;
    }

    struct DevBuyData {
        PoolKey key;
        uint256 ethIn;
        address recipient;
    }

    /// Fee/sell-lock config is in the hook's LaunchRegistered event, the
    /// milestone in BondingMilestoneRegistered, and the starting tick in the
    /// PoolManager's Initialize event (same tx, same poolId) — the indexer
    /// joins on poolId. `graduationThresholdEth` is repeated here so a launch
    /// row is self-contained (progress-bar denominator without a factory read).
    event BondingLaunchCreated(
        PoolId indexed poolId,
        address indexed creator,
        address token,
        address locker,
        uint256 positionTokenId,
        uint256 graduationThresholdEth
    );
    event DevBuyExecuted(PoolId indexed poolId, uint256 ethIn, uint256 tokensOut);
    /// The owner redirected where FUTURE launches' protocol fees accrue. Existing
    /// launches are unaffected (their locker recipient is immutable).
    event ProtocolFeeRecipientChanged(address indexed previous, address indexed current);
    event LaunchFeePaid(address indexed payer, address indexed recipient, uint256 amount);

    error ZeroAddress();
    error StartTickOutOfBounds(int24 tick, int24 min, int24 max);
    error StartTickNotAligned(int24 tick, int24 spacing);
    error GraduationThresholdOutOfRange(uint128 value, uint128 min, uint128 max);
    error GraduationUnreachable(uint256 maxAbsorbable, uint128 threshold);
    error FeeAboveCap(uint16 requested, uint16 cap);
    error FeeBelowFloor(uint16 requested, uint16 floor);
    error DecayGoesUp(uint16 startBps, uint16 endBps);
    error DecayTooLong(uint32 requested, uint32 max);
    error SellLockDisabled();
    error HookFlagsWrong(address hookAddr);
    error OnlyPoolManager();
    error DevBuyReturnedNothing();
    error RefundFailed();
    error InsufficientLaunchFee(uint256 sent, uint256 required);
    error LaunchFeeTransferFailed();

    constructor(
        IPoolManager poolManager_,
        IPositionManager positionManager_,
        IAllowanceTransfer permit2_,
        address protocol_,
        address launchFeeRecipient_,
        int24 startTick_,
        uint128 graduationThresholdEth_,
        bytes32 hookSalt
    ) Ownable(msg.sender) {
        if (
            address(poolManager_) == address(0) || address(positionManager_) == address(0)
                || address(permit2_) == address(0) || protocol_ == address(0) || launchFeeRecipient_ == address(0)
        ) revert ZeroAddress();
        if (startTick_ < MIN_START_TICK || startTick_ > MAX_START_TICK) {
            revert StartTickOutOfBounds(startTick_, MIN_START_TICK, MAX_START_TICK);
        }
        if (startTick_ % TICK_SPACING != 0) revert StartTickNotAligned(startTick_, TICK_SPACING);
        if (graduationThresholdEth_ < MIN_GRADUATION_ETH || graduationThresholdEth_ > MAX_GRADUATION_ETH) {
            revert GraduationThresholdOutOfRange(graduationThresholdEth_, MIN_GRADUATION_ETH, MAX_GRADUATION_ETH);
        }

        poolManager = poolManager_;
        positionManager = positionManager_;
        permit2 = permit2_;
        protocolFeeRecipient = protocol_;
        launchFeeRecipient = launchFeeRecipient_;
        startTick = startTick_;
        graduationThresholdEth = graduationThresholdEth_;

        // The launch position is identical for every launch: full supply
        // single-sided over [minUsableTick, startTick]. Compute its liquidity
        // once; the mint and the hook's absorbed-ETH math both use it.
        uint160 sqrtStart = TickMath.getSqrtPriceAtTick(startTick_);
        uint160 sqrtLower = TickMath.getSqrtPriceAtTick(TickMath.minUsableTick(TICK_SPACING));
        uint128 liq = LiquidityAmounts.getLiquidityForAmount1(sqrtLower, sqrtStart, TOKEN_SUPPLY);
        sqrtPriceStartX96 = sqrtStart;
        launchLiquidity = liq;

        // Range-brick foreclosure: the threshold must be reachable strictly
        // inside the range (the milestone would otherwise never fire). The
        // round-DOWN max-absorbable matches the hook's measurement exactly.
        uint256 maxAbsorbable = SqrtPriceMath.getAmount0Delta(sqrtLower, sqrtStart, liq, false);
        if (maxAbsorbable < graduationThresholdEth_) {
            revert GraduationUnreachable(maxAbsorbable, graduationThresholdEth_);
        }

        // CREATE2-deploy our own bonding hook with a mined salt so the hook's
        // `factory` immutable is THIS address (no post-deploy wiring). The
        // flag set is LittleJohnHook's PLUS beforeAddLiquidity — the bonding
        // hook's LP lockdown (only the launch's own seeding mint may ever add
        // liquidity). All SIX bits are asserted here, exactly.
        hook = new LittleJohnBondingHook{salt: hookSalt}(poolManager_, address(this));
        if (
            uint160(address(hook)) & Hooks.ALL_HOOK_MASK
                != uint160(
                    Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG
                        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_FLAG
                        | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG
                )
        ) revert HookFlagsWrong(address(hook));
    }

    // ---------- protocol fee recipient (owner-settable, future launches only) ----------

    /// Legacy getter preserved (selector unchanged): returns the current settable
    /// `protocolFeeRecipient`, so panel/keeper/fork-test reads of `protocol()`
    /// keep working.
    function protocol() external view returns (address) {
        return protocolFeeRecipient;
    }

    /// Owner-only: redirect where FUTURE bonding launches' protocol fees accrue.
    /// Does NOT touch any existing launch (immutable in each LpLockerV4).
    function setProtocolFeeRecipient(address newRecipient) external onlyOwner {
        if (newRecipient == address(0)) revert ZeroAddress();
        address previous = protocolFeeRecipient;
        protocolFeeRecipient = newRecipient;
        emit ProtocolFeeRecipientChanged(previous, newRecipient);
    }

    /**
     * Fire a bonding launch. `msg.sender` is the creator (locker fee recipient
     * + sell-lock subject). `msg.value` is the optional atomic dev-buy. The
     * starting price is the factory's — the creator chooses only the fee/
     * decay/sell-lock config, never the mcap.
     */
    function createBondingLaunch(BondingLaunchParams calldata p)
        external
        payable
        nonReentrant
        returns (address token, PoolId poolId, uint256 positionTokenId, address locker)
    {
        if (msg.value < LAUNCH_FEE) revert InsufficientLaunchFee(msg.value, LAUNCH_FEE);
        _validate(p);

        // 1. token: fixed supply minted to this factory, no mint fn, no owner
        LittleJohnToken t = new LittleJohnToken(p.name, p.symbol, TOKEN_DECIMALS, TOKEN_SUPPLY, address(this));
        token = address(t);

        // native ETH (address(0)) sorts first: always currency0 on this chain
        PoolKey memory key = PoolKey({
            currency0: Currency.wrap(address(0)),
            currency1: Currency.wrap(token),
            fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
            tickSpacing: TICK_SPACING,
            hooks: IHooks(address(hook))
        });
        poolId = key.toId();

        // 2. per-launch locker FIRST, so its address can be baked into the
        //    hook's immutable config. The position NFT id is deterministic —
        //    nothing else mints on the posm between here and step 5.
        positionTokenId = positionManager.nextTokenId();
        LpLockerV4 lock = new LpLockerV4(
            positionManager,
            poolManager,
            positionTokenId,
            key.currency0,
            key.currency1,
            msg.sender,
            protocolFeeRecipient, // CURRENT factory recipient, baked immutable into this locker
            PROTOCOL_SHARE_BPS
        );
        locker = address(lock);

        // 3. immutable per-pool config (hook re-validates every bound) + the
        //    graduation milestone. Decay off => start == end (flat curve).
        uint16 startBps = p.feeDecayDurationSec == 0 ? p.poolFeeBps : p.feeDecayStartBps;
        hook.register(poolId, msg.sender, startBps, p.poolFeeBps, p.feeDecayDurationSec, p.sellLockSeconds, locker);
        hook.registerBonding(poolId, graduationThresholdEth, launchLiquidity, sqrtPriceStartX96);

        // 4. pool starts exactly at the factory's near-zero start tick
        poolManager.initialize(key, sqrtPriceStartX96);

        // 5. single-sided position over [minUsableTick, startTick] holding the
        //    whole supply, minted DIRECTLY into the locker
        _mintFullSupplyPosition(key, token, locker);
        emit BondingLaunchCreated(poolId, msg.sender, token, locker, positionTokenId, graduationThresholdEth);

        // 6. optional atomic dev-buy with the creator's ETH
        if (msg.value > LAUNCH_FEE) _devBuy(key, poolId, msg.value - LAUNCH_FEE);

        // rounding dust from the liquidity math goes to the creator, so the
        // factory ends the tx holding nothing.
        uint256 dust = t.balanceOf(address(this));
        if (dust > 0) IERC20(token).safeTransfer(msg.sender, dust);

        (bool feePaid,) = launchFeeRecipient.call{value: LAUNCH_FEE}("");
        if (!feePaid) revert LaunchFeeTransferFailed();
        emit LaunchFeePaid(msg.sender, launchFeeRecipient, LAUNCH_FEE);
    }

    function _devBuy(PoolKey memory key, PoolId poolId, uint256 ethIn) internal {
        (uint256 ethUsed, uint256 tokensOut) =
            abi.decode(poolManager.unlock(abi.encode(DevBuyData(key, ethIn, msg.sender))), (uint256, uint256));
        if (tokensOut == 0) revert DevBuyReturnedNothing();
        if (ethIn > ethUsed) {
            (bool ok,) = msg.sender.call{value: ethIn - ethUsed}("");
            if (!ok) revert RefundFailed();
        }
        emit DevBuyExecuted(poolId, ethUsed, tokensOut);
    }

    /// Dev-buy execution inside the PoolManager unlock. Only the PoolManager
    /// can call this, and it only runs while THIS contract holds the unlock.
    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert OnlyPoolManager();
        DevBuyData memory d = abi.decode(data, (DevBuyData));

        // buy: ETH (currency0) in, token out => zeroForOne, exact-in
        BalanceDelta delta = poolManager.swap(
            d.key,
            SwapParams({
                zeroForOne: true, amountSpecified: -int256(d.ethIn), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
            }),
            ""
        );
        uint256 ethUsed = uint256(-int256(delta.amount0()));
        uint256 tokensOut = uint256(uint128(delta.amount1()));

        poolManager.take(d.key.currency1, d.recipient, tokensOut);
        poolManager.settle{value: ethUsed}();
        return abi.encode(ethUsed, tokensOut);
    }

    // ---------- internals ----------

    function _validate(BondingLaunchParams calldata p) internal pure {
        if (p.poolFeeBps < MIN_STATIC_FEE_BPS) revert FeeBelowFloor(p.poolFeeBps, MIN_STATIC_FEE_BPS);
        if (p.poolFeeBps > MAX_STATIC_FEE_BPS) revert FeeAboveCap(p.poolFeeBps, MAX_STATIC_FEE_BPS);
        if (p.feeDecayDurationSec > MAX_DECAY_SECONDS) revert DecayTooLong(p.feeDecayDurationSec, MAX_DECAY_SECONDS);
        if (p.feeDecayDurationSec != 0) {
            if (p.feeDecayStartBps > MAX_START_FEE_BPS) revert FeeAboveCap(p.feeDecayStartBps, MAX_START_FEE_BPS);
            if (p.feeDecayStartBps < p.poolFeeBps) revert DecayGoesUp(p.feeDecayStartBps, p.poolFeeBps);
        }
        if (p.sellLockSeconds != 0) revert SellLockDisabled();
    }

    /// Mint the full-supply single-sided position (token side only: the range
    /// sits entirely at/below spot, so amount0Max — the ETH side — is zero)
    /// into the locker via the REAL PositionManager. Uses the precomputed
    /// per-factory `launchLiquidity` (identical for every launch).
    function _mintFullSupplyPosition(PoolKey memory key, address token, address locker) internal {
        int24 tickLower = TickMath.minUsableTick(TICK_SPACING);

        // posm settles ERC20s through Permit2
        IERC20(token).forceApprove(address(permit2), TOKEN_SUPPLY);
        permit2.approve(token, address(positionManager), uint160(TOKEN_SUPPLY), uint48(block.timestamp));

        bytes memory actions = abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR));
        bytes[] memory params = new bytes[](2);
        params[0] = abi.encode(
            key, tickLower, startTick, uint256(launchLiquidity), uint128(0), uint128(TOKEN_SUPPLY), locker, bytes("")
        );
        params[1] = abi.encode(key.currency0, key.currency1);
        positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp);

        IERC20(token).forceApprove(address(permit2), 0);
    }

    // NOTE (deliberate): no receive()/fallback — the factory can only hold
    // ETH transiently inside createBondingLaunch (msg.value), never at rest.
}
Chain explorer2157msChain node104ms