LaunchFactoryV4R2

0x22b5c08869a5a6d12b4e5891a7e78d04817b4e21

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
12,139 bytes
ABI entries
42
27 read · 4 write
License
none

Contract information

Address
0x22b5c08869a5a6d12b4e5891a7e78d04817b4e21
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xe80cf56d89…37ce233fa0

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (27)

FACTORY_VERSION()uint256
FULL_TICK_LOWER()int24
FULL_TICK_UPPER()int24
LP_FEE()uint24
POST_GRAD_CREATOR_BPS()uint16
TICK_LOWER()int24
TICK_SPACING()int24
TICK_UPPER()int24
TOKEN_METADATA_ONCHAIN()bool
TOTAL_SUPPLY()uint256
computeTokenAddress(bytes32)address
graduationEth()uint256
hook()address
isLaunchToken(address)bool
isOgTicker(address)bool
isPool(address)bool
isSymbolTaken(string)bool
launchFee()uint256
launches(uint256)address, address, bytes32, uint256
launchesCount()uint256
locker()address
minDevBuy()uint256
poolKeyOf(address)tuple
poolManager()address
protocolTreasury()address
symbolToToken(string)address
tokenImpl()address

Events (2)

GraduatedLaunched

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "hook_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "locker_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "protocolTreasury_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "launchFee_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minDevBuy_",
        "type": "uint256"
      },
      {
        "internalType": "int24",
        "name": "tickLower_",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "tickUpper_",
        "type": "int24"
      },
      {
        "internalType": "uint256",
        "name": "graduationEth_",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadSymbol",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedDeployment",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeTransferFailed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientValue",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotGraduatable",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethToLp",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensToLp",
        "type": "uint256"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "devBuyEth",
        "type": "uint256"
      }
    ],
    "name": "Launched",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "FACTORY_VERSION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FULL_TICK_LOWER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FULL_TICK_UPPER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LP_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POST_GRAD_CREATOR_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_LOWER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_UPPER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOKEN_METADATA_ONCHAIN",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "computeTokenAddress",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "graduate",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethToLp",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokensToLp",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "graduationEth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "contract FoundryHookV4R2",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isLaunchToken",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "isOgTicker",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isPool",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      }
    ],
    "name": "isSymbolTaken",
    "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"
      },
      {
        "components": [
          {
            "internalType": "string",
            "name": "logo",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "description",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "website",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "twitter",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "telegram",
            "type": "string"
          }
        ],
        "internalType": "struct LaunchpadTokenV4.TokenMeta",
        "name": "meta",
        "type": "tuple"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "components": [
          {
            "internalType": "string",
            "name": "logo",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "description",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "website",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "twitter",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "telegram",
            "type": "string"
          }
        ],
        "internalType": "struct LaunchpadTokenV4.TokenMeta",
        "name": "meta",
        "type": "tuple"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "address[]",
        "name": "payoutWallets",
        "type": "address[]"
      },
      {
        "internalType": "uint16[]",
        "name": "payoutBps",
        "type": "uint16[]"
      }
    ],
    "name": "launchWithPayout",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "launches",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "launchBlock",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchesCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "locker",
    "outputs": [
      {
        "internalType": "contract FeeLockerV4R2",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minDevBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "poolKeyOf",
    "outputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolTreasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      }
    ],
    "name": "symbolToToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenImpl",
    "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"
  }
]

Source code

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
import {SwapParams} from "v4-core/src/types/PoolOperation.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {CurrencySettler} from "v4-core/test/utils/CurrencySettler.sol";
import {LiquidityAmounts} from "v4-core/test/utils/LiquidityAmounts.sol";

import {LaunchpadTokenV4} from "../v4/LaunchpadTokenV4.sol";
import {FoundryHookV4R2} from "./FoundryHookV4R2.sol";
import {FeeLockerV4R2} from "./FeeLockerV4R2.sol";

