QianRWARouter
0x8495a347f71e819db9e5ec12c1fc64c79dbd8708
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
5,342 bytes
ABI entries
27
6 read · 7 write
License
none
Contract information
- Address
- 0x8495a347f71e819db9e5ec12c1fc64c79dbd8708
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x92B92aA9fa…14d7369fA7
- Creation tx
- 0xb5d9a61a10…554a416266
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (6)
FEE_BPS() → uint256
owner() → address
pendingOwner() → address
poolManager() → address
treasury() → address
weth() → address
Events (4)
BoughtSoldSwappedTreasuryUpdated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_weth",
"type": "address"
},
{
"internalType": "address",
"name": "_treasury",
"type": "address"
},
{
"internalType": "address",
"name": "_poolManager",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "Expired",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientOutput",
"type": "error"
},
{
"inputs": [],
"name": "InvalidConfig",
"type": "error"
},
{
"inputs": [],
"name": "InvalidReceiver",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "TransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "Unauthorized",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "buyer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "fee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
}
],
"name": "Bought",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "seller",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "fee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"name": "Sold",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "tokenIn",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "tokenOut",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "trader",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "fee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
}
],
"name": "Swapped",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "oldTreasury",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newTreasury",
"type": "address"
}
],
"name": "TreasuryUpdated",
"type": "event"
},
{
"inputs": [],
"name": "FEE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey[]",
"name": "path",
"type": "tuple[]"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "minTokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "buy",
"outputs": [
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey[]",
"name": "path",
"type": "tuple[]"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokensIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minEthOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "sell",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newTreasury",
"type": "address"
}
],
"name": "setTreasury",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey[]",
"name": "path",
"type": "tuple[]"
},
{
"internalType": "address",
"name": "tokenIn",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "address",
"name": "tokenOut",
"type": "address"
},
{
"internalType": "uint256",
"name": "minOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swap",
"outputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// ~ https://Qian.ag ~ Real-World Asset Router ~
// 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/IPoolManager.sol";
import "./interfaces/ICurrency.sol";
import "./interfaces/IWETH.sol";
import "./errors/Errors.sol";
/// @notice Buy/sell freely-transferable RWA tokens (e.g. tokenized stocks) with plain ETH, routed
/// exact-in through a caller-supplied Uniswap V4 PoolKey path (WETH -> ... -> token). Holds no
/// balances between transactions; the untrusted path can only revert or return a fill minOut rejects.
contract QianRWARouter is ReentrancyGuard {
using SafeERC20 for IERC20;
event Bought(address indexed token, address indexed buyer, uint256 ethIn, uint256 fee, uint256 tokensOut);
event Sold(address indexed token, address indexed seller, uint256 tokensIn, uint256 fee, uint256 ethOut);
event Swapped(address indexed tokenIn, address indexed tokenOut, address indexed trader, uint256 amountIn, uint256 fee, uint256 amountOut);
event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury);
uint256 public constant FEE_BPS = 100;
// Extreme tick bounds so each hop swaps against the full range.
uint160 internal constant MIN_SQRT_LIMIT = 4295128740;
uint160 internal constant MAX_SQRT_LIMIT = 1461446703485210103287273052203988822378723970341;
address public immutable weth;
address public immutable poolManager;
address public owner;
address public pendingOwner;
address public treasury;
constructor(address _weth, address _treasury, address _poolManager) {
if (_weth == address(0) || _treasury == address(0) || _poolManager == address(0)) {
revert InvalidConfig();
}
weth = _weth;
treasury = _treasury;
poolManager = _poolManager;
owner = msg.sender;
}
/// @notice Buy `token` with ETH along `path` (WETH -> ... -> token); reverts if the path's final
/// output isn't `token`. `deadline` rejects a stale tx held back to lock in a price.
function buy(PoolKey[] calldata path, address token, uint256 minTokensOut, uint256 deadline)
external
payable
nonReentrant
returns (uint256 tokensOut)
{
if (block.timestamp > deadline) revert Expired();
if (msg.value == 0 || path.length == 0) revert InvalidConfig();
uint256 fee = (msg.value * FEE_BPS) / 10_000;
uint256 amountIn = msg.value - fee;
IWETH(weth).deposit{value: msg.value}();
IERC20(weth).safeTransfer(treasury, fee);
(address outCurrency, uint256 outAmount) = _swapPath(path, weth, amountIn, msg.sender);
if (outCurrency != token) revert InvalidConfig();
tokensOut = outAmount;
if (tokensOut < minTokensOut) revert InsufficientOutput();
emit Bought(token, msg.sender, msg.value, fee, tokensOut);
}
/// @notice Sell `token` for ETH along `path` (token -> ... -> WETH); needs prior token approval.
/// `deadline` rejects a stale tx held back to lock in a price.
function sell(PoolKey[] calldata path, address token, uint256 tokensIn, uint256 minEthOut, uint256 deadline)
external
nonReentrant
returns (uint256 ethOut)
{
if (block.timestamp > deadline) revert Expired();
if (tokensIn == 0 || path.length == 0) revert InvalidConfig();
// Fee-on-transfer support: swap what actually arrived, not the nominal amount.
uint256 balBefore = IERC20(token).balanceOf(address(this));
IERC20(token).safeTransferFrom(msg.sender, address(this), tokensIn);
tokensIn = IERC20(token).balanceOf(address(this)) - balBefore;
if (tokensIn == 0) revert InvalidConfig();
(address outCurrency, uint256 wethOut) = _swapPath(path, token, tokensIn, address(this));
if (outCurrency != weth) revert InvalidConfig();
uint256 fee = (wethOut * FEE_BPS) / 10_000;
ethOut = wethOut - fee;
if (ethOut < minEthOut) revert InsufficientOutput();
IERC20(weth).safeTransfer(treasury, fee);
IWETH(weth).withdraw(ethOut);
(bool ok, ) = msg.sender.call{value: ethOut}("");
if (!ok) revert TransferFailed();
emit Sold(token, msg.sender, tokensIn, fee, ethOut);
}
/// @notice Swap any ERC20 for any other along `path`; 1% fee on the input side. Needs prior approval.
function swap(PoolKey[] calldata path, address tokenIn, uint256 amountIn, address tokenOut, uint256 minOut, uint256 deadline)
external
nonReentrant
returns (uint256 amountOut)
{
if (block.timestamp > deadline) revert Expired();
if (amountIn == 0 || path.length == 0 || tokenIn == tokenOut) revert InvalidConfig();
// fee-on-transfer support: swap what actually arrived, not the nominal amount
uint256 balBefore = IERC20(tokenIn).balanceOf(address(this));
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
amountIn = IERC20(tokenIn).balanceOf(address(this)) - balBefore;
if (amountIn == 0) revert InvalidConfig();
uint256 fee = (amountIn * FEE_BPS) / 10_000;
IERC20(tokenIn).safeTransfer(treasury, fee);
(address outCurrency, uint256 outAmount) = _swapPath(path, tokenIn, amountIn - fee, msg.sender);
if (outCurrency != tokenOut) revert InvalidConfig();
amountOut = outAmount;
if (amountOut < minOut) revert InsufficientOutput();
emit Swapped(tokenIn, tokenOut, msg.sender, amountIn, fee, amountOut);
}
/// @dev Open the PoolManager lock; the multi-hop runs in unlockCallback.
function _swapPath(PoolKey[] calldata path, address inputCurrency, uint256 amountIn, address recipient)
internal
returns (address outCurrency, uint256 outAmount)
{
bytes memory result = IPoolManager(poolManager).unlock(abi.encode(path, inputCurrency, amountIn, recipient));
(outCurrency, outAmount) = abi.decode(result, (address, uint256));
}
/// @notice PoolManager callback: each hop's exact output feeds the next hop's input, so
/// intermediates net to zero — only the initial input is settled and the final output taken.
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != poolManager) revert Unauthorized();
(PoolKey[] memory path, address inputCurrency, uint256 amountIn, address recipient) =
abi.decode(data, (PoolKey[], address, uint256, address));
IPoolManager manager = IPoolManager(poolManager);
address curCurrency = inputCurrency;
uint256 curAmount = amountIn;
for (uint256 i = 0; i < path.length; i++) {
PoolKey memory key = path[i];
address c0 = Currency.unwrap(key.currency0);
address c1 = Currency.unwrap(key.currency1);
bool zeroForOne;
if (curCurrency == c0) {
zeroForOne = true;
} else if (curCurrency == c1) {
zeroForOne = false;
} else {
revert InvalidConfig(); // hop's currencies don't include the last output
}
BalanceDelta delta = manager.swap(
key,
SwapParams({
zeroForOne: zeroForOne,
amountSpecified: -int256(curAmount),
sqrtPriceLimitX96: zeroForOne ? MIN_SQRT_LIMIT : MAX_SQRT_LIMIT
}),
""
);
int128 outDelta = zeroForOne ? BalanceDeltaLibrary.amount1(delta) : BalanceDeltaLibrary.amount0(delta);
if (outDelta <= 0) revert InsufficientOutput();
curAmount = uint256(int256(outDelta));
curCurrency = zeroForOne ? c1 : c0;
}
// A trade too large to fill hop 0 leaves the input under-settled and v4 reverts the whole
// tx (safe, no funds move); callers size to liquidity.
manager.sync(Currency.wrap(inputCurrency));
IERC20(inputCurrency).safeTransfer(poolManager, amountIn);
manager.settle();
manager.take(Currency.wrap(curCurrency), recipient, curAmount);
return abi.encode(curCurrency, curAmount);
}
function setTreasury(address newTreasury) external {
if (msg.sender != owner) revert Unauthorized();
if (newTreasury == address(0)) revert InvalidReceiver();
emit TreasuryUpdated(treasury, newTreasury);
treasury = newTreasury;
}
/// @notice Two-step owner handover, same shape as everywhere else in the stack.
function transferOwner(address newOwner) external {
if (msg.sender != owner) revert Unauthorized();
if (newOwner == address(0)) revert InvalidReceiver();
pendingOwner = newOwner;
}
function acceptOwner() external {
if (msg.sender != pendingOwner) revert Unauthorized();
owner = pendingOwner;
pendingOwner = address(0);
}
/// @dev Only WETH.withdraw sends ETH here.
receive() external payable {
if (msg.sender != weth) revert Unauthorized();
}
}
Chain explorer3543msChain node82ms