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
+8 -4
View File
@@ -72,8 +72,11 @@ underlying asset stays with its issuer.
- **QianRWAFactory** launches tokens paired to an approved quote asset (a - **QianRWAFactory** launches tokens paired to an approved quote asset (a
tokenized stock or USDG) instead of WETH. The launch works like the core tokenized stock or USDG) instead of WETH. The launch works like the core
factories: full supply into a locked one-sided Uniswap V4 position. The quote factories: full supply into a locked one-sided Uniswap V4 position. The quote
is a per-launch parameter against a controller allowlist, each quote carries is a per-launch parameter against a controller allowlist, and each quote carries
its own liquidity tiers, and the deploy fee is exact (no dev buy). its own liquidity tiers. ETH sent above the deploy fee becomes a same-transaction
dev buy: the factory routes it through `QianRWARouter` along the caller's path,
bounded by the caller's `devBuyMinOut`, and appends the new pool itself so the
final hop cannot be spoofed. Nobody can trade the token before its creator.
- **QianRWAFeeRouter** splits a quote-paired token's accrued trading fees and - **QianRWAFeeRouter** splits a quote-paired token's accrued trading fees and
pays them out as plain ERC20 transfers of the quote. A transfer the recipient pays them out as plain ERC20 transfers of the quote. A transfer the recipient
cannot take, for example when a stock issuer blocklists the address, accrues cannot take, for example when a stock issuer blocklists the address, accrues
@@ -90,11 +93,12 @@ source-verified on https://robinhoodchain.blockscout.com.
| Contract | Address | | Contract | Address |
| --- | --- | | --- | --- |
| QianRWAFactory | `0x5A4ecC6fb7Ab62EC69cB0902397A5EB0a3Cb16D3` | | QianRWAFactory (V2, dev buy) | `0x04953F4322BB1b800C438a9426cDacfCD9785860` |
| QianRWAFactory V1 | `0x5A4ecC6fb7Ab62EC69cB0902397A5EB0a3Cb16D3` |
| QianRWAFeeRouter | `0xc3a89be96b8BDb1123d2Fa40f18000e3FB1A7aAf` | | QianRWAFeeRouter | `0xc3a89be96b8BDb1123d2Fa40f18000e3FB1A7aAf` |
| QianRWARouter | `0x8495A347f71e819dB9e5eC12c1FC64C79DbD8708` | | QianRWARouter | `0x8495A347f71e819dB9e5eC12c1FC64C79DbD8708` |
| QianLocker (RWA stack) | `0x38ceAf1eF6528Aa5775820696A7D14e64136a22d` | | QianLocker (RWA stack) | `0x38ceAf1eF6528Aa5775820696A7D14e64136a22d` |
| GuardHook (RWA stack) | `0x10bbb04ad4ef277daffeecc37a3019dc08dee000` | | GuardHook (RWA stack) | `0x2725462a10d14d863ce9a2ec5d95895d7d65a000` |
## Layout ## Layout
+7 -4
View File
@@ -3,11 +3,11 @@
"deployedAt": "2026-07-19T20:51:38.473Z", "deployedAt": "2026-07-19T20:51:38.473Z",
"deployer": "0x92B92aA9fa5AefC957ab77904f23cf14d7369fA7", "deployer": "0x92B92aA9fa5AefC957ab77904f23cf14d7369fA7",
"locker": "0x38ceAf1eF6528Aa5775820696A7D14e64136a22d", "locker": "0x38ceAf1eF6528Aa5775820696A7D14e64136a22d",
"rwaFactory": "0x5A4ecC6fb7Ab62EC69cB0902397A5EB0a3Cb16D3", "rwaFactory": "0x04953F4322BB1b800C438a9426cDacfCD9785860",
"rwaFeeRouter": "0xc3a89be96b8BDb1123d2Fa40f18000e3FB1A7aAf", "rwaFeeRouter": "0xc3a89be96b8BDb1123d2Fa40f18000e3FB1A7aAf",
"create2Factory": "0xe744510974F2c61dDAC2290238f8CBd375AeF1f2", "create2Factory": "0xe744510974F2c61dDAC2290238f8CBd375AeF1f2",
"guardHook": "0x10bbb04ad4ef277daffeecc37a3019dc08dee000", "guardHook": "0x2725462a10d14d863ce9a2ec5d95895d7d65a000",
"guardHookSalt": "0xcd353772db37b52865c269b4cf6643eb600565237ae1dbc6ae0bf1f57b2cfaec", "guardHookSalt": "0x3c5b0ba1f92f0e2c2c4e15a4789e7b6ead500fe74fcfd4e93f43a893d62a1759",
"tiersUsd": [ "tiersUsd": [
1900, 1900,
3700, 3700,
@@ -27,5 +27,8 @@
"META": "0xc0D6457C16Cc70d6790Dd43521C899C87ce02f35", "META": "0xc0D6457C16Cc70d6790Dd43521C899C87ce02f35",
"AMD": "0x86923f96303D656E4aa86D9d42D1e57ad2023fdC", "AMD": "0x86923f96303D656E4aa86D9d42D1e57ad2023fdC",
"USDG": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168" "USDG": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
} },
"rwaFactoryV1": "0x5A4ecC6fb7Ab62EC69cB0902397A5EB0a3Cb16D3",
"guardHookV1": "0x10bbb04ad4ef277daffeecc37a3019dc08dee000",
"factoryV2Note": "V2 2026-07-19: same-tx dev buy via router V2 (devBuyPath/devBuyMinOut); V1 factory still authorized in the locker"
} }
+28 -8
View File
@@ -14,13 +14,19 @@ import "./interfaces/IConfigurableHook.sol";
import "./errors/Errors.sol"; import "./errors/Errors.sol";
import "./QianLocker.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 { contract QianRWAFactory is ReentrancyGuard {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
using LiquidityProviderV4 for LiquidityProviderV4.Config; using LiquidityProviderV4 for LiquidityProviderV4.Config;
using CurrencyLibrary for Currency; using CurrencyLibrary for Currency;
event ERC20TokenCreated(address tokenAddress); 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 QuotePaired(address indexed token, address indexed quote);
event QuoteApprovalChanged(address indexed quote, bool approved); event QuoteApprovalChanged(address indexed quote, bool approved);
event DeployFeeUpdated(uint256 oldFee, uint256 newFee); event DeployFeeUpdated(uint256 oldFee, uint256 newFee);
@@ -67,17 +73,19 @@ contract QianRWAFactory is ReentrancyGuard {
address public immutable poolManager; address public immutable poolManager;
address public immutable locker; address public immutable locker;
address public immutable swapRouter;
int24 private constant TICK_SPACING = 60; int24 private constant TICK_SPACING = 60;
uint256 private constant FULL_TOKEN_SUPPLY = 1_000_000_000 * 10 ** 18; uint256 private constant FULL_TOKEN_SUPPLY = 1_000_000_000 * 10 ** 18;
uint24 public constant MIN_POOL_FEE = 5000; uint24 public constant MIN_POOL_FEE = 5000;
uint24 public constant MAX_POOL_FEE = 200000; uint24 public constant MAX_POOL_FEE = 200000;
constructor(address _poolManager, address _locker) { constructor(address _poolManager, address _locker, address _swapRouter) {
if (_poolManager == address(0) || _locker == address(0)) revert BadRouterConfig(); if (_poolManager == address(0) || _locker == address(0) || _swapRouter == address(0)) revert BadRouterConfig();
platformController = msg.sender; platformController = msg.sender;
feeVault = msg.sender; feeVault = msg.sender;
poolManager = _poolManager; poolManager = _poolManager;
locker = _locker; locker = _locker;
swapRouter = _swapRouter;
} }
function deployCoin( function deployCoin(
@@ -90,15 +98,17 @@ contract QianRWAFactory is ReentrancyGuard {
address hookAddress, address hookAddress,
bytes calldata hookConfig, bytes calldata hookConfig,
uint256 maxDeployFee, uint256 maxDeployFee,
uint24 poolFee uint24 poolFee,
PoolKey[] calldata devBuyPath,
uint256 devBuyMinOut
) public payable nonReentrant returns (address tokenAddress) { ) public payable nonReentrant returns (address tokenAddress) {
if (!deployCoinEnabled) revert DeployDisabled(); if (!deployCoinEnabled) revert DeployDisabled();
if (!approvedQuotes[quote]) revert InvalidConfig(); if (!approvedQuotes[quote]) revert InvalidConfig();
if (configId >= liquidityConfigCount[quote]) revert InvalidConfig(); if (configId >= liquidityConfigCount[quote]) revert InvalidConfig();
if (deployFee > maxDeployFee) revert DeployFeeTooHigh(); if (deployFee > maxDeployFee) revert DeployFeeTooHigh();
if (poolFee < MIN_POOL_FEE || poolFee > MAX_POOL_FEE) revert InvalidPoolFee(); if (poolFee < MIN_POOL_FEE || poolFee > MAX_POOL_FEE) revert InvalidPoolFee();
// no dev buy on quote pairs: exact fee only, paid in raw ETH // ETH above deployFee is the dev buy, swapped through the router below
if (msg.value != deployFee) revert InsufficientDeployFee(); if (msg.value < deployFee) revert InsufficientDeployFee();
address hook = hookAddress == address(0) ? guardHook : hookAddress; address hook = hookAddress == address(0) ? guardHook : hookAddress;
if (!approvedHooks[hook]) revert HookNotApproved(); if (!approvedHooks[hook]) revert HookNotApproved();
{ {
@@ -118,10 +128,20 @@ contract QianRWAFactory is ReentrancyGuard {
IConfigurableHook(hook).configureHook(poolId, tokenAddress, hookConfig); IConfigurableHook(hook).configureHook(poolId, tokenAddress, hookConfig);
emit HookConfigured(tokenAddress, hook, poolId); emit HookConfigured(tokenAddress, hook, poolId);
} }
if (msg.value > 0 && feeVault != address(0)) { if (deployFee > 0 && feeVault != address(0)) {
(bool ok, ) = feeVault.call{value: msg.value}(""); (bool ok, ) = feeVault.call{value: deployFee}("");
if (!ok) revert TransferFailed(); 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({ tokenConfigs[tokenAddress] = TokenConfig({
deployTime: block.timestamp, deployTime: block.timestamp,
metadata: _metadata, metadata: _metadata,