// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; interface IUniswapV3Factory { function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address); } interface IUniswapV3Pool { function swap(address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data) external returns (int256 amount0, int256 amount1); } /// @title TarnSwap /// @notice Buys and sells tokenized stocks on Robinhood Chain against the Uniswap v3 pools of one factory, /// fixed at deployment. Exact input only, all-or-nothing, and never holds a balance between calls. /// @dev No owner, no fee, no upgrade, no pause. A pool is trusted only if the factory returns it for the /// (tokenIn, tokenOut, fee) triple, and the pays-the-pool callback accepts nothing else. contract TarnSwap { using SafeERC20 for IERC20; IUniswapV3Factory public immutable factory; uint160 internal constant MIN_SQRT_RATIO = 4295128739; uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; error Expired(); error BadAmount(); error NoPool(); error NotPool(); error PartialFill(uint256 used, uint256 asked); error TooLittleReceived(uint256 amountOut, uint256 minOut); event Swapped( address indexed payer, address indexed to, address indexed tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint256 amountOut ); struct Leg { address tokenIn; address tokenOut; uint24 fee; address payer; // address(0) marks a quote: the callback reverts with the result instead of paying } constructor(IUniswapV3Factory factory_) { factory = factory_; } /// @notice Sell exactly `amountIn` of `tokenIn` for at least `minOut` of `tokenOut`, delivered to `to`. /// @dev The whole amount is sold or the call reverts: a pool that runs out of liquidity before the input /// is spent would otherwise leave the caller with a smaller trade than they signed for. function swapExactIn( address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint256 minOut, address to, uint256 deadline ) external returns (uint256 amountOut) { if (block.timestamp > deadline) revert Expired(); (uint256 used, uint256 got) = _swap(tokenIn, tokenOut, fee, amountIn, to, msg.sender); if (used != amountIn) revert PartialFill(used, amountIn); if (got < minOut) revert TooLittleReceived(got, minOut); emit Swapped(msg.sender, to, tokenIn, tokenOut, fee, amountIn, got); return got; } /// @notice What `swapExactIn` would return right now, and how much of the input the pool would take. /// @dev Not a view: it runs the real swap and unwinds it. Call it with eth_call. function quoteExactIn(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn) external returns (uint256 amountOut, uint256 used) { try this.simulate(tokenIn, tokenOut, fee, amountIn) { revert(); // unreachable: simulate always reverts } catch (bytes memory reason) { if (reason.length != 64) { assembly { revert(add(reason, 32), mload(reason)) } } (amountOut, used) = abi.decode(reason, (uint256, uint256)); } } /// @dev Entry point for `quoteExactIn`; reverts with abi.encode(amountOut, used) from inside the callback. function simulate(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn) external { _swap(tokenIn, tokenOut, fee, amountIn, address(this), address(0)); } function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external { Leg memory leg = abi.decode(data, (Leg)); if (msg.sender != factory.getPool(leg.tokenIn, leg.tokenOut, leg.fee)) revert NotPool(); uint256 owed = uint256(amount0Delta > 0 ? amount0Delta : amount1Delta); if (leg.payer == address(0)) { uint256 out = uint256(-(amount0Delta > 0 ? amount1Delta : amount0Delta)); assembly { mstore(0, out) mstore(32, owed) revert(0, 64) } } IERC20(leg.tokenIn).safeTransferFrom(leg.payer, msg.sender, owed); } function _swap(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, address to, address payer) internal returns (uint256 used, uint256 got) { if (amountIn == 0 || amountIn > uint256(type(int256).max)) revert BadAmount(); address pool = factory.getPool(tokenIn, tokenOut, fee); if (pool == address(0)) revert NoPool(); bytes memory data = abi.encode(Leg(tokenIn, tokenOut, fee, payer)); bool zeroForOne = tokenIn < tokenOut; (int256 a0, int256 a1) = IUniswapV3Pool(pool).swap( to, zeroForOne, int256(amountIn), zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, data ); (used, got) = zeroForOne ? (uint256(a0), uint256(-a1)) : (uint256(a1), uint256(-a0)); } }