Qian V2 Contracts

Onchain token launch with permanently locked liquidity and DEX routing
(Uniswap v3 + v4). No admin keys, no mint/blacklist. MIT licensed.
Deployed addresses in README.
This commit is contained in:
Qian
2026-07-17 02:31:24 +00:00
commit 013b52f5a4
28 changed files with 2828 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title Create2Factory
* @notice Factory contract for deploying contracts to deterministic addresses using CREATE2
* @dev Used for deploying Uniswap V4 hooks to addresses with specific flag bits set
*/
contract Create2Factory {
event Deployed(address indexed addr, bytes32 salt);
/**
* @notice Deploy a contract using CREATE2
* @param salt The salt for CREATE2 address calculation
* @param initCode The contract creation code (bytecode + encoded constructor args)
* @return addr The deployed contract address
*/
function deploy(
bytes32 salt,
bytes calldata initCode
) external payable returns (address addr) {
assembly {
// Load initCode from calldata
let ptr := mload(0x40)
calldatacopy(ptr, initCode.offset, initCode.length)
// Deploy using CREATE2
addr := create2(
callvalue(), // ETH to send
ptr, // Init code pointer
initCode.length, // Init code length
salt // Salt
)
// Check deployment succeeded
if iszero(addr) {
revert(0, 0)
}
}
emit Deployed(addr, salt);
}
/**
* @notice Compute the CREATE2 address without deploying
* @param salt The salt for CREATE2 address calculation
* @param initCodeHash The keccak256 hash of the init code
* @return The computed address
*/
function computeAddress(
bytes32 salt,
bytes32 initCodeHash
) external view returns (address) {
return address(
uint160(
uint256(
keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
initCodeHash
)
)
)
)
);
}
/**
* @notice Compute the CREATE2 address from init code directly
* @param salt The salt for CREATE2 address calculation
* @param initCode The contract creation code
* @return The computed address
*/
function computeAddressFromCode(
bytes32 salt,
bytes calldata initCode
) external view returns (address) {
return address(
uint160(
uint256(
keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(initCode)
)
)
)
)
);
}
}
+390
View File
@@ -0,0 +1,390 @@
// ~ 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);
}
}
+196
View File
@@ -0,0 +1,196 @@
// ~ 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 "./interfaces/IPoolManager.sol";
import "./interfaces/ICurrency.sol";
import "./interfaces/IWETH.sol";
import "./QianLocker.sol";
import "./errors/Errors.sol";
interface IRouterSwapRouter02 {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
}
/// @notice Permissionless LP-fee collection for Qian V3 and V4 tokens.
/// WETH proceeds split TREASURY_BPS / creator on every call.
contract QianFeeRouter is ReentrancyGuard {
using SafeERC20 for IERC20;
event FeesCollected(
address indexed token,
address indexed creator,
uint256 creatorWeth,
uint256 treasuryWeth,
uint256 tokenSideTotal,
uint256 platformTokenSide
);
event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury);
event TokensLiquidated(address indexed token, uint256 amountIn, uint256 wethOut);
/// @dev recipient rejected native ETH and was paid WETH instead — see _payEth
event EthPayoutFellBackToWeth(address indexed to, uint256 amount);
uint256 public constant TREASURY_BPS = 3000; // 30% treasury / 70% creator
address public immutable locker;
address public immutable weth;
address public immutable swapRouter02;
address public immutable poolManager;
address public owner;
address public pendingOwner;
address public treasury;
constructor(address _locker, address _weth, address _treasury, address _swapRouter02, address _poolManager) {
if (_locker == address(0) || _weth == address(0) || _treasury == address(0) || _swapRouter02 == address(0) || _poolManager == address(0)) {
revert InvalidConfig();
}
locker = _locker;
weth = _weth;
treasury = _treasury;
swapRouter02 = _swapRouter02;
poolManager = _poolManager;
owner = msg.sender;
}
/// @dev WETH unwraps only; stray ETH would strand.
receive() external payable {
if (msg.sender != weth) revert Unauthorized();
}
/// @dev Pay in native ETH. If the recipient cannot take ETH, re-wrap and send WETH
/// rather than revert, so a claim is never blocked by the recipient's wallet.
/// Callers are nonReentrant.
function _payEth(address to, uint256 amount) internal {
IWETH(weth).withdraw(amount);
(bool ok, ) = to.call{value: amount}("");
if (!ok) {
IWETH(weth).deposit{value: amount}();
IERC20(weth).safeTransfer(to, amount);
emit EthPayoutFellBackToWeth(to, amount);
}
}
/// @notice Collect and distribute accrued LP fees for any Qian token in one call.
/// The locker's own registry answers everything — position, kind, creator — in one
/// read; no factory or token code is ever consulted.
function collectFees(address token) external nonReentrant returns (uint256 creatorWeth, uint256 treasuryWeth) {
QianLocker.Binding memory b = QianLocker(locker).bindingOf(token);
if (b.kind == 0) revert InvalidConfig(); // not a Qian token
if (b.creator == address(0)) revert InvalidReceiver();
uint256 wethBefore = IERC20(weth).balanceOf(address(this));
uint256 tokenBefore = IERC20(token).balanceOf(address(this));
QianLocker(locker).collect(token, address(this));
uint256 collectedWeth = IERC20(weth).balanceOf(address(this)) - wethBefore;
uint256 collectedTokenSide = IERC20(token).balanceOf(address(this)) - tokenBefore;
if (collectedWeth > 0) {
treasuryWeth = (collectedWeth * TREASURY_BPS) / 10_000;
creatorWeth = collectedWeth - treasuryWeth;
if (treasuryWeth > 0) _payEth(treasury, treasuryWeth);
if (creatorWeth > 0) _payEth(b.creator, creatorWeth);
}
uint256 platformTokenSide = 0;
if (collectedTokenSide > 0) {
platformTokenSide = (collectedTokenSide * TREASURY_BPS) / 10_000;
uint256 creatorTokenSide = collectedTokenSide - platformTokenSide;
if (creatorTokenSide > 0) IERC20(token).safeTransfer(b.creator, creatorTokenSide);
}
emit FeesCollected(token, b.creator, creatorWeth, treasuryWeth, collectedTokenSide, platformTokenSide);
// platform's token-side share (plus any dust) is market-sold to WETH -> treasury
uint256 bal = IERC20(token).balanceOf(address(this));
if (bal > 0) _liquidate(token, b, bal);
}
/// @dev Swap the router's token holdings into WETH and forward to treasury.
function _liquidate(address token, QianLocker.Binding memory b, uint256 amountIn) internal returns (uint256 wethOut) {
uint256 wethBefore = IERC20(weth).balanceOf(address(this));
if (b.kind == 1) {
// V3 pool: swap through SwapRouter02 (all Qian V3 pools are 1% tier)
IERC20(token).forceApprove(swapRouter02, amountIn);
IRouterSwapRouter02(swapRouter02).exactInputSingle(
IRouterSwapRouter02.ExactInputSingleParams({
tokenIn: token,
tokenOut: weth,
fee: 10000,
recipient: address(this),
amountIn: amountIn,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
})
);
} else {
bool zeroForOne = token < weth; // selling token for WETH
IPoolManager(poolManager).unlock(abi.encode(b.poolKey, zeroForOne, amountIn));
}
wethOut = IERC20(weth).balanceOf(address(this)) - wethBefore;
if (wethOut > 0) _payEth(treasury, wethOut);
emit TokensLiquidated(token, amountIn, wethOut);
}
/// @notice PoolManager callback for V4 liquidations (exact-in token -> WETH).
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != poolManager) revert Unauthorized();
(PoolKey memory key, bool zeroForOne, uint256 amountIn) = abi.decode(data, (PoolKey, bool, uint256));
IPoolManager manager = IPoolManager(poolManager);
SwapParams memory params = SwapParams({
zeroForOne: zeroForOne,
amountSpecified: -int256(amountIn),
sqrtPriceLimitX96: zeroForOne ? uint160(4295128740) : uint160(1461446703485210103287273052203988822378723970341)
});
BalanceDelta delta = manager.swap(key, params, "");
int128 d0 = BalanceDeltaLibrary.amount0(delta);
int128 d1 = BalanceDeltaLibrary.amount1(delta);
if (d0 < 0) {
manager.sync(key.currency0);
IERC20(Currency.unwrap(key.currency0)).safeTransfer(poolManager, uint256(-int256(d0)));
manager.settle();
}
if (d1 < 0) {
manager.sync(key.currency1);
IERC20(Currency.unwrap(key.currency1)).safeTransfer(poolManager, uint256(-int256(d1)));
manager.settle();
}
if (d0 > 0) manager.take(key.currency0, address(this), uint256(int256(d0)));
if (d1 > 0) manager.take(key.currency1, address(this), uint256(int256(d1)));
return "";
}
/// @notice Update the treasury wallet.
function setTreasury(address newTreasury) external {
if (msg.sender != owner) revert Unauthorized();
if (newTreasury == address(0)) revert InvalidReceiver();
emit TreasuryUpdated(treasury, newTreasury);
treasury = newTreasury;
}
/// @notice Two-step owner handover, same shape as everywhere else in the stack.
function transferOwner(address newOwner) external {
if (msg.sender != owner) revert Unauthorized();
if (newOwner == address(0)) revert InvalidReceiver();
pendingOwner = newOwner;
}
function acceptOwner() external {
if (msg.sender != pendingOwner) revert Unauthorized();
owner = pendingOwner;
pendingOwner = address(0);
}
}
+300
View File
@@ -0,0 +1,300 @@
// ~ 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 "./interfaces/IPoolManager.sol";
import "./interfaces/ICurrency.sol";
import "./interfaces/IUnlockCallback.sol";
import "./errors/Errors.sol";
interface ILockerV3PositionManager {
struct CollectParams {
uint256 tokenId;
address recipient;
uint128 amount0Max;
uint128 amount1Max;
}
function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1);
}
/// @notice Permanent LP locker for Qian V3 and V4 positions.
/// @dev Trust model is structural: this contract has NO function that transfers a V3 NFT
/// out, NO approve path, and NO code path that passes a negative liquidityDelta to the
/// V4 PoolManager. Once a position lands here it can never be withdrawn — only its
/// trading fees can be collected, and those flow to the Qian fee pipeline.
contract QianLocker is IUnlockCallback {
using SafeERC20 for IERC20;
using CurrencyLibrary for Currency;
event V3PositionLocked(uint256 indexed tokenId);
event V4PositionLocked(bytes32 indexed positionSalt);
event V3FeesCollected(uint256 indexed tokenId, uint256 amount0, uint256 amount1);
event V4FeesCollected(bytes32 indexed positionSalt, uint256 amount0, uint256 amount1);
event TokenBound(address indexed token, address indexed factory, address indexed creator, uint8 kind);
event CreatorChanged(address indexed token, address indexed oldCreator, address indexed newCreator);
event CallersSet(address v4Factory, address feeRouter);
event FactoryAuthorized(address indexed factory);
event FactoryRevoked(address indexed factory);
event DeployerTransferInitiated(address indexed current, address indexed pending);
event DeployerTransferred(address indexed oldDeployer, address indexed newDeployer);
address public immutable poolManager;
address public immutable v3PositionManager;
/// @notice Only address that may call setCallers, addFactory or removeFactory.
/// Transferable so custody can move to a multisig; grants no way to move LP out.
address public deployer;
address public pendingDeployer;
/// @notice Factories allowed to lock positions and bind tokens. Grants no way to
/// take LP out.
mapping(address => bool) public authorizedFactories;
address public feeRouter;
uint8 public constant KIND_NONE = 0;
uint8 public constant KIND_V3 = 1;
uint8 public constant KIND_V4 = 2;
/// @notice First-party registry: who a token is, recorded ATOMICALLY when its
/// liquidity is locked. A factory can only bind a position it is locking, in that
/// transaction, to a token it names at that moment — it is never consulted again.
/// Write-once except `creator` (CTO, via the binding factory only).
struct Binding {
uint8 kind; // KIND_V3 | KIND_V4
address factory; // who locked it (sole caller of setCreator)
address creator; // fee receiver, 70% side
uint256 nftId; // V3 only
PoolKey poolKey; // V4 only
int24 tickLower; // V4 only
int24 tickUpper; // V4 only
}
mapping(address => Binding) internal bindings;
function bindingOf(address token) external view returns (Binding memory) {
return bindings[token];
}
function creatorOf(address token) external view returns (address) {
return bindings[token].creator;
}
/// @notice CTO: reassign a token's fee receiver. Only the factory that locked the
/// token's liquidity may do this — a factory manages only its own launches — and only
/// while it is still authorized, so removeFactory revokes CTO along with the ability
/// to launch. Without the second check a revoked factory keeps redirecting the 70%
/// on every token it ever bound.
function setCreator(address token, address newCreator) external {
Binding storage b = bindings[token];
if (b.kind == KIND_NONE || msg.sender != b.factory) revert Unauthorized();
if (!authorizedFactories[msg.sender]) revert Unauthorized();
if (newCreator == address(0)) revert InvalidReceiver();
emit CreatorChanged(token, b.creator, newCreator);
b.creator = newCreator;
}
function _bind(address token, address factory, address creator, uint8 kind) internal returns (Binding storage b) {
if (token == address(0) || creator == address(0)) revert InvalidConfig();
b = bindings[token];
if (b.kind != KIND_NONE) revert InvalidConfig(); // one token, one position, bound once
b.kind = kind;
b.factory = factory;
b.creator = creator;
emit TokenBound(token, factory, creator, kind);
}
constructor(address _poolManager, address _v3PositionManager) {
if (_poolManager == address(0) || _v3PositionManager == address(0)) revert InvalidConfig();
poolManager = _poolManager;
v3PositionManager = _v3PositionManager;
deployer = msg.sender;
}
/// @notice One-time wiring of the fee router + the first factory.
/// @dev feeRouter is one-shot: it names where collected fees go, so a mutable pointer
/// would be a fee-redirection vector.
function setCallers(address _v4Factory, address _feeRouter) external {
if (msg.sender != deployer) revert Unauthorized();
if (feeRouter != address(0)) revert InvalidConfig();
if (_v4Factory == address(0) || _feeRouter == address(0)) revert InvalidConfig();
authorizedFactories[_v4Factory] = true;
feeRouter = _feeRouter;
emit CallersSet(_v4Factory, _feeRouter);
emit FactoryAuthorized(_v4Factory);
}
/// @notice Begin handing over custody. Two-step: the new deployer must accept, so a
/// typo cannot strand addFactory/removeFactory on an address nobody holds.
function transferDeployer(address newDeployer) external {
if (msg.sender != deployer) revert Unauthorized();
if (newDeployer == address(0)) revert InvalidReceiver();
pendingDeployer = newDeployer;
emit DeployerTransferInitiated(deployer, newDeployer);
}
/// @notice Complete the handover. Callable only by the pending deployer.
function acceptDeployer() external {
if (msg.sender != pendingDeployer) revert Unauthorized();
address old = deployer;
deployer = pendingDeployer;
pendingDeployer = address(0);
emit DeployerTransferred(old, deployer);
}
/// @notice Authorize an additional factory generation.
function addFactory(address _factory) external {
if (msg.sender != deployer) revert Unauthorized();
if (_factory == address(0)) revert InvalidConfig();
authorizedFactories[_factory] = true;
emit FactoryAuthorized(_factory);
}
/// @notice Revoke a factory: it can no longer lock positions, bind tokens, or
/// reassign the creator on bindings it already made. Existing bindings still pay out
/// — collect() is permissionless and reads the binding, so revoking never strands
/// fees. Cannot move LP out.
function removeFactory(address _factory) external {
if (msg.sender != deployer) revert Unauthorized();
authorizedFactories[_factory] = false;
emit FactoryRevoked(_factory);
}
// ---------------------------------------------------------------- V3
/// @notice Accepts position NFTs from the canonical position manager, sent by an
/// authorized factory, carrying the (token, creator) binding in `data`. The binding
/// is recorded in the same transfer that surrenders the position — the claim and the
/// collateral arrive atomically.
function onERC721Received(address, address from, uint256 tokenId, bytes calldata data) external returns (bytes4) {
if (msg.sender != v3PositionManager) revert Unauthorized();
if (!authorizedFactories[from]) revert Unauthorized();
(address token, address creator) = abi.decode(data, (address, address));
Binding storage b = _bind(token, from, creator, KIND_V3);
b.nftId = tokenId;
emit V3PositionLocked(tokenId);
return this.onERC721Received.selector;
}
// ---------------------------------------------------------------- V4
/// @notice Mint a V4 position owned by this locker and bind it to `token`. The
/// factory must have transferred the tokens to settle the mint beforehand.
function lockV4(PoolKey calldata key, int24 tickLower, int24 tickUpper, uint128 liquidity, address token, address creator) external {
if (!authorizedFactories[msg.sender]) revert Unauthorized();
if (liquidity == 0 || tickLower >= tickUpper) revert InvalidTickRange();
Binding storage b = _bind(token, msg.sender, creator, KIND_V4);
b.poolKey = key;
b.tickLower = tickLower;
b.tickUpper = tickUpper;
bytes32 salt = _positionSalt(key, tickLower, tickUpper);
IPoolManager(poolManager).unlock(
abi.encode(uint8(0), abi.encode(key, tickLower, tickUpper, liquidity, salt))
);
emit V4PositionLocked(salt);
}
// ---------------------------------------------------------------- fees
/// @notice Collect accrued LP fees for a bound token to `recipient`. The position is
/// resolved from this contract's own registry — nothing external is consulted.
function collect(address token, address recipient) external returns (uint256 amount0, uint256 amount1) {
if (msg.sender != feeRouter) revert Unauthorized();
if (recipient == address(0)) revert InvalidReceiver();
Binding storage b = bindings[token];
if (b.kind == KIND_V3) {
(amount0, amount1) = ILockerV3PositionManager(v3PositionManager).collect(
ILockerV3PositionManager.CollectParams({
tokenId: b.nftId,
recipient: recipient,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
emit V3FeesCollected(b.nftId, amount0, amount1);
} else if (b.kind == KIND_V4) {
bytes32 salt = _positionSalt(b.poolKey, b.tickLower, b.tickUpper);
bytes memory result = IPoolManager(poolManager).unlock(
abi.encode(uint8(1), abi.encode(b.poolKey, b.tickLower, b.tickUpper, salt, recipient))
);
(amount0, amount1) = abi.decode(result, (uint256, uint256));
emit V4FeesCollected(salt, amount0, amount1);
} else {
revert InvalidConfig(); // unbound token
}
}
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != poolManager) revert Unauthorized();
(uint8 op, bytes memory opData) = abi.decode(data, (uint8, bytes));
if (op == 0) return _handleLock(opData);
if (op == 1) return _handleCollect(opData);
revert InvalidConfig();
}
function _handleLock(bytes memory data) internal returns (bytes memory) {
(PoolKey memory key, int24 tickLower, int24 tickUpper, uint128 liquidity, bytes32 salt) =
abi.decode(data, (PoolKey, int24, int24, uint128, bytes32));
IPoolManager manager = IPoolManager(poolManager);
(BalanceDelta callerDelta,) = manager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: int256(uint256(liquidity)),
salt: salt
}),
""
);
_settleOwed(key.currency0, BalanceDeltaLibrary.amount0(callerDelta));
_settleOwed(key.currency1, BalanceDeltaLibrary.amount1(callerDelta));
return "";
}
function _handleCollect(bytes memory data) internal returns (bytes memory) {
(PoolKey memory key, int24 tickLower, int24 tickUpper, bytes32 salt, address recipient) =
abi.decode(data, (PoolKey, int24, int24, bytes32, address));
IPoolManager manager = IPoolManager(poolManager);
(BalanceDelta callerDelta,) = manager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: 0,
salt: salt
}),
""
);
int128 deltaAmount0 = BalanceDeltaLibrary.amount0(callerDelta);
int128 deltaAmount1 = BalanceDeltaLibrary.amount1(callerDelta);
uint256 amount0Collected = 0;
uint256 amount1Collected = 0;
if (deltaAmount0 > 0) {
amount0Collected = uint256(int256(deltaAmount0));
manager.take(key.currency0, recipient, amount0Collected);
}
if (deltaAmount1 > 0) {
amount1Collected = uint256(int256(deltaAmount1));
manager.take(key.currency1, recipient, amount1Collected);
}
return abi.encode(amount0Collected, amount1Collected);
}
function _settleOwed(Currency currency, int128 delta) internal {
if (delta < 0) {
IPoolManager manager = IPoolManager(poolManager);
manager.sync(currency);
IERC20(Currency.unwrap(currency)).safeTransfer(poolManager, uint256(-int256(delta)));
manager.settle();
}
}
/// @dev Positions are addressable from (poolKey, ticks) alone.
function _positionSalt(PoolKey memory key, int24 tickLower, int24 tickUpper) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(
Currency.unwrap(key.currency0),
Currency.unwrap(key.currency1),
key.fee,
tickLower,
tickUpper
));
}
}
+243
View File
@@ -0,0 +1,243 @@
// ~ 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 "./interfaces/IPoolManager.sol";
import "./interfaces/ICurrency.sol";
import "./interfaces/IWETH.sol";
import "./errors/Errors.sol";
import "./QianLocker.sol";
interface ITradeUniV3Factory {
function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address);
}
interface ITradeUniV3Pool {
function liquidity() external view returns (uint128);
}
interface ITradeSwapRouter02 {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
}
/// @notice Qian's user-facing swap router: buy/sell any Qian-launched token (V3 or V4)
/// with plain ETH, platform takes FEE_BPS of the WETH side of every trade.
/// Buys: fee off the ETH in. Sells: fee off the ETH out. Fee goes straight to
/// treasury as WETH — this contract holds no balances between transactions.
/// Qian tokens are auto-detected (V3 1% pool or V4 PoolKey from our factories);
/// any other token trades against its canonical Uniswap V3 WETH pool at the
/// deepest-liquidity fee tier. Non-Qian V4 pools aren't supported (PoolKey unknowable).
contract QianSwapRouter is ReentrancyGuard {
using SafeERC20 for IERC20;
event Bought(address indexed token, address indexed buyer, uint256 ethIn, uint256 fee, uint256 tokensOut);
event Sold(address indexed token, address indexed seller, uint256 tokensIn, uint256 fee, uint256 ethOut);
event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury);
uint256 public constant FEE_BPS = 100; // 1% of the WETH side, every buy and sell
address public immutable weth;
address public immutable swapRouter02;
address public immutable poolManager;
address public immutable uniV3Factory;
/// @dev Root of trust for "is this a Qian token" — see _factoryOf.
address public immutable locker;
address public owner;
address public pendingOwner;
address public treasury;
constructor(address _weth, address _treasury, address _swapRouter02, address _poolManager, address _uniV3Factory, address _locker) {
if (_weth == address(0) || _treasury == address(0) || _swapRouter02 == address(0) || _poolManager == address(0) || _uniV3Factory == address(0) || _locker == address(0)) {
revert InvalidConfig();
}
locker = _locker;
uniV3Factory = _uniV3Factory;
weth = _weth;
treasury = _treasury;
swapRouter02 = _swapRouter02;
poolManager = _poolManager;
owner = msg.sender;
}
/// @notice Buy a Qian token with ETH. 1% platform fee off the ETH in,
/// remainder swapped; tokens go directly to msg.sender (never held here,
/// so launch-guard checks apply to the buyer, not the router).
function buy(address token, uint256 minTokensOut) external payable nonReentrant returns (uint256 tokensOut) {
if (msg.value == 0) revert InvalidConfig();
uint256 fee = (msg.value * FEE_BPS) / 10_000;
uint256 amountIn = msg.value - fee;
IWETH(weth).deposit{value: msg.value}();
IERC20(weth).safeTransfer(treasury, fee);
(bool isV4, uint24 v3Fee) = _route(token);
if (isV4) {
tokensOut = _swapV4(token, /*tokenIn=*/ weth, amountIn, msg.sender);
} else {
IERC20(weth).forceApprove(swapRouter02, amountIn);
tokensOut = ITradeSwapRouter02(swapRouter02).exactInputSingle(
ITradeSwapRouter02.ExactInputSingleParams({
tokenIn: weth,
tokenOut: token,
fee: v3Fee,
recipient: msg.sender,
amountIn: amountIn,
amountOutMinimum: minTokensOut,
sqrtPriceLimitX96: 0
})
);
}
if (tokensOut < minTokensOut) revert InsufficientOutput();
emit Bought(token, msg.sender, msg.value, fee, tokensOut);
}
/// @notice Sell a Qian token for ETH (needs prior token approval on this router).
/// 1% platform fee off the WETH out; remainder unwrapped and sent as ETH.
function sell(address token, uint256 tokensIn, uint256 minEthOut) external nonReentrant returns (uint256 ethOut) {
if (tokensIn == 0) revert InvalidConfig();
// Fee-on-transfer support: swap what actually arrived, not the nominal amount.
uint256 balBefore = IERC20(token).balanceOf(address(this));
IERC20(token).safeTransferFrom(msg.sender, address(this), tokensIn);
tokensIn = IERC20(token).balanceOf(address(this)) - balBefore;
if (tokensIn == 0) revert InvalidConfig();
uint256 wethOut;
(bool isV4, uint24 v3Fee) = _route(token);
if (isV4) {
wethOut = _swapV4(token, /*tokenIn=*/ token, tokensIn, address(this));
} else {
IERC20(token).forceApprove(swapRouter02, tokensIn);
wethOut = ITradeSwapRouter02(swapRouter02).exactInputSingle(
ITradeSwapRouter02.ExactInputSingleParams({
tokenIn: token,
tokenOut: weth,
fee: v3Fee,
recipient: address(this),
amountIn: tokensIn,
amountOutMinimum: 0, // final check is on ethOut after fee
sqrtPriceLimitX96: 0
})
);
}
uint256 fee = (wethOut * FEE_BPS) / 10_000;
ethOut = wethOut - fee;
if (ethOut < minEthOut) revert InsufficientOutput();
IERC20(weth).safeTransfer(treasury, fee);
IWETH(weth).withdraw(ethOut);
(bool ok, ) = msg.sender.call{value: ethOut}("");
if (!ok) revert TransferFailed();
emit Sold(token, msg.sender, tokensIn, fee, ethOut);
}
/// @dev Pick the pool for a token: Qian V3 -> 1% tier, Qian V4 -> V4 path,
/// anything else -> its canonical Uniswap V3 WETH pool at the deepest-liquidity
/// fee tier (non-Qian tokens are welcome; V4 is Qian-only since arbitrary
/// PoolKeys can't be derived from a token address). Qian-ness is one read of the
/// locker's registry — no code on the (untrusted) token is ever called.
function _route(address token) internal view returns (bool isV4, uint24 v3Fee) {
uint8 kind = QianLocker(locker).bindingOf(token).kind;
if (kind == 1) return (false, 10000); // Qian V3 pools are all 1% tier
if (kind == 2) return (true, 0);
uint24[4] memory tiers = [uint24(10000), 3000, 500, 100];
uint128 bestLiquidity;
for (uint256 i = 0; i < 4; i++) {
address pool = ITradeUniV3Factory(uniV3Factory).getPool(token, weth, tiers[i]);
if (pool == address(0)) continue;
uint128 liq = ITradeUniV3Pool(pool).liquidity();
if (liq > bestLiquidity || (v3Fee == 0 && liq == 0)) {
bestLiquidity = liq;
v3Fee = tiers[i];
}
}
if (v3Fee == 0) revert InvalidConfig(); // no WETH pool exists for this token
}
/// @dev Exact-in swap on the token's V4 pool; output taken straight to `recipient`.
function _swapV4(address token, address tokenIn, uint256 amountIn, address recipient) internal returns (uint256 amountOut) {
QianLocker.Binding memory b = QianLocker(locker).bindingOf(token);
if (b.kind != 2) revert InvalidConfig(); // not a Qian V4 token
PoolKey memory key = b.poolKey;
bool zeroForOne = tokenIn == Currency.unwrap(key.currency0);
bytes memory result = IPoolManager(poolManager).unlock(abi.encode(key, zeroForOne, amountIn, recipient));
amountOut = abi.decode(result, (uint256));
}
/// @notice PoolManager callback: settle the input from this contract's balance,
/// take the output directly to the trade's recipient.
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != poolManager) revert Unauthorized();
(PoolKey memory key, bool zeroForOne, uint256 amountIn, address recipient) = abi.decode(data, (PoolKey, bool, uint256, address));
IPoolManager manager = IPoolManager(poolManager);
SwapParams memory params = SwapParams({
zeroForOne: zeroForOne,
amountSpecified: -int256(amountIn),
sqrtPriceLimitX96: zeroForOne ? uint160(4295128740) : uint160(1461446703485210103287273052203988822378723970341)
});
BalanceDelta delta = manager.swap(key, params, "");
int128 d0 = BalanceDeltaLibrary.amount0(delta);
int128 d1 = BalanceDeltaLibrary.amount1(delta);
uint256 amountOut;
if (d0 < 0) {
manager.sync(key.currency0);
IERC20(Currency.unwrap(key.currency0)).safeTransfer(poolManager, uint256(-int256(d0)));
manager.settle();
}
if (d1 < 0) {
manager.sync(key.currency1);
IERC20(Currency.unwrap(key.currency1)).safeTransfer(poolManager, uint256(-int256(d1)));
manager.settle();
}
if (d0 > 0) {
amountOut = uint256(int256(d0));
manager.take(key.currency0, recipient, amountOut);
}
if (d1 > 0) {
amountOut = uint256(int256(d1));
manager.take(key.currency1, recipient, amountOut);
}
return abi.encode(amountOut);
}
function setTreasury(address newTreasury) external {
if (msg.sender != owner) revert Unauthorized();
if (newTreasury == address(0)) revert InvalidReceiver();
emit TreasuryUpdated(treasury, newTreasury);
treasury = newTreasury;
}
/// @notice Two-step owner handover, same shape as everywhere else in the stack.
function transferOwner(address newOwner) external {
if (msg.sender != owner) revert Unauthorized();
if (newOwner == address(0)) revert InvalidReceiver();
pendingOwner = newOwner;
}
function acceptOwner() external {
if (msg.sender != pendingOwner) revert Unauthorized();
owner = pendingOwner;
pendingOwner = address(0);
}
/// @dev Only WETH.withdraw sends ETH here.
receive() external payable {
if (msg.sender != weth) revert Unauthorized();
}
}
+60
View File
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT
//
// ██████╗ ██╗ █████╗ ███╗ ██╗
// ██╔═══██╗██║██╔══██╗████╗ ██║
// ██║ ██║██║███████║██╔██╗ ██║
// ██║▄▄ ██║██║██╔══██║██║╚██╗██║
// ╚██████╔╝██║██║ ██║██║ ╚████║
// ╚══▀▀═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝
//
// 钱 · Qian.ag — Multichain DEX & Launchpad
// Fixed supply. No owner, no mint, no games.
// — V2.1 —
//
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
/// @notice Fixed-supply ERC20. Supply is minted once here and there is no mint function.
/// No transfer hooks, no owner, no privileged functions.
contract QianToken is ERC20, ERC20Burnable {
/// @dev Same signature as OpenZeppelin Ownable. Indexers read renouncement from this
/// event rather than calling owner().
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/// @notice The factory that deployed this token.
address public immutable platform;
/// @notice Original launcher. A record, not the fee receiver — fee rights are
/// reassignable, so read `tokenCreator(token)` on `platform` for who is paid today.
address public immutable creator;
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10 ** 18;
/// @notice ipfs://<cid> for the token's image/description, so indexers need no factory.
string public metadata;
constructor(
string memory _name,
string memory _symbol,
string memory _metadata,
address _creator,
address _platform
) ERC20(_name, _symbol) {
platform = _platform;
creator = _creator;
metadata = _metadata;
_mint(_platform, MAX_SUPPLY);
emit OwnershipTransferred(address(0), _platform);
emit OwnershipTransferred(_platform, address(0));
}
/// @notice Common indexer convention — same value as metadata().
function tokenURI() external view returns (string memory) {
return metadata;
}
/// @notice No owner, no mint, no pause, no blacklist.
function owner() public pure returns (address) {
return address(0);
}
}
+22
View File
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
error BadRouterConfig();
error DeployDisabled();
error InsufficientDeployFee();
error DeployFeeTooHigh();
error InvalidFeeVault();
error InvalidConfig();
error InvalidReceiver();
error InvalidTickRange();
error NotController();
error Unauthorized();
error TransferFailed();
error InsufficientTokenBalance(uint256 required, uint256 available);
error HookNotApproved();
error HookAlreadyConfigured();
error InvalidTaxRate();
error InvalidThreshold();
error InvalidPoolFee();
error InsufficientOutput();
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
// Qian Exchange - https://qian.ag
pragma solidity ^0.8.24;
import "./IPoolManager.sol";
/**
* @title IConfigurableHook
* @notice Generic interface for hooks that can be configured by the factory
* @dev All hooks that want to be auto-configured by QianFactoryV4 must implement this interface
*
* This allows the factory to remain hook-agnostic - new hooks can be added
* without modifying the factory contract. Each hook:
* 1. Defines its own config format (decoded from bytes)
* 2. Validates its own constraints (tax limits, thresholds, etc.)
* 3. Stores per-pool configuration
*
* Example config encoding:
* - AutoBuyBurnHook: abi.encode(uint16 buyTaxBps, uint16 sellTaxBps, uint256 threshold)
* - QianTaxHook: abi.encode(uint16 buyTaxBps, uint16 sellTaxBps, uint16 snipeStartBps, uint16 snipeEndBps, uint32 snipeDecaySeconds)
* - FutureHook: abi.encode(param1, param2, param3, ...)
*/
interface IConfigurableHook {
/**
* @notice Configure the hook for a newly deployed token's pool
* @param poolId The pool identifier (keccak256 of PoolKey)
* @param tokenAddress The token address being configured
* @param config Hook-specific configuration data (abi.encoded)
* @dev Called by factory during deployCoin(). Must validate and store config.
* Should revert if already configured (immutability) or invalid params.
*/
function configureHook(
bytes32 poolId,
address tokenAddress,
bytes calldata config
) external;
/**
* @notice Get the pool ID from a PoolKey
* @param key The pool key
* @return poolId The computed pool identifier
*/
function getPoolId(PoolKey calldata key) external pure returns (bytes32);
/**
* @notice Check if a pool has been configured for this hook
* @param poolId The pool identifier
* @return True if the pool is configured and enabled
*/
function isConfigured(bytes32 poolId) external view returns (bool);
}
+31
View File
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
// Qian Exchange - https://qian.ag
pragma solidity ^0.8.24;
/// @notice Currency type for Uniswap V4
/// Wraps an address to represent a token or native ETH
type Currency is address;
/// @notice Currency library for V4
library CurrencyLibrary {
/// @notice Wrap an address as Currency
function wrap(address token) internal pure returns (Currency) {
return Currency.wrap(token);
}
/// @notice Unwrap Currency to address
function unwrap(Currency currency) internal pure returns (address) {
return Currency.unwrap(currency);
}
/// @notice Native ETH currency
function native() internal pure returns (Currency) {
return Currency.wrap(address(0));
}
/// @notice Check if currency is native ETH
function isNative(Currency currency) internal pure returns (bool) {
return Currency.unwrap(currency) == address(0);
}
}
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
// Qian Exchange - https://qian.ag
pragma solidity ^0.8.24;
/// @notice Optional interface for hooks to expose fee configuration
/// This allows the factory/fee collector to understand hook fee behavior
interface IHookFeeConfig {
/// @notice Get the fee split configuration
/// @return platformBps Platform fee in basis points (out of 10000)
/// @return hookBps Hook fee in basis points (out of 10000)
/// @return creatorBps Creator fee in basis points (out of 10000)
/// @dev Sum should equal 10000
function getFeeSplit() external view returns (uint256 platformBps, uint256 hookBps, uint256 creatorBps);
/// @notice Get hook name/identifier
function getHookName() external view returns (string memory);
}
+50
View File
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
// Qian Exchange - https://qian.ag
pragma solidity ^0.8.24;
import "./IPoolManager.sol";
/// @notice Swap parameters for hook callbacks
struct SwapParams {
bool zeroForOne;
int256 amountSpecified;
uint160 sqrtPriceLimitX96;
}
/// @notice Uniswap V4 Hooks interface
/// Hooks can implement various callbacks to customize pool behavior
interface IHooks {
/// @notice Called before a pool is initialized
function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96) external returns (bytes4);
/// @notice Called after a pool is initialized
function afterInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96, int24 tick) external returns (bytes4);
/// @notice Called before a swap
function beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata hookData) external returns (bytes4, BeforeSwapDelta, uint24);
/// @notice Called after a swap
function afterSwap(address sender, PoolKey calldata key, SwapParams calldata params, BalanceDelta delta, bytes calldata hookData) external returns (bytes4, int128);
/// @notice Called before liquidity is added
function beforeAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, bytes calldata hookData) external returns (bytes4);
/// @notice Called after liquidity is added
function afterAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, BalanceDelta delta, BalanceDelta feesAccrued, bytes calldata hookData) external returns (bytes4, BalanceDelta);
/// @notice Called before liquidity is removed
function beforeRemoveLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, bytes calldata hookData) external returns (bytes4);
/// @notice Called after liquidity is removed
function afterRemoveLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, BalanceDelta delta, BalanceDelta feesAccrued, bytes calldata hookData) external returns (bytes4, BalanceDelta);
/// @notice Called before a donate
function beforeDonate(address sender, PoolKey calldata key, uint256 amount0, uint256 amount1, bytes calldata hookData) external returns (bytes4);
/// @notice Called after a donate
function afterDonate(address sender, PoolKey calldata key, uint256 amount0, uint256 amount1, bytes calldata hookData) external returns (bytes4);
}
/// @notice Before swap delta type - encodes (specifiedDelta, unspecifiedDelta)
type BeforeSwapDelta is int256;
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "./ICurrency.sol";
import "./IHooks.sol";
// BalanceDelta is a packed int256 where:
// - Upper 128 bits = amount0 (int128)
// - Lower 128 bits = amount1 (int128)
type BalanceDelta is int256;
/// @notice Library for getting amount0/amount1 from BalanceDelta
library BalanceDeltaLibrary {
function amount0(BalanceDelta balanceDelta) internal pure returns (int128 _amount0) {
assembly ("memory-safe") {
_amount0 := sar(128, balanceDelta)
}
}
function amount1(BalanceDelta balanceDelta) internal pure returns (int128 _amount1) {
assembly ("memory-safe") {
_amount1 := signextend(15, balanceDelta)
}
}
}
/// @notice PoolKey identifies a V4 pool
struct PoolKey {
Currency currency0;
Currency currency1;
uint24 fee;
int24 tickSpacing;
IHooks hooks;
}
/// @notice Parameters for modifyLiquidity
struct ModifyLiquidityParams {
int24 tickLower;
int24 tickUpper;
int256 liquidityDelta;
bytes32 salt;
}
/// @notice Interface for Uniswap V4 PoolManager
interface IPoolManager {
/// @notice Initialize the state for a given pool
function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24 tick);
/// @notice Modify the liquidity for the given pool
function modifyLiquidity(PoolKey memory key, ModifyLiquidityParams memory params, bytes calldata hookData)
external
returns (BalanceDelta callerDelta, BalanceDelta feesAccrued);
/// @notice All interactions on the contract that account deltas require unlocking
function unlock(bytes calldata data) external returns (bytes memory);
/// @notice Sync the currency to its current balance in the PoolManager
function sync(Currency currency) external;
/// @notice Settle the currency, paying what is owed
function settle() external payable returns (uint256 paid);
/// @notice Take currency from the PoolManager
/// @param currency The currency to take
/// @param to The address to receive the currency
/// @param amount The amount to take
function take(Currency currency, address to, uint256 amount) external;
/// @notice Execute a swap
/// @param key The pool key
/// @param params The swap parameters
/// @param hookData Any data to be passed to hooks
function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData)
external
returns (BalanceDelta swapDelta);
}
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Qian Exchange - https://qian.ag
pragma solidity ^0.8.24;
/// @notice Interface for the callback executed when an address unlocks the pool manager
interface IUnlockCallback {
/// @notice Called by the pool manager on `msg.sender` when the manager is unlocked
/// @param data The data that was passed to the call to unlock
/// @return Any data that you want to be returned from the unlock call
function unlockCallback(bytes calldata data) external returns (bytes memory);
}
+13
View File
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
// Qian Exchange - https://qian.ag
pragma solidity ^0.8.24;
interface IWETH {
function deposit() external payable;
function withdraw(uint256 amount) external;
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
@@ -0,0 +1,177 @@
// SPDX-License-Identifier: MIT
// Qian Exchange - https://qian.ag
pragma solidity ^0.8.24;
/// @title LiquidityMath
/// @notice Math library for calculating liquidity from amounts and tick ranges
/// @dev Similar to Uniswap V3's LiquidityAmounts library
library LiquidityMath {
/// @notice Calculates the amount of liquidity for a given amount of token0
/// @param sqrtRatioAX96 A sqrt price representing the first tick boundary
/// @param sqrtRatioBX96 A sqrt price representing the second tick boundary
/// @param amount0 The amount0 being sent in
/// @return liquidity The amount of returned liquidity
function getLiquidityForAmount0(
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
uint256 amount0
) internal pure returns (uint128 liquidity) {
if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
uint256 intermediate = FullMath.mulDiv(sqrtRatioAX96, sqrtRatioBX96, FixedPoint96.Q96);
return uint128(FullMath.mulDiv(amount0, intermediate, sqrtRatioBX96 - sqrtRatioAX96));
}
/// @notice Calculates the amount of liquidity for a given amount of token1
/// @param sqrtRatioAX96 A sqrt price representing the first tick boundary
/// @param sqrtRatioBX96 A sqrt price representing the second tick boundary
/// @param amount1 The amount1 being sent in
/// @return liquidity The amount of returned liquidity
function getLiquidityForAmount1(
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
uint256 amount1
) internal pure returns (uint128 liquidity) {
if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
return uint128(FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtRatioBX96 - sqrtRatioAX96));
}
/// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current
/// pool prices and the prices at the tick boundaries
/// @param sqrtRatioX96 A sqrt price representing the current pool prices
/// @param sqrtRatioAX96 A sqrt price representing the first tick boundary
/// @param sqrtRatioBX96 A sqrt price representing the second tick boundary
/// @param amount0 The amount of token0 being sent in
/// @param amount1 The amount of token1 being sent in
/// @return liquidity The maximum amount of liquidity received
function getLiquidityForAmounts(
uint160 sqrtRatioX96,
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
uint256 amount0,
uint256 amount1
) internal pure returns (uint128 liquidity) {
if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
if (sqrtRatioX96 <= sqrtRatioAX96) {
liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0);
} else if (sqrtRatioX96 < sqrtRatioBX96) {
uint128 liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0);
uint128 liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1);
liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1;
} else {
liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1);
}
}
/// @notice Converts a tick to sqrt price
/// @param tick The tick to convert
/// @return sqrtPriceX96 The sqrt price
function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) {
uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));
require(absTick <= uint256(int256(887272)), "T");
uint256 ratio = absTick & 0x1 != 0
? 0xfffcb933bd6fad37aa2d162d1a594001
: 0x100000000000000000000000000000000;
if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128;
if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128;
if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128;
if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128;
if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128;
if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128;
if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128;
if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128;
if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128;
if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128;
if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128;
if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;
if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128;
if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128;
if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128;
if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128;
if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128;
if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128;
if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128;
if (tick > 0) ratio = type(uint256).max / ratio;
sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1));
}
}
/// @title FullMath
/// @notice Math library for full precision multiplication and division
library FullMath {
/// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
/// @param a The multiplicand
/// @param b The multiplier
/// @param denominator The divisor
/// @return result The 256-bit result
function mulDiv(
uint256 a,
uint256 b,
uint256 denominator
) internal pure returns (uint256 result) {
unchecked {
uint256 prod0;
uint256 prod1;
assembly {
let mm := mulmod(a, b, not(0))
prod0 := mul(a, b)
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}
if (prod1 == 0) {
require(denominator > 0);
assembly {
result := div(prod0, denominator)
}
return result;
}
require(denominator > prod1);
uint256 remainder;
assembly {
remainder := mulmod(a, b, denominator)
}
assembly {
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}
uint256 twos = (type(uint256).max - denominator + 1) & denominator;
assembly {
denominator := div(denominator, twos)
}
assembly {
prod0 := div(prod0, twos)
}
assembly {
twos := add(div(sub(0, twos), twos), 1)
}
prod0 |= prod1 * twos;
uint256 inv = (3 * denominator) ^ 2;
inv *= 2 - denominator * inv;
inv *= 2 - denominator * inv;
inv *= 2 - denominator * inv;
inv *= 2 - denominator * inv;
inv *= 2 - denominator * inv;
inv *= 2 - denominator * inv;
result = prod0 * inv;
return result;
}
}
}
/// @title FixedPoint96
/// @notice Math library for fixed point arithmetic with 96 bits of precision
library FixedPoint96 {
uint8 internal constant RESOLUTION = 96;
uint256 internal constant Q96 = 0x1000000000000000000000000;
}
+180
View File
@@ -0,0 +1,180 @@
// 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 "../interfaces/IPoolManager.sol";
import "../interfaces/ICurrency.sol";
import "../interfaces/IHooks.sol";
import "../libraries/LiquidityMath.sol";
import "../errors/Errors.sol";
library LiquidityProviderV4 {
using SafeERC20 for IERC20;
using CurrencyLibrary for Currency;
using LiquidityMath for int24;
struct Config {
uint160 sqrtPriceX96A;
uint160 sqrtPriceX96B;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
}
struct LiquidityParams {
PoolKey poolKey;
int24 tickLower;
int24 tickUpper;
uint128 liquidity;
int24 tickSpacing;
address token0;
address token1;
Currency currency0;
Currency currency1;
}
/// @notice Create and initialize a V4 pool, calculate liquidity parameters
/// @param poolManager The Uniswap V4 PoolManager address
/// @param tokenA First token address
/// @param tokenB Second token address (should be WETH)
/// @param config Liquidity configuration
/// @param hookAddress Optional hook address (zero address = no hook)
/// @param tickSpacing Tick spacing for the pool
/// @param fee Fee tier for the pool (in basis points)
/// @return params Struct containing all parameters needed for modifyLiquidity
/// @dev Note: The actual modifyLiquidity call must be done via unlock() pattern in the calling contract
function provideLiquidity(
address poolManager,
address tokenA,
address tokenB,
Config memory config,
address hookAddress,
int24 tickSpacing,
uint24 fee
) internal returns (LiquidityParams memory params) {
bool tokenAIsToken0 = tokenA < tokenB;
address token0 = tokenAIsToken0 ? tokenA : tokenB;
address token1 = tokenAIsToken0 ? tokenB : tokenA;
// Approve tokens for PoolManager
IERC20(token0).approve(poolManager, type(uint256).max);
IERC20(token1).approve(poolManager, type(uint256).max);
// Create PoolKey
Currency currency0 = Currency.wrap(token0);
Currency currency1 = Currency.wrap(token1);
IHooks hooks = IHooks(hookAddress); // Can be zero address
PoolKey memory poolKey = PoolKey({
currency0: currency0,
currency1: currency1,
fee: fee,
tickSpacing: tickSpacing,
hooks: hooks
});
// Calculate initial sqrt price
uint160 sqrtPriceX96 = tokenAIsToken0
? config.sqrtPriceX96A
: config.sqrtPriceX96B;
// Initialize pool
// Note: In V4, if pool already exists, initialize will revert
// For new tokens, this should not happen
IPoolManager manager = IPoolManager(poolManager);
// V4 PoolManager.initialize() signature: initialize(PoolKey memory key, uint160 sqrtPriceX96)
// Note: No hookData parameter in initialize!
manager.initialize(poolKey, sqrtPriceX96);
// If we get here, pool was initialized successfully
// Calculate tick range - ensure ticks are aligned to tickSpacing
//
// One-sided liquidity math:
// - For token0-only (100% tokens): tickLower <= currentTick < tickUpper
// Position starts as 100% token0, converts to token1 as price increases
// - For token1-only (100% WETH): tickLower < currentTick <= tickUpper
// Position starts as 100% token1, converts to token0 as price decreases
//
// Config stores tick range assuming token is token0 (our new token)
// When token is token1, we need to invert the ticks (multiply by -1)
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
if (tokenAIsToken0) {
// Token is token0 (token < WETH)
// Use config values directly - for token0-only liquidity:
// tickLower <= currentTick < tickUpper
// At start: position is 100% token0 (our tokens)
// As buyers buy: price rises, tokens convert to WETH
tickLower = int24(config.tickLower);
tickUpper = int24(config.tickUpper);
amount0Desired = config.amount0Desired; // 1B tokens
amount1Desired = config.amount1Desired; // 0 WETH
} else {
// Token is token1 (token > WETH)
// WETH is now token0, our token is token1
// Invert tick range: multiply by -1 and swap
// If config has (-207300, 887220), we use (-887220, 207300)
// This keeps the liquidity at current price with token1-only
tickLower = -int24(config.tickUpper); // -887220
tickUpper = -int24(config.tickLower); // 207300
amount0Desired = config.amount1Desired; // 0 WETH (now token0)
amount1Desired = config.amount0Desired; // 1B tokens (now token1)
}
// Align to tick spacing
tickLower = (tickLower / tickSpacing) * tickSpacing;
tickUpper = ((tickUpper + tickSpacing - 1) / tickSpacing) * tickSpacing;
// Sanity check
if (tickLower >= tickUpper) revert InvalidTickRange();
// Calculate sqrt prices for tick boundaries
uint160 sqrtPriceAX96 = LiquidityMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtPriceBX96 = LiquidityMath.getSqrtRatioAtTick(tickUpper);
// Calculate liquidity delta from amounts and tick range
// For one-sided liquidity, use the appropriate function directly
uint128 liquidity;
if (amount1Desired == 0) {
// Only token0: use getLiquidityForAmount0
liquidity = LiquidityMath.getLiquidityForAmount0(sqrtPriceAX96, sqrtPriceBX96, amount0Desired);
} else if (amount0Desired == 0) {
// Only token1: use getLiquidityForAmount1
liquidity = LiquidityMath.getLiquidityForAmount1(sqrtPriceAX96, sqrtPriceBX96, amount1Desired);
} else {
// Both tokens: use getLiquidityForAmounts
liquidity = LiquidityMath.getLiquidityForAmounts(
sqrtPriceX96,
sqrtPriceAX96,
sqrtPriceBX96,
amount0Desired,
amount1Desired
);
}
// Return all parameters needed for modifyLiquidity
// The Factory will call unlock() and handle modifyLiquidity in unlockCallback
params = LiquidityParams({
poolKey: poolKey,
tickLower: tickLower,
tickUpper: tickUpper,
liquidity: liquidity,
tickSpacing: tickSpacing,
token0: token0,
token1: token1,
currency0: currency0,
currency1: currency1
});
return params;
}
}
+112
View File
@@ -0,0 +1,112 @@
// ~ https://Qian.ag ~
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "../../interfaces/IPoolManager.sol";
import "../../interfaces/IHooks.sol";
import "../../errors/Errors.sol";
import { Hooks } from "./Hooks.sol";
/**
* @title GuardHook
* @notice Restricts pool initialization to the factory. A Qian PoolKey is derivable from
* a pending deployCoin, and v4's initialize is permissionless and reverts on an
* already-initialized pool, so without this anyone can front-run the initialize and the
* launch reverts.
*
* This is the hook for launches that want no hook behaviour: every swap-time flag is
* false, so the PoolManager never calls it on a trade. The creator's tax is the pool fee.
*
* Required hook flags: 0x2000 (beforeInitialize only). That is bit 13 — the SECOND byte
* of the address, not the last. A miner matching only the final byte will never find a
* valid salt.
*/
contract GuardHook is IHooks {
/// @notice Immutable: this hook has no admin functions and no owner. Deploy the
/// factory first, then this (an arg, not msg.sender — CREATE2 deploy).
address public immutable factory;
constructor(address _factory) {
if (_factory == address(0)) revert InvalidConfig();
factory = _factory;
}
// ------------------------------------------------ hook permissions
function getHookPermissions() external pure returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: true,
afterInitialize: false,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: false,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
// ------------------------------------------------ the gate
function beforeInitialize(address sender, PoolKey calldata, uint160) external view returns (bytes4) {
if (sender != factory) revert Unauthorized();
return IHooks.beforeInitialize.selector;
}
// ------------------------------------------------ unused callbacks
// Every flag above is false, so the PoolManager never calls these. IHooks only.
function afterInitialize(address, PoolKey calldata, uint160, int24) external pure returns (bytes4) {
return IHooks.afterInitialize.selector;
}
function beforeSwap(address, PoolKey calldata, SwapParams calldata, bytes calldata)
external pure returns (bytes4, BeforeSwapDelta, uint24)
{
return (IHooks.beforeSwap.selector, BeforeSwapDelta.wrap(0), 0);
}
function afterSwap(address, PoolKey calldata, SwapParams calldata, BalanceDelta, bytes calldata)
external pure returns (bytes4, int128)
{
return (IHooks.afterSwap.selector, int128(0));
}
function beforeAddLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata)
external pure returns (bytes4)
{
return IHooks.beforeAddLiquidity.selector;
}
function afterAddLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, BalanceDelta, BalanceDelta, bytes calldata)
external pure returns (bytes4, BalanceDelta)
{
return (IHooks.afterAddLiquidity.selector, BalanceDelta.wrap(0));
}
function beforeRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata)
external pure returns (bytes4)
{
return IHooks.beforeRemoveLiquidity.selector;
}
function afterRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, BalanceDelta, BalanceDelta, bytes calldata)
external pure returns (bytes4, BalanceDelta)
{
return (IHooks.afterRemoveLiquidity.selector, BalanceDelta.wrap(0));
}
function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) {
return IHooks.beforeDonate.selector;
}
function afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) {
return IHooks.afterDonate.selector;
}
}
+26
View File
@@ -0,0 +1,26 @@
// ~ https://Qian.ag ~
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @notice Uniswap v4 hook permission flags, mirroring the canonical layout.
/// @dev Declarative only. v4 reads a hook's real permissions from the low 14 bits of its
/// address, not from this struct — if getHookPermissions() and the mined address disagree,
/// the address wins.
library Hooks {
struct Permissions {
bool beforeInitialize;
bool afterInitialize;
bool beforeAddLiquidity;
bool afterAddLiquidity;
bool beforeRemoveLiquidity;
bool afterRemoveLiquidity;
bool beforeSwap;
bool afterSwap;
bool beforeDonate;
bool afterDonate;
bool beforeSwapReturnDelta;
bool afterSwapReturnDelta;
bool afterAddLiquidityReturnDelta;
bool afterRemoveLiquidityReturnDelta;
}
}