QianFactoryV4

0x750d0bc66baa12cf1710a8ba939b97db4ad005c3

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
18,167 bytes
ABI entries
57
20 read · 11 write
License
mit

Contract information

Address
0x750d0bc66baa12cf1710a8ba939b97db4ad005c3
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0xea72f4855e…d140a388e9

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (20)

DEV_BUY_FEE_BPS()uint16
MAX_POOL_FEE()uint24
MIN_POOL_FEE()uint24
approvedHooks(address)bool
deployCoinEnabled()bool
deployFee()uint256
feeVault()address
getTokenInfo(address)string, string, address, uint256, string, address
guardHook()address
liquidityConfigCount()uint256
liquidityConfigs(uint256)uint160, uint160, int24, int24, uint256, uint256
locker()address
pendingController()address
platformController()address
poolManager()address
tokenConfigs(address)uint256, string, address
tokenCount()uint256
tokenCreator(address)address
tokenToPoolKey(address)address, address, uint24, int24, address
weth()address

Events (10)

ControllerTransferInitiatedControllerTransferredCreatorChangedDeployCoinToggledDeployFeeUpdatedERC20TokenCreatedGuardHookUpdatedHookApprovalChangedHookConfiguredInitialBuyExecuted

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_poolManager",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_weth",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_locker",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadRouterConfig",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DeployDisabled",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DeployFeeTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "HookNotApproved",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientDeployFee",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "required",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "available",
        "type": "uint256"
      }
    ],
    "name": "InsufficientTokenBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidConfig",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidFeeVault",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidPoolFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidTickRange",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotController",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Unauthorized",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "currentController",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pendingController",
        "type": "address"
      }
    ],
    "name": "ControllerTransferInitiated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldController",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newController",
        "type": "address"
      }
    ],
    "name": "ControllerTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldCreator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newCreator",
        "type": "address"
      }
    ],
    "name": "CreatorChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      }
    ],
    "name": "DeployCoinToggled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldFee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newFee",
        "type": "uint256"
      }
    ],
    "name": "DeployFeeUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "tokenAddress",
        "type": "address"
      }
    ],
    "name": "ERC20TokenCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "hook",
        "type": "address"
      }
    ],
    "name": "GuardHookUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "hook",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "HookApprovalChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "hook",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "HookConfigured",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensReceived",
        "type": "uint256"
      }
    ],
    "name": "InitialBuyExecuted",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "DEV_BUY_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_POOL_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_POOL_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptController",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "approvedHooks",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenAddress",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "newCreator",
        "type": "address"
      }
    ],
    "name": "changeTokenFeeReceiver",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "_name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "_symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "_metadata",
        "type": "string"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "configId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "hookAddress",
        "type": "address"
      },
      {
        "internalType": "bytes",
        "name": "hookConfig",
        "type": "bytes"
      },
      {
        "internalType": "uint256",
        "name": "maxDeployFee",
        "type": "uint256"
      },
      {
        "internalType": "uint24",
        "name": "poolFee",
        "type": "uint24"
      }
    ],
    "name": "deployCoin",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensReceived",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "deployCoinEnabled",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "deployFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeVault",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "getTokenInfo",
    "outputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deployTime",
        "type": "uint256"
      },
      {
        "internalType": "string",
        "name": "metadata",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "hookAddress",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "guardHook",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "liquidityConfigCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "liquidityConfigs",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "sqrtPriceX96A",
        "type": "uint160"
      },
      {
        "internalType": "uint160",
        "name": "sqrtPriceX96B",
        "type": "uint160"
      },
      {
        "internalType": "int24",
        "name": "tickLower",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "tickUpper",
        "type": "int24"
      },
      {
        "internalType": "uint256",
        "name": "amount0Desired",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount1Desired",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "locker",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingController",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "platformController",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_deployFee",
        "type": "uint256"
      }
    ],
    "name": "setDeployFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_feeVault",
        "type": "address"
      }
    ],
    "name": "setFeeVault",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "hook",
        "type": "address"
      }
    ],
    "name": "setGuardHook",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "hook",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "setHookApproved",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      },
      {
        "internalType": "uint160",
        "name": "sqrtA",
        "type": "uint160"
      },
      {
        "internalType": "uint160",
        "name": "sqrtB",
        "type": "uint160"
      },
      {
        "internalType": "int24",
        "name": "tickL",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "tickU",
        "type": "int24"
      },
      {
        "internalType": "uint256",
        "name": "amt0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amt1",
        "type": "uint256"
      }
    ],
    "name": "setLiquidityConfig",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "toggleDeployCoin",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "tokenConfigs",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "deployTime",
        "type": "uint256"
      },
      {
        "internalType": "string",
        "name": "metadata",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "hookAddress",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "tokenCreator",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "tokenToPoolKey",
    "outputs": [
      {
        "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"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newController",
        "type": "address"
      }
    ],
    "name": "transferController",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// ~ https://Qian.ag ~ 
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./v4/LiquidityProviderV4.sol";
import "./QianToken.sol";
import "./interfaces/IPoolManager.sol";
import "./interfaces/ICurrency.sol";
import "./interfaces/IHooks.sol";
import "./interfaces/IConfigurableHook.sol";
import "./interfaces/IWETH.sol";
import "./interfaces/IUnlockCallback.sol";
import "./errors/Errors.sol";
import "./QianLocker.sol";

contract QianFactoryV4 is ReentrancyGuard, IUnlockCallback {
    using SafeERC20 for IERC20;
    using LiquidityProviderV4 for LiquidityProviderV4.Config;
    using CurrencyLibrary for Currency;

    event ERC20TokenCreated(address tokenAddress);
    event DeployFeeUpdated(uint256 oldFee, uint256 newFee);
    event HookApprovalChanged(address indexed hook, bool approved);
    event HookConfigured(address indexed token, address indexed hook, bytes32 poolId);
    event InitialBuyExecuted(address indexed token, address indexed buyer, uint256 ethAmount, uint256 tokensReceived);
    event ControllerTransferInitiated(address indexed currentController, address indexed pendingController);
    event ControllerTransferred(address indexed oldController, address indexed newController);
    event GuardHookUpdated(address indexed hook);
    event DeployCoinToggled(bool enabled);
    event CreatorChanged(address indexed token, address indexed oldCreator, address indexed newCreator);

    struct TokenConfig {
        uint256 deployTime;
        string metadata;
        address hookAddress;
    }

    struct LiquidityConfig {
        uint160 sqrtPriceX96A;
        uint160 sqrtPriceX96B;
        int24 tickLower;
        int24 tickUpper;
        uint256 amount0Desired;
        uint256 amount1Desired;
    }

    mapping(address => TokenConfig) public tokenConfigs;
    /// @notice Indexer read surface (position/ticks live in the locker's binding).
    mapping(address => PoolKey) public tokenToPoolKey;
    uint256 public tokenCount = 0;
    address public platformController;
    address public pendingController;
    uint256 public deployFee = 0.005 ether;
    address public feeVault;
    mapping(uint256 => LiquidityConfig) public liquidityConfigs;
    uint256 public liquidityConfigCount = 0;
    mapping(address => bool) public approvedHooks;
    /// @notice Hook used when a launch requests none. hookAddress=0 resolves to this,
    /// never to address(0): an ungated pool can be initialized by anyone.
    address public guardHook;
    bool public deployCoinEnabled = true;

    address public immutable poolManager;
    address public immutable weth;
    address public immutable locker;
    int24 private constant TICK_SPACING = 60;
    uint256 private constant FULL_TOKEN_SUPPLY = 1_000_000_000 * 10 ** 18;
    // dev buy pays the same 1% QianSwapRouter charges on every other trade
    uint16 public constant DEV_BUY_FEE_BPS = 100;
    uint24 public constant MIN_POOL_FEE = 5000;
    uint24 public constant MAX_POOL_FEE = 200000;

    constructor(address _poolManager, address _weth, address _locker) {
        if (_poolManager == address(0) || _weth == address(0) || _locker == address(0)) {
            revert BadRouterConfig();
        }
        platformController = msg.sender;
        feeVault = msg.sender;
        poolManager = _poolManager;
        weth = _weth;
        locker = _locker;
        liquidityConfigs[0] = LiquidityConfig({
            sqrtPriceX96A: 1_761_835_841_251_780_000_000_000,
            sqrtPriceX96B: 3_562_818_730_561_648_429_908_957_438_177_799, // 2^192/sqrtPriceX96A — exact mirror for flipped token ordering
            tickLower: -207300,
            tickUpper: 887220,
            amount0Desired: FULL_TOKEN_SUPPLY,
            amount1Desired: 0
        });
        liquidityConfigCount = 1;
    }



    function deployCoin(
        string memory _name,
        string memory _symbol,
        string memory _metadata,
        bytes32 salt,
        uint256 configId,
        address hookAddress,
        bytes calldata hookConfig,
        uint256 maxDeployFee,
        uint24 poolFee
    ) public payable nonReentrant returns (uint256 tokensReceived) {
        if (!deployCoinEnabled) revert DeployDisabled();
        if (configId >= liquidityConfigCount) revert InvalidConfig();
        if (deployFee > maxDeployFee) revert DeployFeeTooHigh();
        // The pool fee is the creator's tax: it accrues to the locker's LP position and
        // pays out via QianFeeRouter's 70/30. Mandatory and explicit — no default
        // sentinel, since the fee lands in the immutable PoolKey.
        if (poolFee < MIN_POOL_FEE || poolFee > MAX_POOL_FEE) revert InvalidPoolFee();
        if (msg.value < deployFee) revert InsufficientDeployFee();
        // No hook requested resolves to the guard hook. An unset guardHook fails closed.
        address hook = hookAddress == address(0) ? guardHook : hookAddress;
        if (!approvedHooks[hook]) revert HookNotApproved();
        {
            uint256 size;
            assembly { size := extcodesize(hook) }
            if (size == 0) revert InvalidConfig();
        }
        // dev buy pays the same 1% QianSwapRouter takes on every other trade; it ships
        // with the deploy fee as a single transfer rather than a separate hop
        uint256 devBuyValue = msg.value - deployFee;
        uint256 devBuyFee = (devBuyValue * DEV_BUY_FEE_BPS) / 10_000;
        uint256 remainingValue = devBuyValue - devBuyFee;
        QianToken t = new QianToken{salt: salt}(
            _name,
            _symbol,
            _metadata,
            msg.sender,
            address(this)
        );
        emit ERC20TokenCreated(address(t));
        address tokenAddress = address(t);
        _provideLiquidity(tokenAddress, weth, configId, hook, poolFee, msg.sender);
        // the guard hook has no configuration — it only gates initialize
        if (hook != guardHook) {
            PoolKey memory poolKey = tokenToPoolKey[tokenAddress];
            bytes32 poolId = IConfigurableHook(hook).getPoolId(poolKey);
            IConfigurableHook(hook).configureHook(poolId, tokenAddress, hookConfig);
            emit HookConfigured(tokenAddress, hook, poolId);
        }
        uint256 platformTake = deployFee + devBuyFee;
        if (platformTake > 0 && feeVault != address(0)) {
            IWETH(weth).deposit{value: platformTake}();
            IERC20(weth).safeTransfer(feeVault, platformTake);
        }
        tokensReceived = 0;
        if (remainingValue > 0) {
            tokensReceived = _executeInitialBuy(tokenAddress, remainingValue, msg.sender);
        }
        tokenConfigs[tokenAddress] = TokenConfig({
            deployTime: block.timestamp,
            metadata: _metadata,
            hookAddress: hook
        });
        tokenCount++;
        return tokensReceived;
    }

    // Fee collection now lives in QianFeeRouter.collectFees(token) — permissionless,
    // fixed 30% treasury / 70% creator split, served by the locker.

    /// @notice Controller-only: reassign a token's creator fee rights (approved CTO).
    /// @dev Fee routing only; cannot touch balances, transfers or supply.
    /// @dev The locker's binding is the only record; it rejects this call unless the token
    /// was bound by THIS factory and this factory is still authorized.
    function changeTokenFeeReceiver(address tokenAddress, address newCreator) external {
        if (msg.sender != platformController) revert NotController();
        if (newCreator == address(0)) revert InvalidReceiver();
        address oldCreator = tokenCreator(tokenAddress);
        // a token no factory ever launched has no fee rights to reassign
        if (oldCreator == address(0)) revert InvalidConfig();
        emit CreatorChanged(tokenAddress, oldCreator, newCreator);
        QianLocker(locker).setCreator(tokenAddress, newCreator);
    }

    function setHookApproved(address hook, bool approved) external {
        if (msg.sender != platformController) revert NotController();
        approvedHooks[hook] = approved;
        emit HookApprovalChanged(hook, approved);
    }

    // No withdrawFees / recoverETH / recoverToken, and no receive(): nothing accumulates
    // here, so nothing needs a sweep. deployCoin is payable and takes msg.value directly;
    // every other ETH movement is outbound.

    function setDeployFee(uint256 _deployFee) external {
        if (msg.sender != platformController) revert NotController();
        uint256 oldFee = deployFee;
        deployFee = _deployFee;
        emit DeployFeeUpdated(oldFee, _deployFee);
    }

    function setFeeVault(address _feeVault) external {
        if (msg.sender != platformController) revert NotController();
        if (_feeVault == address(0)) revert InvalidFeeVault();
        feeVault = _feeVault;
    }

    // setDefaultPoolFee is GONE: poolFee is mandatory and explicit on deployCoin. A
    // controller-settable default that lands in an immutable PoolKey was a front-run —
    // it could turn a pending launch into a permanent 20% tax. See deployCoin.

    /// @notice Controller-only: set the hook used by launches that request none. Must
    /// also be approved via setHookApproved. Set after deploy — factory and hook are
    /// circular, so neither can be a constructor arg of the other.
    function setGuardHook(address hook) external {
        if (msg.sender != platformController) revert NotController();
        if (hook == address(0)) revert InvalidConfig();
        guardHook = hook;
        emit GuardHookUpdated(hook);
    }

    /// @notice Controller-only kill switch.
    function toggleDeployCoin() external {
        if (msg.sender != platformController) revert NotController();
        deployCoinEnabled = !deployCoinEnabled;
        emit DeployCoinToggled(deployCoinEnabled);
    }

    function transferController(address newController) external {
        if (msg.sender != platformController) revert NotController();
        if (newController == address(0)) revert InvalidReceiver();
        pendingController = newController;
        emit ControllerTransferInitiated(platformController, newController);
    }

    function acceptController() external {
        if (msg.sender != pendingController) revert Unauthorized();
        address oldController = platformController;
        platformController = pendingController;
        pendingController = address(0);
        emit ControllerTransferred(oldController, platformController);
    }

    function setLiquidityConfig(
        uint256 id,
        uint160 sqrtA,
        uint160 sqrtB,
        int24 tickL,
        int24 tickU,
        uint256 amt0,
        uint256 amt1
    ) external returns (uint256) {
        if (msg.sender != platformController) revert NotController();
        if (tickL >= tickU) revert InvalidConfig();
        uint256 configId = id == type(uint256).max ? liquidityConfigCount++ : id;
        if (id != type(uint256).max && id >= liquidityConfigCount) revert InvalidConfig();
        liquidityConfigs[configId] = LiquidityConfig(sqrtA, sqrtB, tickL, tickU, amt0, amt1);
        return configId;
    }

    /// @notice Live receiver of a token's 70% creator fees, read by QianFeeRouter and the
    /// hooks. Reassignable via changeTokenFeeReceiver (CTO). The token's own `creator` is
    /// an immutable record of the original launcher, not this.
    /// @dev The binding in the locker is the only copy — it is written atomically with the
    /// lock and is what QianFeeRouter pays. This reads through so callers keep one address.
    function tokenCreator(address token) public view returns (address) {
        return QianLocker(locker).creatorOf(token);
    }

    function getTokenInfo(address token) external view returns (
        string memory name,
        string memory symbol,
        address creator,
        uint256 deployTime,
        string memory metadata,
        address hookAddress
    ) {
        TokenConfig memory config = tokenConfigs[token];
        QianToken t = QianToken(token);
        return (
            t.name(),
            t.symbol(),
            // the LIVE fee receiver (CTO-aware), not the token's immutable original —
            // one source of truth, the same one QianFeeRouter and the hooks read
            tokenCreator(token),
            config.deployTime,
            config.metadata,
            config.hookAddress
        );
    }

    function _provideLiquidity(
        address tokenA,
        address tokenB,
        uint256 configId,
        address hookAddress,
        uint24 poolFee,
        address creator
    ) internal {
        LiquidityConfig memory config = liquidityConfigs[configId];
        LiquidityProviderV4.Config memory libConfig = LiquidityProviderV4.Config({
            sqrtPriceX96A: config.sqrtPriceX96A,
            sqrtPriceX96B: config.sqrtPriceX96B,
            tickLower: config.tickLower,
            tickUpper: config.tickUpper,
            amount0Desired: config.amount0Desired,
            amount1Desired: config.amount1Desired
        });
        LiquidityProviderV4.LiquidityParams memory liqParams = LiquidityProviderV4.provideLiquidity(
            poolManager,
            tokenA,
            tokenB,
            libConfig,
            hookAddress,
            TICK_SPACING,
            poolFee
        );
        tokenToPoolKey[tokenA] = liqParams.poolKey;
        IERC20 token = IERC20(tokenA);
        uint256 tokenBalance = token.balanceOf(address(this));
        // Must be exact: the whole balance goes to the locker but lockV4 only mints
        // liquidity for amount0Desired, and the locker has no withdrawal path. Any
        // mismatch is a bad config and must revert rather than strand the difference.
        if (tokenBalance != config.amount0Desired) {
            revert InsufficientTokenBalance(config.amount0Desired, tokenBalance);
        }
        // The locker mints and permanently owns the LP position; it settles the mint
        // from the token balance we hand it here.
        token.safeTransfer(locker, tokenBalance);
        QianLocker(locker).lockV4(liqParams.poolKey, liqParams.tickLower, liqParams.tickUpper, liqParams.liquidity, tokenA, creator);
    }

    function _executeInitialBuy(
        address tokenAddress,
        uint256 ethAmount,
        address recipient
    ) internal returns (uint256 tokensReceived) {
        IWETH(weth).deposit{value: ethAmount}();
        IERC20(weth).approve(poolManager, ethAmount);
        PoolKey memory poolKey = tokenToPoolKey[tokenAddress];
        bool zeroForOne = weth < tokenAddress;
        bytes memory swapData = abi.encode(uint8(1), abi.encode(poolKey, zeroForOne, int256(ethAmount), recipient));
        bytes memory result = IPoolManager(poolManager).unlock(swapData);
        tokensReceived = abi.decode(result, (uint256));
        emit InitialBuyExecuted(tokenAddress, recipient, ethAmount, tokensReceived);
    }

    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != poolManager) revert Unauthorized();
        (uint8 opType, bytes memory opData) = abi.decode(data, (uint8, bytes));
        if (opType == 1) return _handleSwapCallback(opData);
        revert InvalidConfig();
    }

    function _handleSwapCallback(bytes memory data) internal returns (bytes memory) {
        (PoolKey memory key, bool zeroForOne, int256 amountSpecified, address recipient) =
            abi.decode(data, (PoolKey, bool, int256, address));
        IPoolManager manager = IPoolManager(poolManager);
        address token0 = Currency.unwrap(key.currency0);
        address token1 = Currency.unwrap(key.currency1);
        SwapParams memory params = SwapParams({
            zeroForOne: zeroForOne,
            amountSpecified: -amountSpecified,
            sqrtPriceLimitX96: zeroForOne ? uint160(4295128740) : uint160(1461446703485210103287273052203988822378723970341)
        });
        BalanceDelta delta = manager.swap(key, params, "");
        int128 deltaAmount0 = BalanceDeltaLibrary.amount0(delta);
        int128 deltaAmount1 = BalanceDeltaLibrary.amount1(delta);
        uint256 tokensReceived = 0;
        if (deltaAmount0 < 0) {
            uint256 amountOwed = uint256(-int256(deltaAmount0));
            manager.sync(key.currency0);
            IERC20(token0).safeTransfer(poolManager, amountOwed);
            manager.settle();
        }
        if (deltaAmount1 < 0) {
            uint256 amountOwed = uint256(-int256(deltaAmount1));
            manager.sync(key.currency1);
            IERC20(token1).safeTransfer(poolManager, amountOwed);
            manager.settle();
        }
        if (deltaAmount0 > 0) {
            tokensReceived = uint256(int256(deltaAmount0));
            manager.take(key.currency0, recipient, tokensReceived);
        }
        if (deltaAmount1 > 0) {
            tokensReceived = uint256(int256(deltaAmount1));
            manager.take(key.currency1, recipient, tokensReceived);
        }
        return abi.encode(tokensReceived);
    }

}
Chain explorer4394msChain node119ms