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:36:14 +00:00
commit 5f6db506c9
28 changed files with 2828 additions and 0 deletions
+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;
}
}