InstantLauncher

0xec180fc74118dbb2789eba16ebdbfca869e02f90

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
24,525 bytes
ABI entries
36
16 read · 5 write
License
none

Contract information

Address
0xec180fc74118dbb2789eba16ebdbfca869e02f90
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x1ad923e8fe…3d41b47f57

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (16)

allLaunches(uint256)address
claimableVested(address)uint256
hook()address
launchCount()uint256
launches(address)uint64, address, uint8, tuple
owner()address
pendingOwner()address
permit2()address
poolKeyOf(address)tuple
poolManager()address
positionManager()address
predictToken(uint64, string, string, string, bytes32)address
registry()address
tokenInitCodeHash(uint64, string, string, string)bytes32
vestedAmount(address)uint256
vesting(address)address, uint128, uint128, uint40, uint32, uint32

Events (5)

DevReserveClaimedDevReserveVestedLaunchedOwnershipTransferStartedOwnershipTransferred

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      },
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "contract ConfigRegistry",
        "name": "registry_",
        "type": "address"
      },
      {
        "internalType": "contract LaunchpadHook",
        "name": "hook_",
        "type": "address"
      },
      {
        "internalType": "contract IPositionManager",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "contract IAllowanceTransfer",
        "name": "permit2_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "NoVesting",
    "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": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenOrderingFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownLaunch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "VanityMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "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": "DevReserveClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "amount",
        "type": "uint128"
      }
    ],
    "name": "DevReserveVested",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint64",
        "name": "configId",
        "type": "uint64"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "liquidity",
        "type": "uint128"
      },
      {
        "indexed": false,
        "internalType": "uint40",
        "name": "activation",
        "type": "uint40"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "tokenURI",
        "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": "OwnershipTransferStarted",
    "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"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "allLaunches",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "claimVested",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "claimableVested",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "contract LaunchpadHook",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64",
        "name": "configId",
        "type": "uint64"
      },
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "tokenURI",
        "type": "string"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "launches",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "configId",
        "type": "uint64"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "enum LaunchStatus",
        "name": "status",
        "type": "uint8"
      },
      {
        "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": "poolKey",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "permit2",
    "outputs": [
      {
        "internalType": "contract IAllowanceTransfer",
        "name": "",
        "type": "address"
      }
    ],
    "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": "positionManager",
    "outputs": [
      {
        "internalType": "contract IPositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64",
        "name": "configId",
        "type": "uint64"
      },
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "tokenURI",
        "type": "string"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "predictToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "registry",
    "outputs": [
      {
        "internalType": "contract ConfigRegistry",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64",
        "name": "configId",
        "type": "uint64"
      },
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "tokenURI",
        "type": "string"
      }
    ],
    "name": "tokenInitCodeHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "vestedAmount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "vesting",
    "outputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "uint128",
        "name": "total",
        "type": "uint128"
      },
      {
        "internalType": "uint128",
        "name": "claimed",
        "type": "uint128"
      },
      {
        "internalType": "uint40",
        "name": "start",
        "type": "uint40"
      },
      {
        "internalType": "uint32",
        "name": "cliff",
        "type": "uint32"
      },
      {
        "internalType": "uint32",
        "name": "duration",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
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 {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {IPositionManager} from "v4-periphery/interfaces/IPositionManager.sol";
import {Actions} from "v4-periphery/libraries/Actions.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";

import {LaunchConfig, DerivedConfig, LaunchStatus, FeeKind, Constants} from "./libraries/Types.sol";
import {LaunchMath} from "./libraries/LaunchMath.sol";
import {ConfigRegistry} from "./ConfigRegistry.sol";
import {LaunchToken} from "./LaunchToken.sol";
import {LaunchpadHook} from "./hooks/LaunchpadHook.sol";

/// @notice Launches a token straight onto Uniswap V4: deploys a fixed-supply token (mining the
/// address so the token is currency0), seeds the ENTIRE supply as a single-sided concentrated
/// position spanning the configured market-cap range, and permanently locks (burns) that
/// position — no code path can ever remove it. The token is tradable in the same transaction.
/// No quote/ETH is required from the creator: single-sided means the position is 100% token.
contract InstantLauncher is Ownable2Step, ReentrancyGuard {
    using PoolIdLibrary for PoolKey;
    using SafeERC20 for IERC20;

    /// Burn address the launch-LP position NFTs are minted to (permanently unremovable).
    address internal constant DEAD = 0x000000000000000000000000000000000000dEaD;

    error TokenOrderingFailed();
    error VanityMismatch();
    error UnknownLaunch();
    error NoVesting();
    error ZeroAddress();

    /// Every launch token address ends in these hex digits (Robinhood Chain id, 4663). The salt is
    /// mined off-chain (see tokenInitCodeHash / predictToken) so the launch tx just verifies it.
    uint160 internal constant VANITY_SUFFIX = 0x4663;
    uint160 internal constant VANITY_MASK = 0xffff;

    event Launched(
        address indexed token,
        uint64 indexed configId,
        address indexed creator,
        PoolId poolId,
        uint128 liquidity,
        uint40 activation,
        string name,
        string symbol,
        string tokenURI
    );
    event DevReserveVested(address indexed token, address indexed creator, uint128 amount);
    event DevReserveClaimed(address indexed token, address indexed creator, uint256 amount);

    struct LaunchRecord {
        uint64 configId;
        address creator;
        LaunchStatus status;
        PoolKey poolKey;
    }

    /// @notice Dev-reserve vesting held by the launcher: cliff then linear release to the creator.
    struct VestingSchedule {
        address creator;
        uint128 total;
        uint128 claimed;
        uint40 start;
        uint32 cliff;
        uint32 duration;
    }

    IPoolManager public immutable poolManager;
    ConfigRegistry public immutable registry;
    LaunchpadHook public immutable hook;
    /// V4 PositionManager: launch LP is minted as NFT position(s) to the burn address through it.
    IPositionManager public immutable positionManager;
    /// Canonical Permit2, used to authorize the PositionManager to pull seed tokens from the launcher.
    IAllowanceTransfer public immutable permit2;

    mapping(address token => LaunchRecord) public launches;
    mapping(address token => VestingSchedule) public vesting;
    address[] public allLaunches;

    constructor(
        address owner_,
        IPoolManager poolManager_,
        ConfigRegistry registry_,
        LaunchpadHook hook_,
        IPositionManager positionManager_,
        IAllowanceTransfer permit2_
    ) Ownable(owner_) {
        poolManager = poolManager_;
        registry = registry_;
        hook = hook_;
        positionManager = positionManager_;
        permit2 = permit2_;
    }

    function launchCount() external view returns (uint256) {
        return allLaunches.length;
    }

    function poolKeyOf(address token) external view returns (PoolKey memory) {
        if (launches[token].status == LaunchStatus.None) revert UnknownLaunch();
        return launches[token].poolKey;
    }

    // ---------------------------------------------------------------- dev-reserve vesting

    /// @notice Tokens vested (cliff + linear) for the dev reserve as of now.
    function vestedAmount(address token) public view returns (uint256) {
        VestingSchedule memory v = vesting[token];
        if (v.total == 0) return 0;
        uint256 unlockStart = uint256(v.start) + v.cliff;
        if (block.timestamp < unlockStart) return 0;
        if (v.duration == 0) return v.total; // fully unlocked at the cliff
        uint256 elapsed = block.timestamp - unlockStart;
        if (elapsed >= v.duration) return v.total;
        return (uint256(v.total) * elapsed) / v.duration;
    }

    /// @notice Currently-claimable (vested minus already-claimed) dev-reserve tokens.
    function claimableVested(address token) external view returns (uint256) {
        return vestedAmount(token) - vesting[token].claimed;
    }

    /// @notice Release vested dev-reserve tokens to the creator. Callable by anyone; always pays the
    /// recorded creator. Bypasses the launch-window limits (a vesting release is not a trader buy).
    function claimVested(address token) external nonReentrant returns (uint256 amount) {
        VestingSchedule storage v = vesting[token];
        if (v.total == 0) revert NoVesting();
        amount = vestedAmount(token) - v.claimed;
        if (amount == 0) return 0;
        v.claimed += uint128(amount);
        LaunchToken(payable(token)).setPrivilegedTransfer(true);
        IERC20(token).safeTransfer(v.creator, amount);
        LaunchToken(payable(token)).setPrivilegedTransfer(false);
        emit DevReserveClaimed(token, v.creator, amount);
    }

    // ---------------------------------------------------------------- launch

    function launch(
        uint64 configId,
        string calldata name,
        string calldata symbol,
        string calldata tokenURI,
        bytes32 salt
    ) external nonReentrant returns (address token, PoolId poolId) {
        LaunchConfig memory cfg = registry.getConfig(configId);
        DerivedConfig memory d = registry.getDerived(configId);
        uint40 activation = cfg.startTime == 0 ? uint40(block.timestamp) : cfg.startTime;

        token = _deployToken(cfg, name, symbol, tokenURI, salt);

        PoolKey memory key = PoolKey({
            currency0: Currency.wrap(token),
            currency1: Currency.wrap(cfg.quoteToken),
            fee: 0, // hook is the fee engine; the (burned) position earns no LP fee
            tickSpacing: Constants.TICK_SPACING,
            hooks: IHooks(address(hook))
        });
        // register with the hook BEFORE initialize (beforeInitialize rejects unregistered pools)
        _registerWithHook(key, cfg, token, msg.sender, activation, d.sqrtPriceTargetX96);

        poolManager.initialize(key, d.sqrtPriceStartX96);

        // seed the whole supply as two single-sided token positions (main band + forever tail), minted
        // as NFTs to the burn address via the V4 PositionManager — permanently burned LP that no owner
        // (the NFTs live at 0xdead) can ever remove.
        _seedBurnedLiquidity(key, d);

        // Hand the dev reserve to the creator (liquid or vested), then burn the seeding dust.
        // After seeding the launcher holds (totalSupply - baseDeposited) == devReserve + lpDust.
        uint256 held; // dev reserve retained here under vesting
        if (d.devReserve != 0) {
            if (cfg.devVestCliff == 0 && cfg.devVestDuration == 0) {
                // immediately liquid: privileged transfer bypasses the launch-window limits
                LaunchToken(payable(token)).setPrivilegedTransfer(true);
                IERC20(token).safeTransfer(msg.sender, d.devReserve);
                LaunchToken(payable(token)).setPrivilegedTransfer(false);
            } else {
                held = d.devReserve;
                vesting[token] = VestingSchedule({
                    creator: msg.sender,
                    total: d.devReserve,
                    claimed: 0,
                    start: activation,
                    cliff: cfg.devVestCliff,
                    duration: cfg.devVestDuration
                });
                emit DevReserveVested(token, msg.sender, d.devReserve);
            }
        }
        uint256 dust = IERC20(token).balanceOf(address(this)) - held;
        if (dust != 0) LaunchToken(payable(token)).burn(dust);

        poolId = key.toId(); // computed here (not earlier) to keep launch() under the via-ir stack limit
        launches[token] = LaunchRecord({configId: configId, creator: msg.sender, status: LaunchStatus.Live, poolKey: key});
        allLaunches.push(token);

        emit Launched(
            token,
            configId,
            msg.sender,
            poolId,
            d.liquidityMain + d.liquidityTail,
            activation,
            name,
            symbol,
            tokenURI
        );
    }

    /// @dev Isolated frame for the (large) hook registration, to keep `launch` off the Yul stack limit.
    function _registerWithHook(
        PoolKey memory key,
        LaunchConfig memory cfg,
        address token,
        address creator,
        uint40 activation,
        uint160 sqrtPriceTargetX96
    ) private {
        // For meme pools, precompute the Q96-price anchors once from the config's market-cap anchors
        // and fixed supply; the hook interpolates the fee in price so it never re-reads totalSupply.
        (uint256 priceStartX96, uint256 priceEndX96) = _memePriceAnchors(cfg);
        hook.register(
            key,
            LaunchpadHook.RegisterParams({
                token: token,
                creator: creator,
                partner: cfg.partner,
                creatorFeeShareBps: cfg.creatorFeeShareBps,
                activation: activation,
                sqrtPriceTargetX96: sqrtPriceTargetX96,
                schedule: LaunchpadHook.FeeSchedule({
                    kind: cfg.feeKind,
                    baseFeeBps: cfg.baseFeeBps,
                    antiSnipeMode: cfg.antiSnipeMode,
                    antiSnipeStartBps: cfg.antiSnipeStartBps,
                    antiSnipeDuration: cfg.antiSnipeDuration,
                    antiSnipePeriod: cfg.antiSnipePeriod,
                    antiSnipeDecayBps: cfg.antiSnipeDecayBps,
                    memeStartFeeBps: cfg.memeStartFeeBps,
                    memeEndFeeBps: cfg.memeEndFeeBps,
                    priceStartX96: priceStartX96,
                    priceEndX96: priceEndX96
                })
            })
        );
        // creator-fee routing (validated + defaulted in the registry) — separate call keeps this
        // frame small and avoids widening RegisterParams past the Yul stack limit.
        hook.registerFlywheel(key, cfg.flywheel);
    }

    /// @dev The meme fee's Q96-price anchors (zero for Standard configs, where they're unused).
    function _memePriceAnchors(LaunchConfig memory cfg) private pure returns (uint256, uint256) {
        if (cfg.feeKind != FeeKind.Meme) return (0, 0);
        return LaunchMath.memePriceAnchors(cfg.startMarketCap, cfg.memeEndMarketCap, cfg.totalSupply);
    }

    // ---------------------------------------------------------------- liquidity seeding (burned LP)

    /// @dev Seed both single-sided token positions as NFTs minted to `DEAD` via the V4 PositionManager.
    /// The positions are 100% currency0 (the token; ranges sit at/above the launch tick), so only
    /// currency0 is ever owed — the launcher pre-approves the PositionManager (through Permit2) to pull
    /// exactly the seed tokens from its freshly-minted supply. Because the NFTs are owned by the burn
    /// address, no one can ever decrease/withdraw the liquidity: the LP is provably burned, and explorers
    /// see the position NFTs held by 0xdead. Any `amount1Max` is 0 (no quote is ever pulled).
    function _seedBurnedLiquidity(PoolKey memory key, DerivedConfig memory d) private {
        address token = Currency.unwrap(key.currency0);
        IERC20(token).forceApprove(address(permit2), type(uint256).max);
        permit2.approve(token, address(positionManager), type(uint160).max, type(uint48).max);
        positionManager.modifyLiquidities(_seedCalldata(key, d), block.timestamp);
    }

    /// @dev Build the PositionManager calldata that mints both single-sided token positions (main band +
    /// forever tail) to the burn address and settles the token owed. Kept in its own (pure) frame so the
    /// deep abi.encode marshalling stays under the via-ir stack limit.
    function _seedCalldata(PoolKey memory key, DerivedConfig memory d) private pure returns (bytes memory) {
        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] = _mintToBurnParams(key, d.tickLower, d.tickMid, d.liquidityMain);
        params[1] = _mintToBurnParams(key, d.tickMid, d.tickUpper, d.liquidityTail);
        params[2] = abi.encode(key.currency0, key.currency1); // SETTLE_PAIR (only currency0 is ever owed)
        return abi.encode(actions, params);
    }

    /// @dev Encode one MINT_POSITION action minting a single-sided token position to the burn address.
    /// amount1Max = 0 (no quote is ever owed); amount0Max is uncapped — the launcher seeds its own fresh
    /// pool atomically, so there is no slippage/MEV surface to guard against.
    function _mintToBurnParams(PoolKey memory key, int24 tickLower, int24 tickUpper, uint128 liquidity)
        private
        pure
        returns (bytes memory)
    {
        return abi.encode(key, tickLower, tickUpper, uint256(liquidity), type(uint128).max, uint128(0), DEAD, bytes(""));
    }

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

    /// @notice CREATE2 init-code hash for a launch token, so off-chain salt miners (SDK/frontend)
    /// compute the exact same token address this contract will deploy. Mine a `salt` whose resulting
    /// address is < quoteToken AND ends in `VANITY_SUFFIX`, then pass it to `launch`.
    function tokenInitCodeHash(
        uint64 configId,
        string calldata name,
        string calldata symbol,
        string calldata tokenURI
    ) public view returns (bytes32) {
        LaunchConfig memory cfg = registry.getConfig(configId);
        return keccak256(abi.encodePacked(type(LaunchToken).creationCode, _tokenArgs(cfg, name, symbol, tokenURI)));
    }

    /// @notice The CREATE2 address for a given salt (mirror of what `launch` deploys).
    function predictToken(
        uint64 configId,
        string calldata name,
        string calldata symbol,
        string calldata tokenURI,
        bytes32 salt
    ) external view returns (address) {
        return _create2Address(salt, tokenInitCodeHash(configId, name, symbol, tokenURI));
    }

    function _tokenArgs(LaunchConfig memory cfg, string calldata name, string calldata symbol, string calldata tokenURI)
        private
        view
        returns (bytes memory)
    {
        // NOTE: constructor arity changed in Phase B — the trailing `weth` (== cfg.quoteToken, the
        // reward/quote currency) was appended. This changes the token init-code hash; the off-chain
        // SDK/frontend vanity miner mirror MUST be updated to match (mine over these exact args).
        return abi.encode(
            name,
            symbol,
            tokenURI,
            uint256(cfg.totalSupply),
            address(hook),
            address(registry),
            cfg.maxTxBaseAmount,
            cfg.maxWalletBaseAmount,
            cfg.startTime,
            cfg.limitsDuration,
            cfg.quoteToken
        );
    }

    function _deployToken(
        LaunchConfig memory cfg,
        string calldata name,
        string calldata symbol,
        string calldata tokenURI,
        bytes32 salt
    ) private returns (address token) {
        // Deploy via CREATE2 from the in-memory init code (creationCode ++ encoded constructor args).
        // This is the exact preimage `tokenInitCodeHash` hashes, so the deployed address matches
        // `predictToken`; it also keeps the 11 constructor args off the Yul stack (a `new` with named
        // args overflows it under via-ir). See _tokenArgs for the Phase-B arity change (trailing weth).
        bytes memory initCode =
            abi.encodePacked(type(LaunchToken).creationCode, _tokenArgs(cfg, name, symbol, tokenURI));
        assembly ("memory-safe") {
            token := create2(0, add(initCode, 0x20), mload(initCode), salt)
        }
        if (token == address(0)) revert TokenOrderingFailed(); // CREATE2 failed (salt collision)
        // the caller must have mined the salt off-chain: token sorts below quote (== currency0)
        // and the address ends in the Squeez vanity suffix
        if (token >= cfg.quoteToken) revert TokenOrderingFailed();
        if (uint160(token) & VANITY_MASK != VANITY_SUFFIX) revert VanityMismatch();
    }

    function _create2Address(bytes32 salt, bytes32 initCodeHash) private view returns (address) {
        return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash)))));
    }
}
Chain explorer3083msChain node88ms