// SPDX-License-Identifier: MIT pragma solidity 0.8.28; /// @title Touchstone — an assay is a real buy and a real sell of a token in one breath, reverted, with only the mark kept on chain /// @notice Anyone pays the fee and names a pool. For native-USDC pools the contract runs ArcExitProbe (which does the round trip and /// reverts with the numbers); for pools on the ERC-20 USDC view it runs both legs through the V4Quoter. The verdict, the cost of /// the round trip and the block are written as an event and kept as the pool's last assay and streak. The fee goes to the /// revenue splitter: half to the USDC faucet pot, half into APEX for the drip vault. No withdraw, no owner over funds. interface IProbe { function probe(PoolKey calldata key, bool usdcIsCurrency0, uint256 usdcIn) external; } interface IQuoter { function quoteExactInputSingle(QuoteExactSingleParams calldata params) external returns (uint256 amountOut, uint256 gasEstimate); } struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; } struct QuoteExactSingleParams { PoolKey poolKey; bool zeroForOne; uint128 exactAmount; bytes hookData; } contract ArcAssay { error ProbeResult(uint256 usdcIn, uint256 tokensOut, uint256 usdcBack, bool buyOk, bool sellOk, bytes buyError, bytes sellError); IProbe public immutable probe; IQuoter public immutable quoter; address payable public immutable splitter; address constant USDC_ERC20 = 0x3600000000000000000000000000000000000000; uint256 public constant MAX_FEE = 0.5 ether; uint256 public fee = 0.03 ether; address public operator; uint8 public constant SELLABLE = 1; uint8 public constant CANNOT_SELL = 2; uint8 public constant CANNOT_BUY = 3; uint8 public constant NOTHING_OUT = 4; struct Record { uint64 blockNumber; uint40 time; uint8 verdict; uint8 path; uint32 lossBps; uint32 streak; uint40 okSince; } mapping(bytes32 => Record) public last; uint256 public assays; uint256 public feesTotal; event Assayed(bytes32 indexed poolId, address indexed token, address indexed payer, uint8 verdict, uint8 path, uint32 lossBps, uint256 usdcIn, uint32 streak); event FeeSet(uint256 fee); constructor(address probe_, address quoter_, address payable splitter_) { probe = IProbe(probe_); quoter = IQuoter(quoter_); splitter = splitter_; operator = msg.sender; } function setFee(uint256 f) external { require(msg.sender == operator && f <= MAX_FEE, "fee"); fee = f; emit FeeSet(f); } function setOperator(address o) external { require(msg.sender == operator && o != address(0)); operator = o; } function poolId(PoolKey calldata key) public pure returns (bytes32) { return keccak256(abi.encode(key)); } function isUsdc(address a) internal pure returns (bool) { return a == address(0) || a == USDC_ERC20; } /// @param usdcIn test size: 18 dp for native-USDC pools, 6 dp for ERC-20-USDC pools. 0 = default (0.5 USDC). function assay(PoolKey calldata key, uint256 usdcIn) external payable returns (uint8 verdict, uint32 lossBps) { require(msg.value >= fee, "fee"); bool c0 = isUsdc(key.currency0); require(c0 || isUsdc(key.currency1), "no USDC side"); address usdc = c0 ? key.currency0 : key.currency1; address token = c0 ? key.currency1 : key.currency0; uint8 path; uint256 back; bool buyOk; bool sellOk; uint256 tokensOut; if (usdc == address(0)) { path = 1; if (usdcIn == 0) usdcIn = 0.5 ether; require(usdcIn <= 5 ether, "size"); try probe.probe(key, c0, usdcIn) { revert("probe returned"); } catch (bytes memory err) { bytes4 sel; assembly { sel := mload(add(err, 32)) } if (sel == ProbeResult.selector) { assembly { let len := mload(err) err := add(err, 4) mstore(err, sub(len, 4)) } (, tokensOut, back, buyOk, sellOk, , ) = abi.decode(err, (uint256, uint256, uint256, bool, bool, bytes, bytes)); } } } else { path = 2; if (usdcIn == 0) usdcIn = 500000; require(usdcIn <= 5000000, "size"); try quoter.quoteExactInputSingle(QuoteExactSingleParams(key, c0, uint128(usdcIn), "")) returns (uint256 out, uint256) { buyOk = true; tokensOut = out; } catch {} if (buyOk && tokensOut > 0) { try quoter.quoteExactInputSingle(QuoteExactSingleParams(key, !c0, uint128(tokensOut), "")) returns (uint256 out2, uint256) { sellOk = true; back = out2; } catch {} } } verdict = !buyOk ? CANNOT_BUY : tokensOut == 0 ? NOTHING_OUT : !sellOk ? CANNOT_SELL : SELLABLE; lossBps = verdict == SELLABLE ? (back >= usdcIn ? 0 : uint32((usdcIn - back) * 10000 / usdcIn)) : 10000; bytes32 id = poolId(key); Record storage r = last[id]; uint32 streak = verdict == SELLABLE ? r.streak + 1 : 0; uint40 okSince = verdict == SELLABLE ? (r.okSince == 0 ? uint40(block.timestamp) : r.okSince) : 0; last[id] = Record(uint64(block.number), uint40(block.timestamp), verdict, path, lossBps, streak, okSince); assays += 1; feesTotal += msg.value; (bool ok, ) = splitter.call{value: msg.value}(""); require(ok, "split"); emit Assayed(id, token, msg.sender, verdict, path, lossBps, usdcIn, streak); } }