QianRWAFactory V2: same-transaction dev buy through the router

ETH above the deploy fee swaps WETH -> USDG -> quote -> new pool inside the
launch transaction, floored by the caller's devBuyMinOut. The factory appends
its own pool as the final hop. Fresh GuardHook; V1 factory stays authorized.
This commit is contained in:
Qian
2026-07-19 22:09:34 +00:00
parent 297962041e
commit df03c1186e
3 changed files with 43 additions and 16 deletions
+28 -8
View File
@@ -14,13 +14,19 @@ import "./interfaces/IConfigurableHook.sol";
import "./errors/Errors.sol";
import "./QianLocker.sol";
/// @notice Launches tokens against an allowlisted quote asset (tokenized stocks, stablecoins) instead of WETH; no dev buy.
interface IQianRWARouter {
function buy(PoolKey[] memory path, address token, uint256 minTokensOut, uint256 deadline) external payable returns (uint256);
}
/// @notice Launches tokens against an allowlisted quote asset (tokenized stocks, stablecoins) instead of WETH.
/// @dev V2: optional same-tx dev buy — ETH above deployFee routes through the swap router along the caller's path.
contract QianRWAFactory is ReentrancyGuard {
using SafeERC20 for IERC20;
using LiquidityProviderV4 for LiquidityProviderV4.Config;
using CurrencyLibrary for Currency;
event ERC20TokenCreated(address tokenAddress);
event DevBuy(address indexed token, address indexed buyer, uint256 ethIn, uint256 tokensOut);
event QuotePaired(address indexed token, address indexed quote);
event QuoteApprovalChanged(address indexed quote, bool approved);
event DeployFeeUpdated(uint256 oldFee, uint256 newFee);
@@ -67,17 +73,19 @@ contract QianRWAFactory is ReentrancyGuard {
address public immutable poolManager;
address public immutable locker;
address public immutable swapRouter;
int24 private constant TICK_SPACING = 60;
uint256 private constant FULL_TOKEN_SUPPLY = 1_000_000_000 * 10 ** 18;
uint24 public constant MIN_POOL_FEE = 5000;
uint24 public constant MAX_POOL_FEE = 200000;
constructor(address _poolManager, address _locker) {
if (_poolManager == address(0) || _locker == address(0)) revert BadRouterConfig();
constructor(address _poolManager, address _locker, address _swapRouter) {
if (_poolManager == address(0) || _locker == address(0) || _swapRouter == address(0)) revert BadRouterConfig();
platformController = msg.sender;
feeVault = msg.sender;
poolManager = _poolManager;
locker = _locker;
swapRouter = _swapRouter;
}
function deployCoin(
@@ -90,15 +98,17 @@ contract QianRWAFactory is ReentrancyGuard {
address hookAddress,
bytes calldata hookConfig,
uint256 maxDeployFee,
uint24 poolFee
uint24 poolFee,
PoolKey[] calldata devBuyPath,
uint256 devBuyMinOut
) public payable nonReentrant returns (address tokenAddress) {
if (!deployCoinEnabled) revert DeployDisabled();
if (!approvedQuotes[quote]) revert InvalidConfig();
if (configId >= liquidityConfigCount[quote]) revert InvalidConfig();
if (deployFee > maxDeployFee) revert DeployFeeTooHigh();
if (poolFee < MIN_POOL_FEE || poolFee > MAX_POOL_FEE) revert InvalidPoolFee();
// no dev buy on quote pairs: exact fee only, paid in raw ETH
if (msg.value != deployFee) revert InsufficientDeployFee();
// ETH above deployFee is the dev buy, swapped through the router below
if (msg.value < deployFee) revert InsufficientDeployFee();
address hook = hookAddress == address(0) ? guardHook : hookAddress;
if (!approvedHooks[hook]) revert HookNotApproved();
{
@@ -118,10 +128,20 @@ contract QianRWAFactory is ReentrancyGuard {
IConfigurableHook(hook).configureHook(poolId, tokenAddress, hookConfig);
emit HookConfigured(tokenAddress, hook, poolId);
}
if (msg.value > 0 && feeVault != address(0)) {
(bool ok, ) = feeVault.call{value: msg.value}("");
if (deployFee > 0 && feeVault != address(0)) {
(bool ok, ) = feeVault.call{value: deployFee}("");
if (!ok) revert TransferFailed();
}
uint256 devBuyEth = msg.value - deployFee;
if (devBuyEth > 0) {
// caller's prefix ends at the quote; the real pool is appended so the final hop can't be spoofed
PoolKey[] memory path = new PoolKey[](devBuyPath.length + 1);
for (uint256 i = 0; i < devBuyPath.length; i++) path[i] = devBuyPath[i];
path[devBuyPath.length] = tokenToPoolKey[tokenAddress];
uint256 bought = IQianRWARouter(swapRouter).buy{value: devBuyEth}(path, tokenAddress, devBuyMinOut, block.timestamp);
IERC20(tokenAddress).safeTransfer(msg.sender, bought);
emit DevBuy(tokenAddress, msg.sender, devBuyEth, bought);
}
tokenConfigs[tokenAddress] = TokenConfig({
deployTime: block.timestamp,
metadata: _metadata,