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
- Creator
- 0x92B92aA9fa…14d7369fA7
- 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