FeatherFactory

0x106600e8fa3c688df23ab3e70a0c9c85c45927f6

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
11,028 bytes
ABI entries
39
22 read · 4 write
License
none

Contract information

Address
0x106600e8fa3c688df23ab3e70a0c9c85c45927f6
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xe5dc43e2e1…fec62a670e

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (22)

CURVE_SUPPLY()uint256
CURVE_TICK_WIDTH()int24
MIN_TICK()int24
RESERVE_SUPPLY()uint256
TICK_SPACING()int24
TOTAL_SUPPLY()uint256
bandPlan()uint128, uint256, uint128, uint256
gradTick()int24
hook()address
launchNonce(address)uint256
launchTick()int24
locker()address
owner()address
paused()bool
permit2()address
poolKeyFor(address)tuple
poolManager()address
positionManager()address
predictTokenAddress(address, string, uint256)address
predictVanityTokenAddress(bytes32)address
registry()address
tokenInitCodeHash()bytes32

Events (3)

LaunchedOwnershipTransferredPausedSet

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract FeatherRegistry",
        "name": "registry_",
        "type": "address"
      },
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "contract IPositionManager",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "contract IAllowanceTransfer",
        "name": "permit2_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "hook_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "locker_",
        "type": "address"
      },
      {
        "internalType": "int24",
        "name": "launchTick_",
        "type": "int24"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "InvalidLaunchTick",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LaunchesPaused",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NativeTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SaltNotOwned",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "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": "uint256",
        "name": "ethSpentOnBuy",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensBought",
        "type": "uint256"
      }
    ],
    "name": "Launched",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "name": "PausedSet",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "CURVE_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CURVE_TICK_WIDTH",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_TICK",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "RESERVE_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": "bandPlan",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "curveLiquidity",
        "type": "uint128"
      },
      {
        "internalType": "uint256",
        "name": "curveAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint128",
        "name": "reserveLiquidity",
        "type": "uint128"
      },
      {
        "internalType": "uint256",
        "name": "reserveTarget",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "gradTick",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "initialBuyMinOut",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "vanitySalt",
        "type": "bytes32"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "launchNonce",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchTick",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "locker",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "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": "poolKeyFor",
    "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": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "nonce",
        "type": "uint256"
      }
    ],
    "name": "predictTokenAddress",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "vanitySalt",
        "type": "bytes32"
      }
    ],
    "name": "predictVanityTokenAddress",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "registry",
    "outputs": [
      {
        "internalType": "contract FeatherRegistry",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "paused_",
        "type": "bool"
      }
    ],
    "name": "setPaused",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenInitCodeHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

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

import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {SqrtPriceMath} from "@uniswap/v4-core/src/libraries/SqrtPriceMath.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";

import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {LiquidityAmounts} from "@uniswap/v4-periphery/src/libraries/LiquidityAmounts.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";

import {FeatherToken} from "./FeatherToken.sol";
import {FeatherRegistry} from "./FeatherRegistry.sol";
import {FeatherFeeHook} from "./FeatherFeeHook.sol";
import {LpLocker} from "./LpLocker.sol";

