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
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