HoodsFactory

0xc7c1d953861bc0e12f4e5ce3abf54c5e5b0465c1

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
10,572 bytes
ABI entries
44
26 read · 8 write
License
none

Contract information

Address
0xc7c1d953861bc0e12f4e5ce3abf54c5e5b0465c1
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x8e726adc23…a332673eaf

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (26)

CURVE_SUPPLY()uint256
DEAD()address
MAX_LAUNCH_FEE()uint256
MAX_LAUNCH_LIMIT_BPS()uint256
MAX_LAUNCH_PERIOD()uint256
MIN_LAUNCH_LIMIT_BPS()uint256
POOL_FEE()uint24
TAIL_SUPPLY()uint256
TICK_SPACING()int24
TOTAL_SUPPLY()uint256
VANITY_SUFFIX()uint16
accruedFees()uint256
creatorNFT()address
hook()address
isHoodsToken(address)bool
launchFee()uint256
launchLimitBps()uint256
launchPeriod()uint256
owner()address
permit2()address
poolManager()address
positionManager()address
predictToken(address, bytes32)address
tokenImplementation()address
totalLaunches()uint256
treasury()address

Events (8)

LaunchFeeChangedLaunchFeePaidLaunchFeesCollectedLaunchLimitChangedLaunchPeriodChangedLaunchedOwnerChangedTreasuryChanged

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "permit2_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "hook_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "creatorNFT_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenImplementation_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "LaunchFeeChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "LaunchFeePaid",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "LaunchFeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "bps",
        "type": "uint256"
      }
    ],
    "name": "LaunchLimitChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "seconds_",
        "type": "uint256"
      }
    ],
    "name": "LaunchPeriodChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "graduationTick",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "metadataUri",
        "type": "string"
      }
    ],
    "name": "Launched",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnerChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      }
    ],
    "name": "TreasuryChanged",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "CURVE_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEAD",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_LAUNCH_FEE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_LAUNCH_LIMIT_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_LAUNCH_PERIOD",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_LAUNCH_LIMIT_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POOL_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TAIL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "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": "VANITY_SUFFIX",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "accruedFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "collectLaunchFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creatorNFT",
    "outputs": [
      {
        "internalType": "contract HoodsCreatorNFT",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "contract IHooks",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isHoodsToken",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataUri",
        "type": "string"
      },
      {
        "internalType": "bytes32",
        "name": "userSalt",
        "type": "bytes32"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchLimitBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchPeriod",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "permit2",
    "outputs": [
      {
        "internalType": "contract IPermit2Minimal",
        "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 IPositionManagerMinimal",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "userSalt",
        "type": "bytes32"
      }
    ],
    "name": "predictToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "setLaunchFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "bps",
        "type": "uint256"
      }
    ],
    "name": "setLaunchLimitBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "seconds_",
        "type": "uint256"
      }
    ],
    "name": "setLaunchPeriod",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "setOwner",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      }
    ],
    "name": "setTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenImplementation",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalLaunches",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

/*
█   █  ███   ███  ████   ████     █   █ █████ █   █ █████
█   █ █   █ █   █ █   █ █         ██ ██ █     ██ ██ █
█████ █   █ █   █ █   █  ███      █ █ █ ████  █ █ █ ████
█   █ █   █ █   █ █   █     █  █  █   █ █     █   █ █
█   █  ███   ███  ████  ████   █  █   █ █████ █   █ █████

   The most rewarding place to launch and trade.
   Fixed supply · LP burned forever · fees flow to creators.

   web  https://hoods.meme
   x    https://x.com/hoodsmeme
   tg   https://t.me/hoods_meme
*/


