// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ISwapRouter} from "../bridge/trustless/interfaces/ISwapRouter.sol"; /** * @title UniswapV3ExternalUnwinder * @notice External unwind adapter for quote-push workflows using the Uniswap V3 SwapRouter. * @dev `data` is optional: * - `abi.encode(uint24 fee)` for exactInputSingle * - `abi.encode(bytes path)` for exactInput multi-hop path */ contract UniswapV3ExternalUnwinder { using SafeERC20 for IERC20; address public immutable router; error BadParams(); constructor(address router_) { router = router_; } function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data) external returns (uint256 amountOut) { if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut) revert BadParams(); if (amountIn == 0) revert BadParams(); IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); IERC20(tokenIn).forceApprove(router, amountIn); if (data.length == 32) { uint24 fee = abi.decode(data, (uint24)); amountOut = ISwapRouter(router).exactInputSingle( ISwapRouter.ExactInputSingleParams({ tokenIn: tokenIn, tokenOut: tokenOut, fee: fee, recipient: msg.sender, deadline: block.timestamp + 300, amountIn: amountIn, amountOutMinimum: minAmountOut, sqrtPriceLimitX96: 0 }) ); return amountOut; } bytes memory path = abi.decode(data, (bytes)); amountOut = ISwapRouter(router).exactInput( ISwapRouter.ExactInputParams({ path: path, recipient: msg.sender, deadline: block.timestamp + 300, amountIn: amountIn, amountOutMinimum: minAmountOut }) ); } }