Qian on Arc Testnet
This commit is contained in:
@@ -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,60 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="https://qian.ag"><img src="https://qian.ag/branding/qian-glyph-2048.png" alt="Qian" width="120"></a>
|
||||||
|
|
||||||
|
# Qian Contracts (Arc Testnet)
|
||||||
|
|
||||||
|
**[qian.ag](https://qian.ag)** · [Explorer](https://testnet.arcscan.app) · [License: MIT](LICENSE)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Qian is a multichain exchange and payments layer for every asset. This repo
|
||||||
|
holds what Qian runs on [Arc](https://arc.io), Circle's stablecoin-native L1,
|
||||||
|
during its public testnet: QianPay, the payment-settlement contract behind
|
||||||
|
[qian.ag/pay](https://qian.ag/pay). MIT licensed. No proxies, and no admin key
|
||||||
|
over user funds.
|
||||||
|
|
||||||
|
Gas on Arc is USDC, so a payment settles without the payer ever holding a
|
||||||
|
separate gas token. The rest of the Qian stack follows when Arc reaches
|
||||||
|
mainnet.
|
||||||
|
|
||||||
|
## Deployed addresses
|
||||||
|
|
||||||
|
| Contract | Address |
|
||||||
|
| --- | --- |
|
||||||
|
| QianPay | `0x73Da198D58fFD43B5754a2d72D989cab1c9aF0ce` |
|
||||||
|
|
||||||
|
Source-verified on https://testnet.arcscan.app.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- **QianPay** settles payment links: pay a fixed USDC amount straight to a
|
||||||
|
wallet. Stateless, no admin functions, 0.5% platform fee on the USDC side.
|
||||||
|
On chains with Uniswap V3 it also accepts any token a V3 path can turn into
|
||||||
|
USDC; Arc testnet has no canonical Uniswap deployment yet, so the
|
||||||
|
`swapRouter02` constructor slot holds an inert placeholder and only
|
||||||
|
`payUSDC` is active. Same source as the
|
||||||
|
[Base deployment](https://github.com/QianExchange/base-contracts).
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `dtm-v4/contracts`: `QianPay` and the interfaces it calls
|
||||||
|
- `chains/`: chain config and the deployed address
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Solidity 0.8.24 with the optimizer on (1 run, `viaIR` enabled). The exact
|
||||||
|
verified build lives on https://testnet.arcscan.app; this repo is the
|
||||||
|
plain-source mirror.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
OpenZeppelin (SafeERC20, ReentrancyGuard) and the Uniswap V3 router interface,
|
||||||
|
kept in-tree under `dtm-v4/contracts/interfaces`. No forked or vendored AMM
|
||||||
|
code.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
See [SECURITY.md](SECURITY.md) for the security model, the design tradeoffs, and how to report a vulnerability.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT. Contact: dev@qian.ag
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
Email dev@qian.ag with the details and a proof of concept if you have one. Report it to us first and give us time to ship a fix before you disclose it.
|
||||||
|
|
||||||
|
## Security model
|
||||||
|
|
||||||
|
- **Immutable locker.** `QianLocker` holds the launch LP position and has no withdrawal function. Nobody can pull the liquidity.
|
||||||
|
- **No proxies or upgrades.** Every contract ships as final bytecode. The controller can repoint the treasury, fee sink, and hook whitelist, but no one can swap the contract logic.
|
||||||
|
- **Renounced tokens.** A launched token's `owner()` is the zero address. It has no mint, pause, blacklist, or transfer hook.
|
||||||
|
- **Permissionless fees.** Anyone can call `QianFeeRouter.collectFees(token)`. It pays the creator and treasury on a fixed split and moves no user funds.
|
||||||
|
- **Launch integrity.** `GuardHook` lets only the factory initialize a pool, so nobody can front-run a launch and brick it. It runs no trade-time logic.
|
||||||
|
|
||||||
|
## Accepted design tradeoffs
|
||||||
|
|
||||||
|
- **Liquidity stays locked forever.** There is no unlock, migration, or emergency withdrawal path anywhere in the code.
|
||||||
|
- **No launch-time trading protections.** No dead window, wallet cap, or anti-snipe tax. Every swap-time hook flag reads false.
|
||||||
|
- **Fee liquidation has no slippage floor.** `collectFees` swaps the platform's token-side share to WETH in the same call with no minimum-out. The amounts are tiny, and only the treasury's own revenue is exposed, never user or creator funds.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Contracts under `dtm-v3/contracts` and `dtm-v4/contracts`, deployed on Robinhood Chain, Base, and Ethereum. We verify every core contract's source on each chain's block explorer.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"chain": "arc-testnet",
|
||||||
|
"chainId": 5042002,
|
||||||
|
"qianPay": "0x73Da198D58fFD43B5754a2d72D989cab1c9aF0ce",
|
||||||
|
"qianPayDeployer": "0x92B92aA9fa5AefC957ab77904f23cf14d7369fA7",
|
||||||
|
"qianPayTreasury": "0x92B92aA9fa5AefC957ab77904f23cf14d7369fA7",
|
||||||
|
"qianPaySwapRouter02": "0x3600000000000000000000000000000000000000",
|
||||||
|
"qianPaySwapRouter02Placeholder": true,
|
||||||
|
"qianPayBlock": 52409927,
|
||||||
|
"qianPayTx": "0xfad468b076cdb2bad4cdf26317c34fe3f71dcb97962887ed993d80254e984b70",
|
||||||
|
"qianPayDeployedAt": "2026-07-18"
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "Arc Testnet",
|
||||||
|
"slug": "arc-testnet",
|
||||||
|
"chainId": 5042002,
|
||||||
|
"rpcUrl": "https://rpc.testnet.arc.network",
|
||||||
|
"usdc": "0x3600000000000000000000000000000000000000",
|
||||||
|
"usdcNote": "ERC20 interface, 6 decimals. Native gas token is ALSO USDC but at 18 decimals precision (Arc-specific) - do not confuse the two.",
|
||||||
|
"swapRouter02": "0x3600000000000000000000000000000000000000",
|
||||||
|
"swapRouter02Placeholder": true,
|
||||||
|
"swapRouter02Note": "No canonical Uniswap V3 SwapRouter02 published in Arc testnet docs (docs.arc.io/arc/references/contract-addresses lists only USDC/Permit2/Multicall3/CREATE2Factory). Set to the USDC address itself as a deliberate inert placeholder: nonzero (passes QianPay's constructor zero-check), has no exactOutput function, so payWithToken always reverts loudly. payUSDC is unaffected.",
|
||||||
|
"explorerApi": "https://testnet.arcscan.app/api/v2/smart-contracts",
|
||||||
|
"explorerUrl": "https://testnet.arcscan.app",
|
||||||
|
"faucet": "https://faucet.circle.com"
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// ~ 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/IWETH.sol";
|
||||||
|
import "./errors/Errors.sol";
|
||||||
|
|
||||||
|
interface ISwapRouter02 {
|
||||||
|
struct ExactOutputParams {
|
||||||
|
bytes path;
|
||||||
|
address recipient;
|
||||||
|
uint256 amountOut;
|
||||||
|
uint256 amountInMaximum;
|
||||||
|
}
|
||||||
|
function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Stateless settlement for Qian payment links. Payer settles in USDC
|
||||||
|
/// directly, in any ERC20 via an exact-output Uniswap V3 swap, or in plain ETH
|
||||||
|
/// (Base only, by usage — no chain guard in code). FEE_BPS of the USDC amount
|
||||||
|
/// goes to treasury on every path; the rest goes straight to recipient. No
|
||||||
|
/// balances are ever held here between transactions.
|
||||||
|
contract QianPay is ReentrancyGuard {
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
event Paid(bytes32 indexed intentRef, address indexed recipient, address payer, address tokenIn, uint256 usdcOut);
|
||||||
|
|
||||||
|
uint256 public constant FEE_BPS = 50; // 0.5% of the USDC amount, every path
|
||||||
|
|
||||||
|
IERC20 public immutable USDC;
|
||||||
|
address public immutable TREASURY;
|
||||||
|
address public immutable SWAP_ROUTER02;
|
||||||
|
|
||||||
|
constructor(address usdc, address treasury, address swapRouter02) {
|
||||||
|
if (usdc == address(0) || treasury == address(0) || swapRouter02 == address(0)) revert InvalidConfig();
|
||||||
|
USDC = IERC20(usdc);
|
||||||
|
TREASURY = treasury;
|
||||||
|
SWAP_ROUTER02 = swapRouter02;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pay directly in USDC.
|
||||||
|
function payUSDC(bytes32 intentRef, address recipient, uint256 amount) external nonReentrant {
|
||||||
|
if (recipient == address(0)) revert InvalidReceiver();
|
||||||
|
if (amount == 0) revert InvalidConfig();
|
||||||
|
|
||||||
|
USDC.safeTransferFrom(msg.sender, address(this), amount);
|
||||||
|
_settle(intentRef, recipient, address(USDC), amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pay in any ERC20 via an exact-output V3 swap to USDC. Pulls up to
|
||||||
|
/// `maxIn` of `tokenIn`, refunds whatever the swap didn't spend (balance-diff,
|
||||||
|
/// fee-on-transfer safe).
|
||||||
|
function payWithToken(bytes32 intentRef, address recipient, uint256 usdcAmount, address tokenIn, uint256 maxIn, bytes calldata path) external nonReentrant {
|
||||||
|
if (recipient == address(0)) revert InvalidReceiver();
|
||||||
|
if (usdcAmount == 0 || maxIn == 0) revert InvalidConfig();
|
||||||
|
|
||||||
|
uint256 balBefore = IERC20(tokenIn).balanceOf(address(this));
|
||||||
|
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), maxIn);
|
||||||
|
uint256 pulled = IERC20(tokenIn).balanceOf(address(this)) - balBefore;
|
||||||
|
|
||||||
|
IERC20(tokenIn).forceApprove(SWAP_ROUTER02, pulled);
|
||||||
|
uint256 spent = ISwapRouter02(SWAP_ROUTER02).exactOutput(
|
||||||
|
ISwapRouter02.ExactOutputParams({
|
||||||
|
path: path,
|
||||||
|
recipient: address(this),
|
||||||
|
amountOut: usdcAmount,
|
||||||
|
amountInMaximum: pulled
|
||||||
|
})
|
||||||
|
);
|
||||||
|
IERC20(tokenIn).forceApprove(SWAP_ROUTER02, 0);
|
||||||
|
|
||||||
|
if (pulled > spent) {
|
||||||
|
IERC20(tokenIn).safeTransfer(msg.sender, pulled - spent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_settle(intentRef, recipient, tokenIn, usdcAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pay in plain ETH via an exact-output V3 swap to USDC. Base only
|
||||||
|
/// by usage (no chain guard in code). Wraps msg.value to WETH — the WETH
|
||||||
|
/// address is read off the tail of `path` (the tokenIn of an exact-output
|
||||||
|
/// path is its last 20 bytes) — then swaps and refunds unspent ETH.
|
||||||
|
function payETH(bytes32 intentRef, address recipient, uint256 usdcAmount, bytes calldata path) external payable nonReentrant {
|
||||||
|
if (recipient == address(0)) revert InvalidReceiver();
|
||||||
|
if (usdcAmount == 0 || msg.value == 0) revert InvalidConfig();
|
||||||
|
if (path.length < 20) revert InvalidConfig();
|
||||||
|
|
||||||
|
address weth = _lastToken(path);
|
||||||
|
IWETH(weth).deposit{value: msg.value}();
|
||||||
|
IERC20(weth).forceApprove(SWAP_ROUTER02, msg.value);
|
||||||
|
uint256 spent = ISwapRouter02(SWAP_ROUTER02).exactOutput(
|
||||||
|
ISwapRouter02.ExactOutputParams({
|
||||||
|
path: path,
|
||||||
|
recipient: address(this),
|
||||||
|
amountOut: usdcAmount,
|
||||||
|
amountInMaximum: msg.value
|
||||||
|
})
|
||||||
|
);
|
||||||
|
IERC20(weth).forceApprove(SWAP_ROUTER02, 0);
|
||||||
|
|
||||||
|
if (msg.value > spent) {
|
||||||
|
uint256 refund = msg.value - spent;
|
||||||
|
IWETH(weth).withdraw(refund);
|
||||||
|
(bool ok, ) = msg.sender.call{value: refund}("");
|
||||||
|
if (!ok) revert TransferFailed();
|
||||||
|
}
|
||||||
|
|
||||||
|
_settle(intentRef, recipient, weth, usdcAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev FEE_BPS of usdcAmount to treasury, remainder to recipient, emit Paid.
|
||||||
|
/// Assumes this contract already holds >= usdcAmount of USDC.
|
||||||
|
function _settle(bytes32 intentRef, address recipient, address tokenIn, uint256 usdcAmount) private {
|
||||||
|
uint256 fee = (usdcAmount * FEE_BPS) / 10_000;
|
||||||
|
USDC.safeTransfer(TREASURY, fee);
|
||||||
|
USDC.safeTransfer(recipient, usdcAmount - fee);
|
||||||
|
emit Paid(intentRef, recipient, msg.sender, tokenIn, usdcAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev Exact-output V3 paths encode tokenIn as the last 20 bytes.
|
||||||
|
function _lastToken(bytes calldata path) private pure returns (address token) {
|
||||||
|
assembly {
|
||||||
|
token := shr(96, calldataload(add(path.offset, sub(path.length, 20))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev Expected source: this contract's own WETH.withdraw() during a payETH
|
||||||
|
/// refund. Not enforced — no weth immutable exists to check msg.sender against
|
||||||
|
/// (constructor signature is fixed; WETH varies per-call via `path`), so stray
|
||||||
|
/// direct ETH sends are accepted and unrecoverable. Deliberate trade-off.
|
||||||
|
receive() external payable {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// 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();
|
||||||
|
error Expired();
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user