import { IPoolManager } from "v4-core/src/interfaces/IPoolManager.sol";
import { IUnlockCallback } from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
import { PoolKey } from "v4-core/src/types/PoolKey.sol";
import { Currency } from "v4-core/src/types/Currency.sol";
import { CurrencyLibrary } from "v4-core/src/types/Currency.sol";
import { IHooks } from "v4-core/src/interfaces/IHooks.sol";
import { BalanceDelta } from "v4-core/src/types/BalanceDelta.sol";
import { TickMath } from "v4-core/src/libraries/TickMath.sol";
import { LiquidityAmounts } from "v4-periphery/src/libraries/LiquidityAmounts.sol";
import { Actions } from "v4-periphery/src/libraries/Actions.sol";
import { HoodsToken } from "./HoodsToken.sol";
import { HoodsCreatorNFT } from "./HoodsCreatorNFT.sol";
import { CurveConfig } from "./CurveConfig.sol";

interface IHoodsHook {
    function registerLaunch(PoolKey calldata key, int24 graduationTick) external;
}

interface IPositionManagerMinimal {
    function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable;
}

interface IPermit2Minimal {
    function approve(address token, address spender, uint160 amount, uint48 expiration) external;
}

interface IERC20Minimal {
    function approve(address spender, uint256 amount) external returns (bool);
    function balanceOf(address) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
}

