// SPDX-License-Identifier: MIT pragma solidity 0.8.28; interface IERC20 { function balanceOf(address) external view returns (uint256); function approve(address, uint256) external returns (bool); } interface IPermit2 { function approve(address token, address spender, uint160 amount, uint48 expiration) external; } interface IUniversalRouter { function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable; } /// @title Arc Exit Probe — "if I buy this, can I get out?", answered by actually doing it and then undoing it /// @notice Meant for eth_call only. `probe` buys a token with USDC through the Universal Router, sells everything it /// received straight back, and then REVERTS with the numbers, so no state ever changes and nothing is spent. /// Because it runs the real swaps, hooks that block or tax sells, tokens with transfer fees, and thin pools all /// show up as what they are. Works for pools whose USDC side is native (address 0) or the ERC-20 view. contract ArcExitProbe { struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; } struct ExactInputSingleParams { PoolKey poolKey; bool zeroForOne; uint128 amountIn; uint128 amountOutMinimum; bytes hookData; } IUniversalRouter public immutable router; IPermit2 public immutable permit2; error ProbeResult(uint256 usdcIn, uint256 tokensOut, uint256 usdcBack, bool buyOk, bool sellOk, bytes buyError, bytes sellError); constructor(address router_, address permit2_) { router = IUniversalRouter(router_); permit2 = IPermit2(permit2_); } receive() external payable {} function _swap(PoolKey memory key, bool zeroForOne, uint256 amountIn) internal { address cin = zeroForOne ? key.currency0 : key.currency1; address cout = zeroForOne ? key.currency1 : key.currency0; bytes memory actions = abi.encodePacked(uint8(0x06), uint8(0x0c), uint8(0x0f)); bytes[] memory params = new bytes[](3); params[0] = abi.encode(ExactInputSingleParams(key, zeroForOne, uint128(amountIn), 0, bytes(""))); params[1] = abi.encode(cin, amountIn); params[2] = abi.encode(cout, uint256(0)); bytes[] memory inputs = new bytes[](1); inputs[0] = abi.encode(actions, params); if (cin != address(0)) { IERC20(cin).approve(address(permit2), type(uint256).max); permit2.approve(cin, address(router), type(uint160).max, type(uint48).max); } router.execute{value: cin == address(0) ? amountIn : 0}(abi.encodePacked(uint8(0x10)), inputs, block.timestamp + 60); } function doSwap(PoolKey calldata key, bool zeroForOne, uint256 amountIn) external { require(msg.sender == address(this), "self"); _swap(key, zeroForOne, amountIn); } function bal(address c) internal view returns (uint256) { return c == address(0) ? address(this).balance : IERC20(c).balanceOf(address(this)); } /// @param usdcIsCurrency0 true when the USDC side (native 0x0 or the ERC-20 view) is currency0 function probe(PoolKey calldata key, bool usdcIsCurrency0, uint256 usdcIn) external { address usdc = usdcIsCurrency0 ? key.currency0 : key.currency1; address token = usdcIsCurrency0 ? key.currency1 : key.currency0; uint256 t0 = bal(token); uint256 u0 = bal(usdc); bool buyOk = true; bytes memory buyErr; try this.doSwap(key, usdcIsCurrency0, usdcIn) {} catch (bytes memory e) { buyOk = false; buyErr = e; } uint256 got = buyOk ? bal(token) - t0 : 0; bool sellOk = false; bytes memory sellErr; uint256 back = 0; if (buyOk && got > 0) { uint256 u1 = bal(usdc); try this.doSwap(key, !usdcIsCurrency0, got) { sellOk = true; back = bal(usdc) - u1; } catch (bytes memory e) { sellErr = e; } } u0; revert ProbeResult(usdcIn, got, back, buyOk, sellOk, buyErr, sellErr); } }