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