/// @title HoodsFactory — atomic v4 launcher (arc §2.1, §3)
/// @notice One tx: CREATE2 vanity token (suffix `9999`, sender-bound salt) → pool init at the config's
/// price → single-sided full-supply position minted straight to 0xdEaD (provable permanent lock, no
/// locker) → Creator NFT to msg.sender → hook launch-registry → event. Configs are IMMUTABLE (arc §10:
/// Klik's mutable configs/hook/fee-receiver are all rejected). Native ETH is always currency0.
contract HoodsFactory is IUnlockCallback {
    using CurrencyLibrary for Currency;

    uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18; // 1B, minted across the two locked positions
    uint256 public constant CURVE_SUPPLY = 800_000_000e18; // 80% → Position A (the curve, [grad, launch])
    uint256 public constant TAIL_SUPPLY = 200_000_000e18; // 20% → Position B (the tail, [tail, grad])
    uint24 public constant POOL_FEE = 0; // LP fee 0 — the hook takes the fee
    int24 public constant TICK_SPACING = 200;
    uint16 public constant VANITY_SUFFIX = 0x9999; // low 16 bits (hex-string suffix), branding only
    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;

    IPoolManager public immutable poolManager;
    IPositionManagerMinimal public immutable positionManager;
    IPermit2Minimal public immutable permit2;
    IHooks public immutable hook;
    HoodsCreatorNFT public immutable creatorNFT;
    address public immutable tokenImplementation;

    uint256 public totalLaunches;
    mapping(address => bool) public isHoodsToken; // registry (indexer / UI verification, arc §3.1)

    // ── bounded owner: tunes the launch-limit window for FUTURE launches (baked per-token at launch,
    //    so existing tokens never change) and the flat deploy fee (hard-capped). Cannot mint, move LP,
    //    or seize tokens — no such code. ──
    address public owner;
    uint256 public launchPeriod = 30 minutes; // limit-window baked into each new token
    uint256 public constant MAX_LAUNCH_PERIOD = 24 hours; // hard bound — limits can never trap a token
    uint256 public launchLimitBps = 200; // per-wallet launch cap (bps of supply) baked into each new token
    uint256 public constant MIN_LAUNCH_LIMIT_BPS = 50; // 0.5% — a cap below this would block all trading
    uint256 public constant MAX_LAUNCH_LIMIT_BPS = 1000; // 10% — above this the "limit" is decorative
    uint256 public launchFee = 0; // flat deploy fee per launch — DEFAULT 0 (free); owner can raise ≤ MAX_LAUNCH_FEE
    uint256 public constant MAX_LAUNCH_FEE = 0.01 ether; // hard cap — the fee can never be extractive
    uint256 public accruedFees; // collected launch fees awaiting sweep (pull model — no hot-path sends)
    address public treasury; // launch-fee destination (owner-settable, never zero)

    event OwnerChanged(address indexed previousOwner, address indexed newOwner);
    event LaunchPeriodChanged(uint256 seconds_);
    event LaunchLimitChanged(uint256 bps);
    event LaunchFeeChanged(uint256 fee);
    event TreasuryChanged(address indexed treasury);
    event LaunchFeePaid(address indexed token, address indexed creator, uint256 amount);
    event LaunchFeesCollected(address indexed treasury, uint256 amount);

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    /// Set the launch-limit window (seconds) for tokens launched AFTER this call. Bounded; never
    /// affects already-live tokens (each bakes its window at launch).
    function setLaunchPeriod(uint256 seconds_) external onlyOwner {
        require(seconds_ <= MAX_LAUNCH_PERIOD, "too long");
        launchPeriod = seconds_;
        emit LaunchPeriodChanged(seconds_);
    }

    /// Set the per-wallet launch-limit (bps of supply) for FUTURE launches. Bounded both ways; baked
    /// per-token at launch, so tuning never changes an already-live token's rules.
    function setLaunchLimitBps(uint256 bps) external onlyOwner {
        require(bps >= MIN_LAUNCH_LIMIT_BPS && bps <= MAX_LAUNCH_LIMIT_BPS, "limit out of bounds");
        launchLimitBps = bps;
        emit LaunchLimitChanged(bps);
    }

    /// Set the flat deploy fee for FUTURE launches. Hard-capped at MAX_LAUNCH_FEE; 0 removes the fee.
    function setLaunchFee(uint256 fee) external onlyOwner {
        require(fee <= MAX_LAUNCH_FEE, "fee too high");
        launchFee = fee;
        emit LaunchFeeChanged(fee);
    }

    /// Rotate the launch-fee destination (e.g. to a multisig).
    function setTreasury(address treasury_) external onlyOwner {
        require(treasury_ != address(0), "zero treasury");
        treasury = treasury_;
        emit TreasuryChanged(treasury_);
    }

    /// Sweep accrued launch fees to the treasury. Permissionless — the destination is fixed, and a
    /// failing treasury can never brick launches because fees accrue instead of being sent inline.
    function collectLaunchFees() external {
        uint256 amount = accruedFees;
        require(amount > 0, "nothing accrued");
        accruedFees = 0;
        (bool ok,) = payable(treasury).call{ value: amount }("");
        require(ok, "treasury send failed");
        emit LaunchFeesCollected(treasury, amount);
    }

    /// Rotate owner (e.g. to a multisig) or renounce with address(0).
    function setOwner(address newOwner) external onlyOwner {
        emit OwnerChanged(owner, newOwner);
        owner = newOwner;
    }

    event Launched(
        address indexed token,
        bytes32 indexed poolId,
        address indexed creator,
        int24 graduationTick,
        string name,
        string symbol,
        string metadataUri
    );

    constructor(
        address poolManager_,
        address positionManager_,
        address permit2_,
        address hook_,
        address creatorNFT_,
        address tokenImplementation_,
        address treasury_
    ) {
        require(treasury_ != address(0), "zero treasury");
        poolManager = IPoolManager(poolManager_);
        positionManager = IPositionManagerMinimal(positionManager_);
        permit2 = IPermit2Minimal(permit2_);
        hook = IHooks(hook_);
        creatorNFT = HoodsCreatorNFT(creatorNFT_);
        tokenImplementation = tokenImplementation_;
        treasury = treasury_;
        owner = msg.sender;
        emit OwnerChanged(address(0), msg.sender);
        emit TreasuryChanged(treasury_);
    }

    /// `userSalt` is ground off-chain so predictToken(msg.sender, userSalt) ends in 0x9999. Salt is
    /// sender-bound (stolen salts yield a different address). One fixed curve — no configId.
    function launch(string calldata name, string calldata symbol, string calldata metadataUri, bytes32 userSalt)
        external
        payable
        returns (address token, bytes32 poolId)
    {
        uint256 fee = launchFee; // snapshot — owner tuning mid-tx can't change this launch's terms
        require(msg.value >= fee, "launch fee");
        uint256 strayBefore = address(this).balance - msg.value; // any ETH resident BEFORE this call (not ours to refund)
        uint160 sqrtLaunch = TickMath.getSqrtPriceAtTick(CurveConfig.TICK_LAUNCH);

        // 1) deterministic vanity token
        bytes32 salt = keccak256(abi.encodePacked(msg.sender, userSalt));
        token = _clone(salt);
        require(uint16(uint160(token)) == VANITY_SUFFIX, "vanity");
        // full supply to the factory (it seeds the LP + dev-buy); creator + launch window baked in
        HoodsToken(token).initialize(name, symbol, metadataUri, TOTAL_SUPPLY, address(this), msg.sender, launchPeriod, launchLimitBps);

        // 2) pool: ETH (currency0) / token (currency1), LP fee 0, our hook
        PoolKey memory key = PoolKey({
            currency0: Currency.wrap(address(0)),
            currency1: Currency.wrap(token),
            fee: POOL_FEE,
            tickSpacing: TICK_SPACING,
            hooks: hook
        });
        poolManager.initialize(key, sqrtLaunch); // fires hook.beforeInitialize (records launch block)
        poolId = keccak256(abi.encode(key));
        IHoodsHook(address(hook)).registerLaunch(key, CurveConfig.GRADUATION_TICK);

        // 3) two single-sided locked positions (80% curve + 20% tail) → 0xdEaD (permanent lock)
        _mintLockedPositions(key, sqrtLaunch, token);

        // 4) creator ownership NFT (fee-stream indirection)
        creatorNFT.mint(msg.sender, uint256(uint160(token)));

        // 5) take the deploy fee (accrues for a pull-model treasury sweep), then the optional atomic
        //    dev-buy with the REMAINDER — fee-free (launch block) and EXEMPT from the buy limits (the
        //    hook waives them for sender == factory, arc §5); tokens delivered to the creator.
        if (fee > 0) {
            accruedFees += fee;
            emit LaunchFeePaid(token, msg.sender, fee);
        }
        uint256 devEth = msg.value - fee;
        if (devEth > 0) _devBuy(key, token, devEth, msg.sender);

        // burn any residual token dust the position didn't consume (tick-snap rounding)
        uint256 dust = IERC20Minimal(token).balanceOf(address(this));
        if (dust > 0) IERC20Minimal(token).transfer(DEAD, dust);

        isHoodsToken[token] = true;
        unchecked {
            ++totalLaunches;
        }
        emit Launched(token, poolId, msg.sender, CurveConfig.GRADUATION_TICK, name, symbol, metadataUri);

        // refund only the UNCONSUMED portion of the dev-buy (e.g. a buy that walked the whole curve and
        // hit the price limit) — NOT the whole balance; stray ETH sent to the factory isn't the
        // launcher's to sweep (M-2), and this launch's fee stays accrued for the treasury. Last, after
        // all state writes (CEI).
        uint256 refund = address(this).balance - strayBefore - fee;
        if (refund > 0) {
            (bool ok,) = payable(msg.sender).call{ value: refund }("");
            require(ok, "refund failed");
        }
    }

    // ── dev-buy via the PoolManager unlock pattern (self-contained; buyer = creator via hookData) ──

    function _devBuy(PoolKey memory key, address token, uint256 ethIn, address buyer) private {
        poolManager.unlock(abi.encode(key, token, ethIn, buyer));
    }

    /// Only the PoolManager, only mid-launch (transient flag), executes the ETH→token dev swap.
    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        require(msg.sender == address(poolManager), "not manager");
        (PoolKey memory key, address token, uint256 ethIn, address buyer) =
            abi.decode(data, (PoolKey, address, uint256, address));

        // zeroForOne (ETH→token) exact-in; recipient passed to the hook for recipient-keyed limits
        BalanceDelta delta = poolManager.swap(
            key,
            IPoolManager.SwapParams({ zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: 4295128740 }),
            abi.encode(buyer)
        );

        // settle the ETH we owe (native) and take the token out to the creator
        uint256 ethOwed = uint256(int256(-delta.amount0()));
        poolManager.settle{ value: ethOwed }();
        int128 tokenOut = delta.amount1();
        if (tokenOut > 0) poolManager.take(Currency.wrap(token), buyer, uint256(int256(tokenOut)));
        return "";
    }

    receive() external payable { } // dev-buy ETH refunds / native settle change

    /// Two single-sided token positions, both minted to 0xdEaD (permanent lock) in one batch:
    /// Position A (curve, 80%) over [GRADUATION_TICK, TICK_LAUNCH]; Position B (tail, 20%) over
    /// [TICK_TAIL, GRADUATION_TICK]. At the launch price (= sqrt at TICK_LAUNCH) both ranges sit at/
    /// below the current tick, so each is 100% token (currency1) — buys walk the tick down, selling A
    /// first (→ graduation at GRADUATION_TICK), then B.
    function _mintLockedPositions(PoolKey memory key, uint160 sqrtLaunch, address token) private {
        uint160 sqrtGrad = TickMath.getSqrtPriceAtTick(CurveConfig.GRADUATION_TICK);
        uint160 sqrtTail = TickMath.getSqrtPriceAtTick(CurveConfig.TICK_TAIL);

        uint128 liqA = LiquidityAmounts.getLiquidityForAmounts(
            sqrtLaunch, sqrtGrad, sqrtLaunch, 0, CURVE_SUPPLY // Position A range [grad, launch]
        );
        uint128 liqB = LiquidityAmounts.getLiquidityForAmounts(
            sqrtLaunch, sqrtTail, sqrtGrad, 0, TAIL_SUPPLY // Position B range [tail, grad]
        );

        // approve the token to be pulled by the PositionManager via Permit2
        IERC20Minimal(token).approve(address(permit2), type(uint256).max);
        permit2.approve(token, address(positionManager), type(uint160).max, type(uint48).max);

        bytes memory actions =
            abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR));
        bytes[] memory params = new bytes[](3);
        params[0] = abi.encode(
            key, CurveConfig.GRADUATION_TICK, CurveConfig.TICK_LAUNCH, liqA, uint256(0), CURVE_SUPPLY, DEAD, bytes("")
        );
        params[1] = abi.encode(
            key, CurveConfig.TICK_TAIL, CurveConfig.GRADUATION_TICK, liqB, uint256(0), TAIL_SUPPLY, DEAD, bytes("")
        );
        params[2] = abi.encode(key.currency0, key.currency1); // settle both; currency0(ETH)=0 owed
        positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp + 60);
    }

    // ── CREATE2 clones (EIP-1167) ──

    function _clone(bytes32 salt) private returns (address instance) {
        bytes memory code = _cloneCode();
        assembly {
            instance := create2(0, add(code, 0x20), mload(code), salt)
        }
        require(instance != address(0), "clone failed");
    }

    /// Predict the token address for (creator, userSalt) — the off-chain vanity miner target.
    function predictToken(address creator, bytes32 userSalt) external view returns (address) {
        bytes32 salt = keccak256(abi.encodePacked(creator, userSalt));
        bytes32 h = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(_cloneCode())));
        return address(uint160(uint256(h)));
    }

    function _cloneCode() private view returns (bytes memory) {
        return abi.encodePacked(
            hex"3d602d80600a3d3981f3363d3d373d3d3d363d73", tokenImplementation, hex"5af43d82803e903d91602b57fd5bf3"
        );
    }
}
Chain explorer4884msChain node101ms