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
- Creator
- 0x5D4961e638…5586Cb5E6D
- 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