Add QianPay: payment-link settlement
This commit is contained in:
@@ -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 {}
|
||||
}
|
||||
Reference in New Issue
Block a user