/// @title FeatherFactory
/// @notice Single entry point for launching a feather token; a versioned
///         writer behind the permanent FeatherRegistry.
///
///         `launch()` performs the entire lifecycle in one transaction:
///           1. deploys the ERC-20 as a full contract via CREATE2 (no proxy;
///              salt is keccak(creator, symbol, nonce), or a caller-mined
///              vanity salt whose first 20 bytes must equal the caller),
///           2. mints the fixed 1B supply to itself,
///           3. initializes the canonical Uniswap v4 pool (token / native
///              ETH, dynamic-fee flag, FeatherFeeHook) at the launch tick,
///           4. registers the launch, emitting `TokenLaunched` from the
///              registry's permanent address,
///           5. mints the supply as two single-sided positions — the curve
///              band (79.31%, pump.fun's bonding curve tick-for-tick) and
///              the reserve band (20.69%, pump's post-graduation deposit) —
///              owned by the LpLocker from the moment they exist,
///           6. executes the creator's optional initial buy as an ordinary,
///              unprivileged swap that pays the standard hook fee.
///
///         Curve constants replicate pump.fun's dollar optics ($69,000
///         graduation market cap, x14.7 from launch), frozen in ETH terms at
///         the deploy-time ETH/USD rate. The launch tick is immutable deploy
///         config; band liquidity is derived on-chain from the fixed band
///         amounts so supply conservation is exact.
///
///         The pause switch affects new launches only. The factory retains
///         no authority over anything it has deployed.
///
/// @dev Native ETH is currency0 and v4 prices are quoted currency1/currency0
///      (token per ETH), so the token's ETH price rises as the tick falls.
///      The curve band spans [launchTick - 26880, launchTick]; graduation is
///      the tick reaching gradTick = launchTick - 26880. 26880 ticks =
///      ln(14.694)/ln(1.0001) rounded to tick spacing 60, pump's x14.69
///      price multiple.
contract FeatherFactory is IUnlockCallback {
    // ---------------------------------------------------------------------
    // Supply and curve shape (pump.fun replica)
    // ---------------------------------------------------------------------

    uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
    uint256 public constant CURVE_SUPPLY = 793_100_000e18; // 79.31%
    uint256 public constant RESERVE_SUPPLY = 206_900_000e18; // 20.69%

    /// @dev ln(14.694)/ln(1.0001) ~= 26877, rounded to tick spacing 60.
    int24 public constant CURVE_TICK_WIDTH = 26880;
    int24 public constant TICK_SPACING = 60;
    /// @dev TickMath.minUsableTick(60): the reserve band's lower bound.
    int24 public constant MIN_TICK = -887220;

    // ---------------------------------------------------------------------
    // Immutable deploy config
    // ---------------------------------------------------------------------

    FeatherRegistry public immutable registry;
    IPoolManager public immutable poolManager;
    IPositionManager public immutable positionManager;
    IAllowanceTransfer public immutable permit2;
    address public immutable hook;
    address public immutable locker;

    /// @notice Pool tick at launch, fixed at deploy from the ETH/USD anchor
    ///         so that launch market cap is ~$4,696 and graduation market
    ///         cap is ~$69,000 at that rate.
    int24 public immutable launchTick;

    /// @notice Tick at which the curve band is fully sold — the graduation
    ///         condition. The hook latches a permanent per-token `graduated`
    ///         flag at the first crossing; there is no migration or trading
    ///         pause, the pool simply opens to external liquidity.
    int24 public immutable gradTick;

    // ---------------------------------------------------------------------
    // State
    // ---------------------------------------------------------------------

    address public owner;
    bool public paused;

    /// @notice Per-creator launch nonce (CREATE2 salt component).
    mapping(address => uint256) public launchNonce;

    // ---------------------------------------------------------------------
    // Events / errors
    // ---------------------------------------------------------------------

    event Launched(
        address indexed token,
        bytes32 indexed poolId,
        address indexed creator,
        uint256 ethSpentOnBuy,
        uint256 tokensBought
    );
    event PausedSet(bool paused);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    error NotOwner();
    error LaunchesPaused();
    error SlippageExceeded();
    error NotPoolManager();
    error InvalidLaunchTick();
    error ZeroAddress();
    error NativeTransferFailed();
    error TokenTransferFailed();
    error SaltNotOwned();

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    constructor(
        FeatherRegistry registry_,
        IPoolManager poolManager_,
        IPositionManager positionManager_,
        IAllowanceTransfer permit2_,
        address hook_,
        address locker_,
        int24 launchTick_,
        address owner_
    ) {
        registry = registry_;
        poolManager = poolManager_;
        positionManager = positionManager_;
        permit2 = permit2_;
        hook = hook_;
        locker = locker_;
        owner = owner_;

        if (launchTick_ % TICK_SPACING != 0) revert InvalidLaunchTick();
        if (launchTick_ - CURVE_TICK_WIDTH <= MIN_TICK) revert InvalidLaunchTick();
        if (launchTick_ >= TickMath.maxUsableTick(TICK_SPACING)) revert InvalidLaunchTick();
        launchTick = launchTick_;
        gradTick = launchTick_ - CURVE_TICK_WIDTH;

        emit OwnershipTransferred(address(0), owner_);
    }

    // ---------------------------------------------------------------------
    // Launch
    // ---------------------------------------------------------------------

    /// @notice Launches a feather token. There is no launch fee: the full
    ///         `msg.value` funds the creator's initial buy through the pool,
    ///         subject to `initialBuyMinOut`; zero `msg.value` is a valid
    ///         free launch.
    /// @param vanitySalt Optional CREATE2 salt mined off-chain for a vanity
    ///        token address (see `predictVanityTokenAddress`). Its first 20
    ///        bytes must equal the caller's address, so a mined salt is
    ///        spendable only by the wallet it was mined for and cannot be
    ///        sniped if observed. `bytes32(0)` selects the default
    ///        nonce-derived salt.
    function launch(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        uint256 initialBuyMinOut,
        bytes32 vanitySalt
    ) external payable returns (address token, bytes32 poolId) {
        if (paused) revert LaunchesPaused();

        bytes32 salt;
        if (vanitySalt == bytes32(0)) {
            salt = keccak256(abi.encode(msg.sender, symbol, launchNonce[msg.sender]++));
        } else {
            // Truncation is intentional: a vanity salt's first 20 bytes
            // encode the launcher address by construction.
            // forge-lint: disable-next-line(unsafe-typecast)
            if (address(bytes20(vanitySalt)) != msg.sender) revert SaltNotOwned();
            salt = vanitySalt;
        }

        // Full CREATE2 token contract (no proxy); the fixed supply is minted
        // to the factory within the same transaction.
        token = address(new FeatherToken{salt: salt}());
        FeatherToken(token).initialize(name, symbol, address(this), TOTAL_SUPPLY);

        {
            // Canonical pool: token vs native ETH, dynamic-fee flag, hook.
            PoolKey memory key = poolKeyFor(token);
            poolManager.initialize(key, TickMath.getSqrtPriceAtTick(launchTick));
            poolId = PoolId.unwrap(key.toId());

            // Registration precedes the band mints: the locker verifies the
            // positions against registry.canonicalPool(token), and
            // TokenLaunched then precedes every other event of the launch.
            registry.register(token, poolId, msg.sender, name, symbol, metadataURI);

            // Curve + reserve bands, owned by the LpLocker from the mint.
            _mintLockedBands(key, token);
        }

        // Creator initial buy: same pool, same hook fee as any other trader.
        uint256 tokensBought = _initialBuy(poolKeyFor(token), msg.value, initialBuyMinOut);

        emit Launched(token, poolId, msg.sender, msg.value, tokensBought);
    }

    function _initialBuy(PoolKey memory key, uint256 buyEth, uint256 minOut) internal returns (uint256 tokensBought) {
        if (buyEth == 0) {
            if (minOut > 0) revert SlippageExceeded();
            return 0;
        }
        bytes memory result = poolManager.unlock(abi.encode(key, buyEth, minOut, msg.sender));
        uint256 ethSpent;
        (tokensBought, ethSpent) = abi.decode(result, (uint256, uint256));
        // Refund any ETH the swap did not consume so nothing is stranded in
        // the factory. Only reachable for zero-fee dust fills: the hook's
        // fill guard reverts any fee-bearing partial fill.
        if (ethSpent < buyEth) {
            (bool ok,) = msg.sender.call{value: buyEth - ethSpent}("");
            if (!ok) revert NativeTransferFailed();
        }
    }

    /// @dev Initial-buy swap, executed inside the PoolManager unlock — an
    ///      ordinary unprivileged swap that walks the curve band and pays
    ///      the hook fee like any later buyer.
    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        (PoolKey memory key, uint256 ethIn, uint256 minOut, address recipient) =
            abi.decode(data, (PoolKey, uint256, uint256, address));

        BalanceDelta delta = poolManager.swap(
            key,
            SwapParams({
                // Casting is safe: ethIn is bounded by msg.value, far below
                // type(int256).max.
                // forge-lint: disable-next-line(unsafe-typecast)
                zeroForOne: true,
                // forge-lint: disable-next-line(unsafe-typecast)
                amountSpecified: -int256(ethIn),
                sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
            }),
            ""
        );

        uint256 ethOwed = uint256(uint128(uint256(-int256(delta.amount0()))));
        poolManager.settle{value: ethOwed}();

        uint256 tokensOut = uint256(uint128(uint256(int256(delta.amount1()))));
        if (tokensOut < minOut) revert SlippageExceeded();
        poolManager.take(key.currency1, recipient, tokensOut);

        return abi.encode(tokensOut, ethOwed);
    }

    // ---------------------------------------------------------------------
    // Views
    // ---------------------------------------------------------------------

    /// @notice The canonical PoolKey for a feather token.
    function poolKeyFor(address token) public view returns (PoolKey memory) {
        return PoolKey({
            currency0: CurrencyLibrary.ADDRESS_ZERO,
            currency1: Currency.wrap(token),
            fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
            tickSpacing: TICK_SPACING,
            hooks: IHooks(hook)
        });
    }

    /// @notice keccak256 of FeatherToken's creation code — the CREATE2
    ///         init-code hash used by off-chain vanity miners and address
    ///         verifiers. Constant, as FeatherToken takes no constructor
    ///         arguments.
    function tokenInitCodeHash() public pure returns (bytes32) {
        return keccak256(type(FeatherToken).creationCode);
    }

    /// @notice Predicts the token address of a default (nonce-salted)
    ///         launch, so anyone can verify a token was factory-made from
    ///         its address alone.
    function predictTokenAddress(address creator, string calldata symbol, uint256 nonce)
        external
        view
        returns (address)
    {
        return _create2Address(keccak256(abi.encode(creator, symbol, nonce)));
    }

    /// @notice Predicts the token address of a vanity-salted launch. The
    ///         salt's first 20 bytes encode the launcher (enforced in
    ///         `launch`), so no separate creator input is needed.
    function predictVanityTokenAddress(bytes32 vanitySalt) external view returns (address) {
        return _create2Address(vanitySalt);
    }

    function _create2Address(bytes32 salt) internal view returns (address) {
        return
            address(
                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, tokenInitCodeHash()))))
            );
    }

    /// @notice Band liquidities as the factory will mint them.
    function bandPlan()
        public
        view
        returns (uint128 curveLiquidity, uint256 curveAmount, uint128 reserveLiquidity, uint256 reserveTarget)
    {
        uint160 sqrtLaunch = TickMath.getSqrtPriceAtTick(launchTick);
        uint160 sqrtGrad = TickMath.getSqrtPriceAtTick(gradTick);
        uint160 sqrtMin = TickMath.getSqrtPriceAtTick(MIN_TICK);

        curveLiquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtGrad, sqrtLaunch, CURVE_SUPPLY);
        curveAmount = SqrtPriceMath.getAmount1Delta(sqrtGrad, sqrtLaunch, curveLiquidity, true);
        // The reserve band absorbs the curve band's tick-rounding remainder,
        // so the two bands always sum to the full supply; sub-token residue
        // is locked in the LpLocker.
        reserveTarget = TOTAL_SUPPLY - curveAmount;
        reserveLiquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtMin, sqrtGrad, reserveTarget);
    }

    // ---------------------------------------------------------------------
    // Owner ops — new launches only
    // ---------------------------------------------------------------------

    function setPaused(bool paused_) external onlyOwner {
        paused = paused_;
        emit PausedSet(paused_);
    }

    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert ZeroAddress();
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }

    // ---------------------------------------------------------------------
    // Internals
    // ---------------------------------------------------------------------

    function _mintLockedBands(PoolKey memory key, address token) internal {
        (uint128 curveLiquidity,, uint128 reserveLiquidity,) = bandPlan();

        // The two MINT_POSITIONs below take the next two sequential ids.
        uint256 curvePositionId = positionManager.nextTokenId();

        // The PositionManager settles ERC-20s through Permit2.
        FeatherToken(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.CLOSE_CURRENCY));
        bytes[] memory params = new bytes[](3);
        // Curve band [gradTick, launchTick]: single-sided in the token at
        // the launch price — pump's bonding curve tick-for-tick.
        params[0] =
            abi.encode(key, gradTick, launchTick, uint256(curveLiquidity), 0, type(uint128).max, locker, bytes(""));
        // Reserve band [MIN_TICK, gradTick]: activates when the price
        // crosses graduation, mirroring pump's post-graduation deposit.
        params[1] =
            abi.encode(key, MIN_TICK, gradTick, uint256(reserveLiquidity), 0, type(uint128).max, locker, bytes(""));
        params[2] = abi.encode(key.currency1);

        // The hook rejects every pre-graduation liquidity addition except
        // these two mints: this pool's gate is open only between the calls
        // below, inside the atomic launch.
        FeatherFeeHook(payable(hook)).openLiquidityGate(token);
        positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp);
        FeatherFeeHook(payable(hook)).closeLiquidityGate(token);

        // Record the positions so the locker's permissionless fees-only
        // sweep (`collectFees`) can find them.
        LpLocker(locker).registerPositions(token, curvePositionId, curvePositionId + 1);

        // Sub-token dust from liquidity rounding is locked with the LP.
        uint256 dust = FeatherToken(token).balanceOf(address(this));
        if (dust > 0 && !FeatherToken(token).transfer(locker, dust)) revert TokenTransferFailed();
    }
}
Chain explorer3680msChain node88ms