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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user