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