/// @title LaunchFactoryV4R2
/// @notice Revision 2 of the Uniswap-V4 launch factory. The launch flow (clone token → register hook →
/// initialize pool → lock full supply forever → route launch fee → mandatory dev-buy first swap) is
/// IDENTICAL to LaunchFactoryV4. What changed is where the fee-payout config lives: the optional
/// single-signature creator-fee split now goes to the LOCKER (`FeeLockerV4R2.setInitialPayout`) — the
/// contract where fee money actually moves — instead of the V4 hook's parallel (money-less) config.
/// The token implementation (`LaunchpadTokenV4`, verified) is reused unchanged: clones are born
/// verified on Blockscout, which is what keeps first-seconds scanner verdicts clean.
contract LaunchFactoryV4R2 is IUnlockCallback {
    using SafeERC20 for IERC20;
    using PoolIdLibrary for PoolKey;
    using CurrencyLibrary for Currency;
    using CurrencySettler for Currency;
    using BalanceDeltaLibrary for BalanceDelta;

    /// @dev Launchpad GENERATION counter (V1-V3 curve era, V4 = first Uniswap-V4 stack, 5 = this one).
    /// The app resolves coins per-generation off this. The contract NAMES stay V4-based because the
    /// underlying AMM is (and remains) Uniswap V4.
    uint256 public constant FACTORY_VERSION = 5;
    bool public constant TOKEN_METADATA_ONCHAIN = true;
    uint256 public constant TOTAL_SUPPLY = 1_000_000_000 ether;
    uint16 public constant POST_GRAD_CREATOR_BPS = 5000; // locker FALLBACK split (epoch split preferred)

    uint24 public constant LP_FEE = 0x800000; // DYNAMIC_FEE_FLAG
    int24 public constant TICK_SPACING = 60;
    int24 public constant FULL_TICK_LOWER = -887220;
    int24 public constant FULL_TICK_UPPER = 887220;

    int24 public immutable TICK_LOWER;
    int24 public immutable TICK_UPPER;
    uint256 public immutable graduationEth;

    IPoolManager public immutable poolManager;
    FoundryHookV4R2 public immutable hook;
    FeeLockerV4R2 public immutable locker;
    address public immutable protocolTreasury;
    address public immutable tokenImpl;
    uint256 public immutable launchFee;
    uint256 public immutable minDevBuy;

    struct Launch {
        address token;
        address creator;
        PoolId poolId;
        uint256 launchBlock;
    }

    Launch[] public launches;
    mapping(address => bool) public isPool;
    mapping(address => bool) public isLaunchToken;
    mapping(bytes32 => address) internal _ogTokenByKey;

    event Launched(
        address indexed token, address indexed creator, PoolId indexed poolId, string metadataURI, uint256 devBuyEth
    );
    event Graduated(PoolId indexed poolId, address indexed token, uint256 ethToLp, uint256 tokensToLp);

    error InsufficientValue();
    error FeeTransferFailed();
    error BadSymbol();
    error NotPoolManager();
    error NotGraduatable();

    /// @dev Locker<->factory circularity resolved by deterministic address prediction, exactly like
    /// V4/V3: locker deployed one nonce before this factory with the PREDICTED factory address.
    constructor(
        address poolManager_,
        address hook_,
        address locker_,
        address protocolTreasury_,
        uint256 launchFee_,
        uint256 minDevBuy_,
        int24 tickLower_,
        int24 tickUpper_,
        uint256 graduationEth_
    ) {
        require(tickLower_ < tickUpper_, "bad ticks");
        require(tickLower_ % TICK_SPACING == 0 && tickUpper_ % TICK_SPACING == 0, "unaligned ticks");
        require(graduationEth_ > 0, "bad graduationEth");
        poolManager = IPoolManager(poolManager_);
        hook = FoundryHookV4R2(hook_);
        locker = FeeLockerV4R2(payable(locker_));
        protocolTreasury = protocolTreasury_;
        launchFee = launchFee_;
        minDevBuy = minDevBuy_;
        TICK_LOWER = tickLower_;
        TICK_UPPER = tickUpper_;
        graduationEth = graduationEth_;
        tokenImpl = address(new LaunchpadTokenV4());
    }

    /// @notice Launch with the default payout (100% to the creator). Same selector shape as V4 so the
    /// app's launch encoder ports with only an address change.
    function launch(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        LaunchpadTokenV4.TokenMeta calldata meta,
        bytes32 salt
    ) external payable returns (address token, PoolId poolId) {
        return _launchCore(name, symbol, metadataURI, meta, salt, new address[](0), new uint16[](0));
    }

    /// @notice Single-signature launch WITH a creator-fee split applied in the SAME tx — now recorded in
    /// the LOCKER (one-shot, factory-only, full validation + floor-ratchet seeding there).
    function launchWithPayout(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        LaunchpadTokenV4.TokenMeta calldata meta,
        bytes32 salt,
        address[] calldata payoutWallets,
        uint16[] calldata payoutBps
    ) external payable returns (address token, PoolId poolId) {
        return _launchCore(name, symbol, metadataURI, meta, salt, payoutWallets, payoutBps);
    }

    function _launchCore(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        LaunchpadTokenV4.TokenMeta calldata meta,
        bytes32 salt,
        address[] memory payoutWallets,
        uint16[] memory payoutBps
    ) internal returns (address token, PoolId poolId) {
        if (msg.value < launchFee + minDevBuy) revert InsufficientValue();

        bytes32 sk = _symbolKey(symbol);
        token = _cloneAndInit(name, symbol, metadataURI, meta, salt);

        PoolKey memory key = _poolKey(token);
        poolId = key.toId();

        uint128 liquidity = LiquidityAmounts.getLiquidityForAmount1(
            TickMath.getSqrtPriceAtTick(TICK_LOWER), TickMath.getSqrtPriceAtTick(TICK_UPPER), TOTAL_SUPPLY
        );

        // Register geometry with the hook BEFORE initializing the pool (unregistered pools are inert).
        hook.registerPool(key, msg.sender, TICK_LOWER, TICK_UPPER, liquidity, graduationEth);

        poolManager.initialize(key, TickMath.getSqrtPriceAtTick(TICK_UPPER));

        // Lock the full-supply single-sided position forever, then apply the OPTIONAL one-shot launch
        // fee split IN THE LOCKER (the V4R2 single-source-of-truth change).
        IERC20(token).safeTransfer(address(locker), TOTAL_SUPPLY);
        locker.lockLiquidity(
            key, TICK_LOWER, TICK_UPPER, liquidity, token, msg.sender, protocolTreasury, POST_GRAD_CREATOR_BPS
        );
        if (payoutWallets.length > 0) {
            locker.setInitialPayout(token, payoutWallets, payoutBps);
        }

        isPool[token] = true;
        isLaunchToken[token] = true;
        if (_ogTokenByKey[sk] == address(0)) _ogTokenByKey[sk] = token;
        launches.push(Launch({token: token, creator: msg.sender, poolId: poolId, launchBlock: block.number}));

        if (launchFee > 0) {
            (bool ok,) = protocolTreasury.call{value: launchFee}("");
            if (!ok) revert FeeTransferFailed();
        }

        uint256 devBuyEth = msg.value - launchFee;
        poolManager.unlock(abi.encode(key, msg.sender, devBuyEth));

        emit Launched(token, msg.sender, poolId, metadataURI, devBuyEth);
    }

    /// @notice PoolManager unlock callback: executes the mandatory dev buy and forwards the bought
    /// tokens to the creator.
    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        (PoolKey memory key, address creator, uint256 devBuyEth) = abi.decode(data, (PoolKey, address, uint256));

        BalanceDelta delta = poolManager.swap(
            key,
            SwapParams({
                zeroForOne: true,
                amountSpecified: -int256(devBuyEth),
                sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
            }),
            ""
        );

        if (delta.amount0() < 0) {
            key.currency0.settle(poolManager, address(this), uint256(uint128(-delta.amount0())), false);
        }
        if (delta.amount1() > 0) {
            poolManager.take(key.currency1, creator, uint256(uint128(delta.amount1())));
        }
        return "";
    }

    function _cloneAndInit(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        LaunchpadTokenV4.TokenMeta calldata meta,
        bytes32 salt
    ) internal returns (address token) {
        token = Clones.cloneDeterministic(tokenImpl, salt);
        LaunchpadTokenV4(token).initialize(name, symbol, address(this), meta, metadataURI);
    }

    // ─────────────────────────── graduation ───────────────────────────

    /// @notice Permissionless once the accumulated-ETH threshold is met. Identical to V4.
    function graduate(address token) external returns (uint256 ethToLp, uint256 tokensToLp) {
        if (!isLaunchToken[token]) revert NotGraduatable();
        PoolKey memory key = _poolKey(token);
        PoolId poolId = key.toId();
        if (!hook.graduationReady(poolId)) revert NotGraduatable();

        hook.markGraduated(poolId);
        (ethToLp, tokensToLp) = locker.graduateRerange(token, FULL_TICK_LOWER, FULL_TICK_UPPER);

        emit Graduated(poolId, token, ethToLp, tokensToLp);
    }

    // ─────────────────────────── views / helpers ───────────────────────────

    function _poolKey(address token) internal view returns (PoolKey memory) {
        return PoolKey({
            currency0: CurrencyLibrary.ADDRESS_ZERO,
            currency1: Currency.wrap(token),
            fee: LP_FEE,
            tickSpacing: TICK_SPACING,
            hooks: IHooks(address(hook))
        });
    }

    function poolKeyOf(address token) external view returns (PoolKey memory) {
        return _poolKey(token);
    }

    function computeTokenAddress(bytes32 salt) public view returns (address) {
        return Clones.predictDeterministicAddress(tokenImpl, salt, address(this));
    }

    function launchesCount() external view returns (uint256) {
        return launches.length;
    }

    function symbolToToken(string calldata symbol) external view returns (address) {
        return _ogTokenByKey[_symbolKey(symbol)];
    }

    function isOgTicker(address token) external view returns (bool) {
        if (!isLaunchToken[token]) return false;
        return _ogTokenByKey[_symbolKey(LaunchpadTokenV4(token).symbol())] == token;
    }

    function isSymbolTaken(string calldata symbol) external view returns (bool) {
        return _ogTokenByKey[_symbolKey(symbol)] != address(0);
    }

    function _symbolKey(string memory symbol) internal pure returns (bytes32) {
        bytes memory b = bytes(symbol);
        uint256 len = b.length;
        if (len == 0 || len > 32) revert BadSymbol();
        for (uint256 i = 0; i < len; i++) {
            if (b[i] >= 0x61 && b[i] <= 0x7a) b[i] = bytes1(uint8(b[i]) - 32);
        }
        return keccak256(b);
    }
}
Chain explorer1800msChain node83ms