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,7 @@
|
|||||||
|
node_modules/
|
||||||
|
artifacts/
|
||||||
|
cache/
|
||||||
|
typechain-types/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.key
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Qian ([email protected])
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Qian V2 Contracts (Ethereum)
|
||||||
|
|
||||||
|
Qian is a multichain DEX and launchpad. You launch a token, its supply goes
|
||||||
|
straight into a locked Uniswap position, and anyone can trade it in plain ETH.
|
||||||
|
MIT licensed. No proxies, and no admin key over user funds.
|
||||||
|
|
||||||
|
The core contracts carry the same addresses on every chain (Robinhood Chain,
|
||||||
|
Base, Ethereum), because each one was deployed from a fresh key at nonce 0. This
|
||||||
|
repo is the Ethereum copy (chainId 1); the addresses below hold
|
||||||
|
on all three.
|
||||||
|
|
||||||
|
## Deployed addresses
|
||||||
|
|
||||||
|
| Contract | Address |
|
||||||
|
| --- | --- |
|
||||||
|
| QianLocker | `0x73Da198D58fFD43B5754a2d72D989cab1c9aF0ce` |
|
||||||
|
| QianFactoryV3 | `0xaC83b491b76Dd686db516c0fE9152320c83618a7` |
|
||||||
|
| QianFactoryV4 | `0x750d0bc66baa12CF1710a8ba939B97DB4aD005c3` |
|
||||||
|
| QianFeeRouter | `0xDe598E5F0b428B38EFaA6e887458FEF70bFA9436` |
|
||||||
|
| QianSwapRouter | `0xEE540d2496cfFDf17E29a04bcd7b6dc795C61d66` |
|
||||||
|
| Create2Factory | `0x175ae2c5EC7edaD3eE88e892C677bBDE815A2803` |
|
||||||
|
| GuardHook | `0xa27D4eef437Ab754074c8C9E6bCbC55184dE2000` |
|
||||||
|
|
||||||
|
Every address above is source-verified on https://etherscan.io.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- **Two factories.** `QianFactoryV3` and `QianFactoryV4` mint a fixed
|
||||||
|
1,000,000,000-supply ERC20 and seed the whole supply as one-sided liquidity in
|
||||||
|
a Uniswap V3 or V4 pool.
|
||||||
|
- **QianLocker** takes ownership of that LP position at launch. The contract is
|
||||||
|
immutable and exposes no withdrawal path, so the liquidity stays where it is.
|
||||||
|
- **Creator fees.** `QianFeeRouter.collectFees(token)` is a permissionless call
|
||||||
|
that routes a token's accrued trading fees to its creator. The receiver lives
|
||||||
|
in the locker, and the controller can reassign it for a community takeover. The
|
||||||
|
token's own `creator` field stays fixed as the record of who launched it.
|
||||||
|
- **Creator tax** is the Uniswap V4 pool fee, anywhere from 0.5% to 20%, picked
|
||||||
|
at launch. It lives in the PoolKey, so it never changes after that. V3 pools
|
||||||
|
use the fixed 1% tier.
|
||||||
|
- **QianSwapRouter** handles user trades: buy or sell any Qian token for plain
|
||||||
|
ETH, with a 1% platform fee on the WETH leg. It keeps no balances between
|
||||||
|
trades.
|
||||||
|
|
||||||
|
## Token
|
||||||
|
|
||||||
|
A launched token is ERC20 plus Burnable, and little beyond that. `owner()`
|
||||||
|
returns the zero address, so it reads as renounced. There is no mint, pause,
|
||||||
|
blacklist, or transfer hook. It stores immutable `platform` and `creator`
|
||||||
|
records, a `MAX_SUPPLY`, and its IPFS metadata through `metadata()` and
|
||||||
|
`tokenURI()`.
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
`GuardHook` is the only hook we deploy. It limits pool initialization to the
|
||||||
|
factory, which stops anyone from front-running a launch and bricking it. At trade
|
||||||
|
time it does nothing: every swap-time flag reads false, so the Uniswap V4
|
||||||
|
PoolManager never calls it on a swap. Qian launches ship with no fee, tax, or
|
||||||
|
anti-snipe hook.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `dtm-v3/contracts`: Uniswap V3 factory stack (`QianFactoryV3`, `QianToken`, LP provider)
|
||||||
|
- `dtm-v4/contracts`: Uniswap V4 factory stack, the shared `QianLocker`,
|
||||||
|
`QianFeeRouter`, and `QianSwapRouter`, plus `GuardHook`
|
||||||
|
- `chains/`: chain config and deployed addresses
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Solidity 0.8.24 with the optimizer on: 200 runs for the V3 tree, 1 run for the V4
|
||||||
|
tree, `viaIR` enabled. Each contract's exact verified build lives on https://etherscan.io;
|
||||||
|
this repo is the plain-source mirror.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"chain": "ethereum",
|
||||||
|
"chainId": 1,
|
||||||
|
"deployedAt": "2026-07-16",
|
||||||
|
"deployer": "0x92B92aA9fa5AefC957ab77904f23cf14d7369fA7",
|
||||||
|
"locker": "0x73Da198D58fFD43B5754a2d72D989cab1c9aF0ce",
|
||||||
|
"v3Factory": "0xaC83b491b76Dd686db516c0fE9152320c83618a7",
|
||||||
|
"v4Factory": "0x750d0bc66baa12CF1710a8ba939B97DB4aD005c3",
|
||||||
|
"feeRouter": "0xDe598E5F0b428B38EFaA6e887458FEF70bFA9436",
|
||||||
|
"tradeRouter": "0xEE540d2496cfFDf17E29a04bcd7b6dc795C61d66",
|
||||||
|
"create2Factory": "0x175ae2c5EC7edaD3eE88e892C677bBDE815A2803",
|
||||||
|
"guardHook": "0xa27D4eef437Ab754074c8C9E6bCbC55184dE2000",
|
||||||
|
"guardHookSalt": "0x8a0fc6c7de3df9ae23a30404bfcbbf9097787ab6eac5a487ad90bda54064c16c"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "Ethereum",
|
||||||
|
"slug": "ethereum",
|
||||||
|
"chainId": 1,
|
||||||
|
"rpcUrl": "https://eth.llamarpc.com",
|
||||||
|
"wethAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||||
|
"uniswap": {
|
||||||
|
"v3Factory": "0x1F98431c8aD98523631AE4a59f267346ea31F984",
|
||||||
|
"v3PositionManager": "0xC36442b4a4522E871399CD717aBDD847Ab11FE88",
|
||||||
|
"swapRouter02": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45",
|
||||||
|
"quoterV2": "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
|
||||||
|
"v4PoolManager": "0x000000000004444c5dc75cB358380D2e3dE08A90",
|
||||||
|
"v4PositionManager": "0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e",
|
||||||
|
"v4Quoter": "0x52F0E24D1c21C8A0cB1e5a5dD6198556BD9E1203",
|
||||||
|
"v4StateView": "0x7fFE42C4a5DEeA5b0feC41C94C136Cf115597227",
|
||||||
|
"universalRouter": "0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af",
|
||||||
|
"permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
||||||
|
"multicall": "0xcA11bde05977b3631167028862bE2a173976CA11"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Qian.ag
|
||||||
|
pragma solidity ^0.8.24;
|
||||||
|
|
||||||
|
interface INonfungiblePositionManager {
|
||||||
|
struct MintParams {
|
||||||
|
address token0;
|
||||||
|
address token1;
|
||||||
|
uint24 fee;
|
||||||
|
int24 tickLower;
|
||||||
|
int24 tickUpper;
|
||||||
|
uint256 amount0Desired;
|
||||||
|
uint256 amount1Desired;
|
||||||
|
uint256 amount0Min;
|
||||||
|
uint256 amount1Min;
|
||||||
|
address recipient;
|
||||||
|
uint256 deadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CollectParams {
|
||||||
|
uint256 tokenId;
|
||||||
|
address recipient;
|
||||||
|
uint128 amount0Max;
|
||||||
|
uint128 amount1Max;
|
||||||
|
}
|
||||||
|
|
||||||
|
function factory() external view returns (address);
|
||||||
|
function WETH9() external view returns (address);
|
||||||
|
|
||||||
|
function positions(uint256 tokenId) external view returns (
|
||||||
|
uint96 nonce,
|
||||||
|
address operator,
|
||||||
|
address token0,
|
||||||
|
address token1,
|
||||||
|
uint24 fee,
|
||||||
|
int24 tickLower,
|
||||||
|
int24 tickUpper,
|
||||||
|
uint128 liquidity,
|
||||||
|
uint256 feeGrowthInside0LastX128,
|
||||||
|
uint256 feeGrowthInside1LastX128,
|
||||||
|
uint128 tokensOwed0,
|
||||||
|
uint128 tokensOwed1
|
||||||
|
);
|
||||||
|
|
||||||
|
function createAndInitializePoolIfNecessary(
|
||||||
|
address token0,
|
||||||
|
address token1,
|
||||||
|
uint24 fee,
|
||||||
|
uint160 sqrtPriceX96
|
||||||
|
) external returns (address pool);
|
||||||
|
|
||||||
|
function mint(MintParams calldata params) external returns (
|
||||||
|
uint256 tokenId,
|
||||||
|
uint128 liquidity,
|
||||||
|
uint256 amount0,
|
||||||
|
uint256 amount1
|
||||||
|
);
|
||||||
|
|
||||||
|
function collect(CollectParams calldata params) external payable returns (
|
||||||
|
uint256 amount0,
|
||||||
|
uint256 amount1
|
||||||
|
);
|
||||||
|
|
||||||
|
function setApprovalForAll(address operator, bool approved) external;
|
||||||
|
function safeTransferFrom(address from, address to, uint256 tokenId) external;
|
||||||
|
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
|
||||||
|
function isApprovedForAll(address owner, address operator) external view returns (bool);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Qian.ag
|
||||||
|
pragma solidity ^0.8.24;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import "./INonfungiblePositionManager.sol";
|
||||||
|
|
||||||
|
error InvalidTickRange();
|
||||||
|
error PoolPriceHijacked();
|
||||||
|
|
||||||
|
interface IUniswapV3PoolSlot0 {
|
||||||
|
function slot0() external view returns (
|
||||||
|
uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality,
|
||||||
|
uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
library LiquidityProvider {
|
||||||
|
struct Config {
|
||||||
|
uint160 sqrtPriceX96A;
|
||||||
|
uint160 sqrtPriceX96B;
|
||||||
|
int24 tickLower;
|
||||||
|
int24 tickUpper;
|
||||||
|
uint256 amount0Desired;
|
||||||
|
uint256 amount1Desired;
|
||||||
|
}
|
||||||
|
|
||||||
|
function provideLiquidity(
|
||||||
|
address positionManager,
|
||||||
|
address tokenA,
|
||||||
|
address tokenB,
|
||||||
|
Config memory config
|
||||||
|
) internal returns (uint256 tokenId) {
|
||||||
|
bool tokenAIsToken0 = tokenA < tokenB;
|
||||||
|
|
||||||
|
address token0 = tokenAIsToken0 ? tokenA : tokenB;
|
||||||
|
address token1 = tokenAIsToken0 ? tokenB : tokenA;
|
||||||
|
|
||||||
|
IERC20(token0).approve(positionManager, type(uint256).max);
|
||||||
|
IERC20(token1).approve(positionManager, type(uint256).max);
|
||||||
|
|
||||||
|
INonfungiblePositionManager manager = INonfungiblePositionManager(positionManager);
|
||||||
|
|
||||||
|
uint160 sqrtPriceX96 = tokenAIsToken0
|
||||||
|
? config.sqrtPriceX96A
|
||||||
|
: config.sqrtPriceX96B;
|
||||||
|
|
||||||
|
int24 tickLower;
|
||||||
|
int24 tickUpper;
|
||||||
|
if (tokenAIsToken0) {
|
||||||
|
tickLower = -int24(config.tickLower);
|
||||||
|
tickUpper = int24(config.tickUpper);
|
||||||
|
} else {
|
||||||
|
tickLower = -int24(config.tickUpper);
|
||||||
|
tickUpper = int24(config.tickLower);
|
||||||
|
}
|
||||||
|
if (tickLower >= tickUpper) revert InvalidTickRange();
|
||||||
|
|
||||||
|
uint256 amount0Desired = tokenAIsToken0 ? config.amount0Desired : config.amount1Desired;
|
||||||
|
uint256 amount1Desired = tokenAIsToken0 ? config.amount1Desired : config.amount0Desired;
|
||||||
|
|
||||||
|
// "IfNecessary" never reverts on an existing pool, so a front-run init at a WRONG
|
||||||
|
// price would silently skew the mint + dev buy. Assert the live price; a same-price
|
||||||
|
// front-run is harmless (the attacker just created our pool for us).
|
||||||
|
address pool = manager.createAndInitializePoolIfNecessary(token0, token1, 10000, sqrtPriceX96);
|
||||||
|
(uint160 livePrice, , , , , , ) = IUniswapV3PoolSlot0(pool).slot0();
|
||||||
|
if (livePrice != sqrtPriceX96) revert PoolPriceHijacked();
|
||||||
|
|
||||||
|
(tokenId, , , ) = manager.mint(
|
||||||
|
INonfungiblePositionManager.MintParams({
|
||||||
|
token0: token0,
|
||||||
|
token1: token1,
|
||||||
|
fee: 10000,
|
||||||
|
tickLower: tickLower,
|
||||||
|
tickUpper: tickUpper,
|
||||||
|
amount0Desired: amount0Desired,
|
||||||
|
amount1Desired: amount1Desired,
|
||||||
|
amount0Min: 0,
|
||||||
|
amount1Min: 0,
|
||||||
|
recipient: address(this),
|
||||||
|
deadline: block.timestamp
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Qian.ag
|
||||||
|
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 "./LiquidityProvider.sol";
|
||||||
|
import "./INonfungiblePositionManager.sol";
|
||||||
|
import "./QianToken.sol";
|
||||||
|
|
||||||
|
interface ISwapRouter02 {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
error BadRouterConfig();
|
||||||
|
error DeployDisabled();
|
||||||
|
error NewCreatorZero();
|
||||||
|
error NotController();
|
||||||
|
error Unauthorized();
|
||||||
|
error InsufficientDeployFee();
|
||||||
|
error DeployFeeTooHigh();
|
||||||
|
error InvalidConfig();
|
||||||
|
error TransferFailed();
|
||||||
|
|
||||||
|
interface IWETH {
|
||||||
|
function deposit() external payable;
|
||||||
|
function withdraw(uint256 amount) external;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IBindingLocker {
|
||||||
|
function setCreator(address token, address newCreator) external;
|
||||||
|
function creatorOf(address token) external view returns (address);
|
||||||
|
}
|
||||||
|
|
||||||
|
contract QianFactoryV3 is ReentrancyGuard {
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
event ERC20TokenCreated(address tokenAddress);
|
||||||
|
event DeployCoinToggled(bool enabled);
|
||||||
|
event DeployFeeUpdated(uint256 oldFee, uint256 newFee);
|
||||||
|
event CreatorChanged(address indexed token, address indexed oldCreator, address indexed newCreator);
|
||||||
|
event ControllerTransferInitiated(address indexed currentController, address indexed pendingController);
|
||||||
|
event ControllerTransferred(address indexed oldController, address indexed newController);
|
||||||
|
|
||||||
|
struct TokenInfo {
|
||||||
|
address tokenAddress;
|
||||||
|
string name;
|
||||||
|
string symbol;
|
||||||
|
address deployer;
|
||||||
|
uint256 time;
|
||||||
|
string metadata;
|
||||||
|
uint256 nftId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Indexer read surface. The by-index list and tokenToNFTId mapping were
|
||||||
|
/// deleted — the token itself and the locker's binding carry the same data.
|
||||||
|
mapping(address => TokenInfo) public tokenInfoByAddress;
|
||||||
|
uint256 public tokenCount = 0;
|
||||||
|
address public platformController;
|
||||||
|
/// @dev platformController is both the admin and the fee sink (there is no
|
||||||
|
/// setFeeVault), so it must be transferable. Two-step, mirroring QianFactoryV4.
|
||||||
|
address public pendingController;
|
||||||
|
bool public deployCoinEnabled = false;
|
||||||
|
uint256 public deployFee = 0.005 ether;
|
||||||
|
|
||||||
|
/// @notice Live receiver of a token's 70% creator fees, read by QianFeeRouter.
|
||||||
|
/// 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 IBindingLocker(locker).creatorOf(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiquidityConfig {
|
||||||
|
uint160 sqrtPriceX96A;
|
||||||
|
uint160 sqrtPriceX96B;
|
||||||
|
int24 tickLower;
|
||||||
|
int24 tickUpper;
|
||||||
|
uint256 amount0Desired;
|
||||||
|
uint256 amount1Desired;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping(uint256 => LiquidityConfig) public liquidityConfigs;
|
||||||
|
uint256 public liquidityConfigCount = 0;
|
||||||
|
|
||||||
|
address public immutable positionManager;
|
||||||
|
address public immutable weth;
|
||||||
|
address public immutable swapRouter;
|
||||||
|
address public immutable locker;
|
||||||
|
|
||||||
|
// dev buy pays the same 1% QianSwapRouter charges on every other trade
|
||||||
|
uint16 public constant DEV_BUY_FEE_BPS = 100;
|
||||||
|
uint256 private constant FULL_TOKEN_SUPPLY = 1_000_000_000 * 10 ** 18;
|
||||||
|
|
||||||
|
constructor(address _positionManager, address _swapRouter, address _weth, address _locker) {
|
||||||
|
if (_positionManager == address(0) || _swapRouter == address(0) || _weth == address(0) || _locker == address(0)) {
|
||||||
|
revert BadRouterConfig();
|
||||||
|
}
|
||||||
|
platformController = msg.sender;
|
||||||
|
positionManager = _positionManager;
|
||||||
|
swapRouter = _swapRouter;
|
||||||
|
weth = _weth;
|
||||||
|
locker = _locker;
|
||||||
|
|
||||||
|
liquidityConfigs[0] = LiquidityConfig({
|
||||||
|
sqrtPriceX96A: 2_097_222_954_085_974_151_159_020,
|
||||||
|
sqrtPriceX96B: 2_993_054_087_624_369_706_988_840_588_130_593,
|
||||||
|
tickLower: 210800,
|
||||||
|
tickUpper: 310800,
|
||||||
|
amount0Desired: FULL_TOKEN_SUPPLY,
|
||||||
|
amount1Desired: 0
|
||||||
|
});
|
||||||
|
liquidityConfigCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// @param maxDeployFee caller's ceiling on deployFee, so a controller fee change
|
||||||
|
/// cannot front-run this tx
|
||||||
|
function deployCoin(string memory _name, string memory _symbol, string memory _metadata, bytes32 salt, uint256 configId, uint256 maxDeployFee) public payable nonReentrant returns (uint256 tokensReceived) {
|
||||||
|
if (!deployCoinEnabled) revert DeployDisabled();
|
||||||
|
if (configId >= liquidityConfigCount) revert InvalidConfig();
|
||||||
|
if (deployFee > maxDeployFee) revert DeployFeeTooHigh();
|
||||||
|
if (msg.value < deployFee) revert InsufficientDeployFee();
|
||||||
|
|
||||||
|
// dev buy pays the same 1% the swap router takes on every other trade;
|
||||||
|
// it ships with the deploy fee as a single transfer (no 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 coin_address = address(t);
|
||||||
|
|
||||||
|
uint256 nftId = provideLiquidity(coin_address, weth, configId, msg.sender);
|
||||||
|
|
||||||
|
uint256 platformTake = deployFee + devBuyFee;
|
||||||
|
if (platformTake > 0) {
|
||||||
|
(bool success, ) = platformController.call{value: platformTake}("");
|
||||||
|
if (!success) revert TransferFailed();
|
||||||
|
}
|
||||||
|
|
||||||
|
tokensReceived = 0;
|
||||||
|
|
||||||
|
if (remainingValue > 0) {
|
||||||
|
ISwapRouter02(swapRouter).exactInputSingle{ value: remainingValue }(
|
||||||
|
ISwapRouter02.ExactInputSingleParams({
|
||||||
|
tokenIn: weth,
|
||||||
|
tokenOut: coin_address,
|
||||||
|
fee: 10000,
|
||||||
|
recipient: address(this),
|
||||||
|
amountIn: remainingValue,
|
||||||
|
amountOutMinimum: 0,
|
||||||
|
sqrtPriceLimitX96: 0
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
IERC20 token = IERC20(coin_address);
|
||||||
|
tokensReceived = token.balanceOf(address(this));
|
||||||
|
token.transfer(msg.sender, tokensReceived);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenInfoByAddress[coin_address] = TokenInfo({
|
||||||
|
tokenAddress: coin_address,
|
||||||
|
name: _name,
|
||||||
|
symbol: _symbol,
|
||||||
|
deployer: msg.sender,
|
||||||
|
time: block.timestamp,
|
||||||
|
metadata: _metadata,
|
||||||
|
nftId: nftId
|
||||||
|
});
|
||||||
|
tokenCount++;
|
||||||
|
|
||||||
|
return tokensReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// No withdrawFees and no receive(): nothing accumulates here, so nothing needs a
|
||||||
|
// sweep. deployCoin is payable and takes msg.value directly; the dev buy and the fee
|
||||||
|
// transfer are outbound.
|
||||||
|
|
||||||
|
function toggleDeployCoin() external {
|
||||||
|
if (msg.sender != platformController) revert NotController();
|
||||||
|
deployCoinEnabled = !deployCoinEnabled;
|
||||||
|
emit DeployCoinToggled(deployCoinEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDeployFee(uint256 _deployFee) external {
|
||||||
|
if (msg.sender != platformController) revert NotController();
|
||||||
|
uint256 oldFee = deployFee;
|
||||||
|
deployFee = _deployFee;
|
||||||
|
emit DeployFeeUpdated(oldFee, _deployFee);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLiquidityConfig(
|
||||||
|
uint160 _sqrtPriceX96A,
|
||||||
|
uint160 _sqrtPriceX96B,
|
||||||
|
int24 _tickLower,
|
||||||
|
int24 _tickUpper,
|
||||||
|
uint256 _amount0Desired,
|
||||||
|
uint256 _amount1Desired
|
||||||
|
) external returns (uint256 configId) {
|
||||||
|
if (msg.sender != platformController) revert NotController();
|
||||||
|
if (_tickLower <= 0 || _tickUpper <= 0 || _tickLower >= _tickUpper) revert InvalidTickRange();
|
||||||
|
|
||||||
|
configId = liquidityConfigCount;
|
||||||
|
liquidityConfigs[configId] = LiquidityConfig({
|
||||||
|
sqrtPriceX96A: _sqrtPriceX96A,
|
||||||
|
sqrtPriceX96B: _sqrtPriceX96B,
|
||||||
|
tickLower: _tickLower,
|
||||||
|
tickUpper: _tickUpper,
|
||||||
|
amount0Desired: _amount0Desired,
|
||||||
|
amount1Desired: _amount1Desired
|
||||||
|
});
|
||||||
|
liquidityConfigCount++;
|
||||||
|
|
||||||
|
return configId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLiquidityConfig(
|
||||||
|
uint256 _configId,
|
||||||
|
uint160 _sqrtPriceX96A,
|
||||||
|
uint160 _sqrtPriceX96B,
|
||||||
|
int24 _tickLower,
|
||||||
|
int24 _tickUpper,
|
||||||
|
uint256 _amount0Desired,
|
||||||
|
uint256 _amount1Desired
|
||||||
|
) external {
|
||||||
|
if (msg.sender != platformController) revert NotController();
|
||||||
|
if (_configId >= liquidityConfigCount) revert InvalidConfig();
|
||||||
|
if (_tickLower <= 0 || _tickUpper <= 0 || _tickLower >= _tickUpper) revert InvalidTickRange();
|
||||||
|
|
||||||
|
liquidityConfigs[_configId] = LiquidityConfig({
|
||||||
|
sqrtPriceX96A: _sqrtPriceX96A,
|
||||||
|
sqrtPriceX96B: _sqrtPriceX96B,
|
||||||
|
tickLower: _tickLower,
|
||||||
|
tickUpper: _tickUpper,
|
||||||
|
amount0Desired: _amount0Desired,
|
||||||
|
amount1Desired: _amount1Desired
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function provideLiquidity(address tokenA, address tokenB, uint256 configId, address creator) internal returns (uint256 tokenId) {
|
||||||
|
LiquidityConfig memory config = liquidityConfigs[configId];
|
||||||
|
|
||||||
|
LiquidityProvider.Config memory libConfig = LiquidityProvider.Config({
|
||||||
|
sqrtPriceX96A: config.sqrtPriceX96A,
|
||||||
|
sqrtPriceX96B: config.sqrtPriceX96B,
|
||||||
|
tickLower: config.tickLower,
|
||||||
|
tickUpper: config.tickUpper,
|
||||||
|
amount0Desired: config.amount0Desired,
|
||||||
|
amount1Desired: config.amount1Desired
|
||||||
|
});
|
||||||
|
|
||||||
|
tokenId = LiquidityProvider.provideLiquidity(
|
||||||
|
positionManager,
|
||||||
|
tokenA,
|
||||||
|
tokenB,
|
||||||
|
libConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
// Position NFT goes straight into the locker, carrying the (token, creator)
|
||||||
|
// binding in the transfer data — the locker records who this position belongs
|
||||||
|
// to in the same call that permanently locks it.
|
||||||
|
INonfungiblePositionManager(positionManager).safeTransferFrom(
|
||||||
|
address(this), locker, tokenId, abi.encode(tokenA, creator)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fee collection now lives in QianFeeRouter.collectFees(token) — permissionless,
|
||||||
|
// fixed 30% treasury / 70% creator split, served by the locker.
|
||||||
|
|
||||||
|
/// @notice Begin handing over the controller role, and with it the fee sink.
|
||||||
|
/// @dev Two-step: the new controller must accept, so a typo cannot strand this
|
||||||
|
/// factory's admin and revenue on an address nobody holds.
|
||||||
|
function transferController(address newController) external {
|
||||||
|
if (msg.sender != platformController) revert NotController();
|
||||||
|
if (newController == address(0)) revert NewCreatorZero();
|
||||||
|
pendingController = newController;
|
||||||
|
emit ControllerTransferInitiated(platformController, newController);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Complete the handover. Callable only by the pending controller.
|
||||||
|
function acceptController() external {
|
||||||
|
if (msg.sender != pendingController) revert Unauthorized();
|
||||||
|
address oldController = platformController;
|
||||||
|
platformController = pendingController;
|
||||||
|
pendingController = address(0);
|
||||||
|
emit ControllerTransferred(oldController, platformController);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @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 NewCreatorZero();
|
||||||
|
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);
|
||||||
|
IBindingLocker(locker).setCreator(tokenAddress, newCreator);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
pragma solidity ^0.8.20;
|
||||||
|
// BondingCurveLibrary
|
||||||
|
|
||||||
|
string constant ERR_BONDING_CURVE_LIBRARY_INVALID_AMOUNT_OUT =
|
||||||
|
"BondingCurveLibrary : ERR_BONDING_CURVE_LIBRARY_INVALID_AMOUNT_OUT";
|
||||||
|
string constant ERR_BONDING_CURVE_LIBRARY_INVALID_AMOUNT_IN =
|
||||||
|
"BondingCurveLibrary : ERR_BONDING_CURVE_LIBRARY_INVALID_AMOUNT_IN";
|
||||||
|
string constant ERR_BONDING_CURVE_LIBRARY_INSUFFICIENT_LIQUIDITY =
|
||||||
|
"BondingCurveLibrary : ERR_BONDING_CURVE_LIBRARY_INSUFFICIENT_LIQUIDITY";
|
||||||
|
string constant ERR_BONDING_CURVE_LIBRARY_INVALID_INPUTS =
|
||||||
|
"BondingCurveLibrary : ERR_BONDING_CURVE_LIBRARY_INVALID_INPUTS";
|
||||||
|
//Transfer Helper
|
||||||
|
string constant ERR_TRANSFER_NATIVE_FAILED = "ERR_TRANSFER_NATIVE_FAILED";
|
||||||
|
|
||||||
|
//BondingCurve
|
||||||
|
string constant ERR_BONDING_CURVE_ONLY_OWNER = "BondingCurve : ERR_BONDING_CURVE_ONLY_OWNER";
|
||||||
|
string constant ERR_BONDING_CURVE_ONLY_FACTORY = "BondingCurve : ERR_BONDING_CURVE_ONLY_FACTORY";
|
||||||
|
string constant ERR_BONDING_CURVE_FINISHED = "BondingCurve : ERR_BONDING_CURVE_FINISHED";
|
||||||
|
string constant ERR_BONDING_CURVE_INVALID_AMOUNT_OUT = "BondingCurve : ERR_BONDING_CURVE_INVALID_AMOUNT_OUT";
|
||||||
|
string constant ERR_BONDING_CURVE_INVALID_AMOUNT_IN = "BondingCurve : ERR_BONDING_CURVE_INVALID_AMOUNT_IN";
|
||||||
|
string constant ERR_BONDING_CURVE_OVERFLOW_TARGET = "BondingCurve : ERR_BONDING_CURVE_OVERFLOW_TARGET";
|
||||||
|
string constant ERR_BONDING_CURVE_INVALID_TO = "BondingCurve : ERR_BONDING_CURVE_INVALID_TO";
|
||||||
|
string constant ERR_BONDING_CURVE_INVALID_K = "BondingCurve : ERR_BONDING_CURVE_INVALID_K";
|
||||||
|
string constant ERR_BONDING_CURVE_INSUFFICIENT_RESERVE = "BondingCurve : ERR_BONDING_CURVE_INSUFFICIENT_RESERVE";
|
||||||
|
string constant ERR_BONDING_CURVE_ONLY_LOCK = "BondingCurve : ERR_LISTING_ONLY_LOCK";
|
||||||
|
string constant ERR_BONDING_CURVE_LOCKED = "BondingCurve : ERR_BONDING_CURVE_LOCKED";
|
||||||
|
string constant ERR_BONDING_CURVE_ALREADY_LISTED = "BondingCurve : ERR_BONDING_CURVE_ALREADY_LISTED";
|
||||||
|
string constant ERR_BONDING_CURVE_MUST_LISTING = "BondingCurve : ERR_BONDING_CURVE_MUST_LISTING";
|
||||||
|
//BondingCurveFactory
|
||||||
|
string constant ERR_BONDING_CURVE_FACTORY_ONLY_OWNER = "BondingCurveFactory : ERR_BONDING_CURVE_FACTORY_ONLY_OWNER";
|
||||||
|
string constant ERR_BONDING_CURVE_FACTORY_ONLY_CORE = "BondingCurveFactory : ERR_BONDING_CURVE_FACTORY_ONLY_CORE";
|
||||||
|
|
||||||
|
//CORE
|
||||||
|
string constant ERR_CORE_EXPIRED = "Core : ERR_CORE_EXPIRED";
|
||||||
|
string constant ERR_CORE_INVALID_FEE = "Core : ERR_CORE_INVALID_FEE";
|
||||||
|
string constant ERR_CORE_INVALID_SEND_NATIVE = "Core : ERR_CORE_INVALID_SEND_NATIVE";
|
||||||
|
string constant ERR_CORE_INVALID_AMOUNT_IN = "Core : ERR_CORE_INVALID_AMOUNT_IN";
|
||||||
|
string constant ERR_CORE_INVALID_AMOUNT_OUT = "Core : ERR_CORE_INVALID_AMOUNT_OUT";
|
||||||
|
string constant ERR_CORE_INVALID_ALLOWANCE = "Core : ERR_CORE_INVALID_ALLOWANCE";
|
||||||
|
string constant ERR_CORE_INVALID_AMOUNT_IN_MAX = "Core : ERR_CORE_INVALID_AMOUNT_IN_MAX";
|
||||||
|
string constant ERR_CORE_ALREADY_INITIALIZED = "Core : ERR_CORE_ALREADY_INITIALIZED";
|
||||||
|
|
||||||
|
//LOCK
|
||||||
|
string constant ERR_LOCK_ONLY_OWNER = "Lock : ERR_LOCK_ONLY_OWNER";
|
||||||
|
string constant ERR_LOCK_INVALID_AMOUNT_IN = "Lock : ERR_LOCK_INVALID_AMOUNT_IN";
|
||||||
|
string constant ERR_TOKEN_NOT_LOCKED = "Lock : ERR_TOKEN_NOT_LOCKED";
|
||||||
|
string constant ERR_INVALID_ACCOUNT = "Lock : ERR_INVALID_ACCOUNT";
|
||||||
|
string constant ERR_INVALID_TOKEN_ADDRESS = "Lock : ERR_INVALID_TOKEN_ADDRESS";
|
||||||
|
// MINT PARTY
|
||||||
|
string constant ERR_MINT_PARTY_ONLY_OWNER = "MintParty : ERR_MINT_PARTY_ONLY_OWNER";
|
||||||
|
string constant ERR_MINT_PARTY_ONLY_FACTORY = "MintParty : ERR_MINT_PARTY_ONLY_FACTORY";
|
||||||
|
string constant ERR_MINT_PARTY_FINISHED = "MintParty : ERR_MINT_PARTY_FINISHED";
|
||||||
|
string constant ERR_MINT_PARTY_INVALID_FUNDING_AMOUNT = "MintParty : ERR_MINT_PARTY_INVALID_FUNDING_AMOUNT";
|
||||||
|
string constant ERR_MINT_PARTY_INVALID_WHITE_LIST = "MintParty : ERR_MINT_PARTY_INVALID_WHITE_LIST";
|
||||||
|
string constant ERR_MINT_PARTY_INVALID_WHITELIST = "MintParty : ERR_MINT_PARTY_INVALID_WHITELIST";
|
||||||
|
string constant ERR_MINT_PARTY_WITHDRAW_AMOUNT_IS_ZERO = "MintParty : ERR_MINT_PARTY_WITHDRAW_AMOUNT_IS_ZERO";
|
||||||
|
string constant ERR_MINT_PARTY_ALREADY_DEPOSITED = "MintParty : ERR_MINT_PARTY_ALREADY_DEPOSITED";
|
||||||
|
string constant ERR_MINT_PARTY_BALANCE_ZERO = "MintParty : ERR_MINT_PARTY_BALANCE_ZERO";
|
||||||
|
string constant ERR_MINT_PARTY_WHITELIST_ALREADY_ADDED = "MintParty : ERR_MINT_PARTY_WHITELIST_ALREADY_ADDED";
|
||||||
|
|
||||||
|
//MINT_PARTY Factory
|
||||||
|
string constant ERR_MINT_PARTY_FACTORY_NOT_FINISHED = "MintPartyFactory : ERR_MINT_PARTY_FACTORY_NOT_FINISHED";
|
||||||
|
string constant ERR_MINT_PARTY_FACTORY_INVALID_FUNDING_AMOUNT =
|
||||||
|
"MintPartyFactory : ERR_MINT_PARTY_FACTORY_INVALID_FUNDING_AMOUNT";
|
||||||
|
string constant ERR_MINT_PARTY_FACTORY_INVALID_MAXIMUM_WHITELIST =
|
||||||
|
"MintPartyFactory : ERR_MINT_PARTY_FACTORY_INVALID_MAXIMUM_WHITELIST";
|
||||||
|
string constant ERR_MINT_PARTY_FACTORY_ONLY_OWNER = "MintPartyFactory : ERR_MINT_PARTY_FACTORY_ONLY_OWNER";
|
||||||
|
|
||||||
|
//Token
|
||||||
|
string constant ERR_TOKEN_ONLY_FACTORY = "Token : ERR_TOKEN_ONLY_FACTORY";
|
||||||
|
string constant ERR_TOKEN_ONLY_ONCE_MINT = "Token : ERR_TOKEN_ONLY_ONCE_MINT";
|
||||||
|
string constant ERR_TOKEN_INVALID_EXPIRED = "Token : ERR_TOKEN_INVALID_EXPIRED";
|
||||||
|
string constant ERR_TOKEN_INVALID_SIGNATURE = "Token : ERR_TOKEN_INVALID_SIGNATURE";
|
||||||
|
|
||||||
|
//FeeVault
|
||||||
|
string constant ERR_FEE_VAULT_INVALID_WNATIVE_ADDRESS = "FeeVault : ERR_FEE_VAULT_INVALID_WNATIVE_ADDRESS";
|
||||||
|
string constant ERR_FEE_VAULT_INVALID_OWNER = "FeeVault : ERR_FEE_VAULT_INVALID_OWNER";
|
||||||
|
string constant ERR_FEE_VAULT_NOT_OWNER = "FeeVault : ERR_FEE_VAULT_NOT_OWNER";
|
||||||
|
string constant ERR_FEE_VAULT_INVALID_RECEIVER = "FeeVault : ERR_FEE_VAULT_INVALID_RECEIVER";
|
||||||
|
string constant ERR_FEE_VAULT_INVALID_AMOUNT = "FeeVault : ERR_FEE_VAULT_INVALID_AMOUNT";
|
||||||
|
string constant ERR_FEE_VAULT_INSUFFICIENT_BALANCE = "FeeVault : ERR_FEE_VAULT_INSUFFICIENT_BALANCE";
|
||||||
|
string constant ERR_FEE_VAULT_TRANSFER_FAILED = "FeeVault : ERR_FEE_VAULT_TRANSFER_FAILED";
|
||||||
|
string constant ERR_FEE_VAULT_REENTRANT = "FeeVault : ERR_FEE_VAULT_REENTRANT";
|
||||||
|
|
||||||
|
//DexRouter
|
||||||
|
|
||||||
|
string constant ERR_DEX_ROUTER_EXPIRED = "DexRouter : ERR_DEX_ROUTER_EXPIRED";
|
||||||
|
string constant ERR_DEX_ROUTER_INVALID_SEND_NATIVE = "DexRouter : ERR_DEX_ROUTER_INVALID_SEND_NATIVE";
|
||||||
|
string constant ERR_DEX_ROUTER_INVALID_AMOUNT_IN = "DexRouter : ERR_DEX_ROUTER_INVALID_AMOUNT_IN";
|
||||||
|
string constant ERR_DEX_ROUTER_INVALID_FEE = "DexRouter : ERR_DEX_ROUTER_INVALID_FEE";
|
||||||
|
string constant ERR_DEX_ROUTER_INVALID_ALLOWANCE = "DexRouter : ERR_DEX_ROUTER_INVALID_ALLOWANCE";
|
||||||
|
string constant ERR_DEX_ROUTER_INVALID_AMOUNT_OUT_MIN = "DexRouter : ERR_DEX_ROUTER_INVALID_AMOUNT_OUT_MIN";
|
||||||
|
string constant ERR_DEX_ROUTER_INVALID_AMOUNT_IN_MAX = "DexRouter : ERR_DEX_ROUTER_INVALID_AMOUNT_IN_MAX";
|
||||||
@@ -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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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