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
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
is a per-launch parameter against a controller allowlist, each quote carries
its own liquidity tiers, and the deploy fee is exact (no dev buy).
is a per-launch parameter against a controller allowlist, and each quote carries
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
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
@@ -90,11 +93,12 @@ source-verified on https://robinhoodchain.blockscout.com.
| Contract | Address |
| --- | --- |
| QianRWAFactory | `0x5A4ecC6fb7Ab62EC69cB0902397A5EB0a3Cb16D3` |
| QianRWAFactory (V2, dev buy) | `0x04953F4322BB1b800C438a9426cDacfCD9785860` |
| QianRWAFactory V1 | `0x5A4ecC6fb7Ab62EC69cB0902397A5EB0a3Cb16D3` |
| QianRWAFeeRouter | `0xc3a89be96b8BDb1123d2Fa40f18000e3FB1A7aAf` |
| QianRWARouter | `0x8495A347f71e819dB9e5eC12c1FC64C79DbD8708` |
| QianLocker (RWA stack) | `0x38ceAf1eF6528Aa5775820696A7D14e64136a22d` |
| GuardHook (RWA stack) | `0x10bbb04ad4ef277daffeecc37a3019dc08dee000` |
| GuardHook (RWA stack) | `0x2725462a10d14d863ce9a2ec5d95895d7d65a000` |
## Layout
+7 -4
View File
@@ -3,11 +3,11 @@
"deployedAt": "2026-07-19T20:51:38.473Z",
"deployer": "0x92B92aA9fa5AefC957ab77904f23cf14d7369fA7",
"locker": "0x38ceAf1eF6528Aa5775820696A7D14e64136a22d",
"rwaFactory": "0x5A4ecC6fb7Ab62EC69cB0902397A5EB0a3Cb16D3",
"rwaFactory": "0x04953F4322BB1b800C438a9426cDacfCD9785860",
"rwaFeeRouter": "0xc3a89be96b8BDb1123d2Fa40f18000e3FB1A7aAf",
"create2Factory": "0xe744510974F2c61dDAC2290238f8CBd375AeF1f2",
"guardHook": "0x10bbb04ad4ef277daffeecc37a3019dc08dee000",
"guardHookSalt": "0xcd353772db37b52865c269b4cf6643eb600565237ae1dbc6ae0bf1f57b2cfaec",
"guardHook": "0x2725462a10d14d863ce9a2ec5d95895d7d65a000",
"guardHookSalt": "0x3c5b0ba1f92f0e2c2c4e15a4789e7b6ead500fe74fcfd4e93f43a893d62a1759",
"tiersUsd": [
1900,
3700,
@@ -27,5 +27,8 @@
"META": "0xc0D6457C16Cc70d6790Dd43521C899C87ce02f35",
"AMD": "0x86923f96303D656E4aa86D9d42D1e57ad2023fdC",
"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 "./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,