diff --git a/src/FluxSwapRouter.sol b/src/FluxSwapRouter.sol new file mode 100644 index 0000000..cb7fd12 --- /dev/null +++ b/src/FluxSwapRouter.sol @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.36; + +import {IErc20} from "./interfaces/IErc20.sol"; +import {IFluxClFactory} from "./interfaces/IFluxClFactory.sol"; +import {IFluxClPool} from "./interfaces/IFluxClPool.sol"; +import {IFluxSwapCallback} from "./interfaces/IFluxSwapCallback.sol"; +import {Path} from "./libraries/Path.sol"; +import {SafeCast} from "./libraries/SafeCast.sol"; +import {TickMath} from "./libraries/TickMath.sol"; + +/// @title Multi-hop router for exact-input and exact-output concentrated swaps. +contract FluxSwapRouter is IFluxSwapCallback { + using Path for bytes; + using SafeCast for uint256; + + address public immutable factory; + + struct SwapCallbackData { + bytes path; + address payer; + } + + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + struct ExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + uint160 sqrtPriceLimitX96; + } + + struct ExactOutputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + } + + uint256 private constant amountInUnset = type(uint256).max; + uint256 private amountInCached = amountInUnset; + + modifier checkDeadline(uint256 deadline) { + require(block.timestamp <= deadline, "FluxRouter: EXPIRED"); + _; + } + + constructor(address factoryAddress) { + factory = factoryAddress; + } + + function _safeTransfer(address token, address to, uint256 value) private { + (bool success, bytes memory returndata) = + token.call(abi.encodeWithSelector(IErc20.transfer.selector, to, value)); + require(success && (returndata.length == 0 || abi.decode(returndata, (bool))), "FluxRouter: TRANSFER_FAILED"); + } + + function _safeTransferFrom(address token, address from, address to, uint256 value) private { + (bool success, bytes memory returndata) = + token.call(abi.encodeWithSelector(IErc20.transferFrom.selector, from, to, value)); + require( + success && (returndata.length == 0 || abi.decode(returndata, (bool))), "FluxRouter: TRANSFER_FROM_FAILED" + ); + } + + function pay(address token, address payer, address recipient, uint256 value) private { + if (payer == address(this)) { + _safeTransfer(token, recipient, value); + } else { + _safeTransferFrom(token, payer, recipient, value); + } + } + + function getPool(address tokenA, address tokenB, uint24 fee) private view returns (IFluxClPool) { + address pool = IFluxClFactory(factory).getPool(tokenA, tokenB, fee); + require(pool != address(0), "FluxRouter: NO_POOL"); + return IFluxClPool(pool); + } + + function fluxSwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external override { + require(amount0Delta > 0 || amount1Delta > 0, "FluxRouter: BAD_DELTA"); + SwapCallbackData memory decoded = abi.decode(data, (SwapCallbackData)); + (address tokenIn, address tokenOut, uint24 fee) = decoded.path.decodeFirstPool(); + require(msg.sender == address(getPool(tokenIn, tokenOut, fee)), "FluxRouter: BAD_CALLER"); + + (bool isExactInput, uint256 amountToPay) = + amount0Delta > 0 ? (tokenIn < tokenOut, uint256(amount0Delta)) : (tokenOut < tokenIn, uint256(amount1Delta)); + + if (isExactInput) { + pay(tokenIn, decoded.payer, msg.sender, amountToPay); + } else { + if (decoded.path.hasMultiplePools()) { + decoded.path = decoded.path.skipToken(); + exactOutputInternal(amountToPay, msg.sender, 0, decoded); + } else { + amountInCached = amountToPay; + pay(tokenOut, decoded.payer, msg.sender, amountToPay); + } + } + } + + function exactInputInternal( + uint256 amountIn, + address recipient, + uint160 sqrtPriceLimitX96, + SwapCallbackData memory data + ) private returns (uint256 amountOut) { + if (recipient == address(0)) recipient = address(this); + (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); + bool zeroForOne = tokenIn < tokenOut; + + (int256 amount0, int256 amount1) = getPool(tokenIn, tokenOut, fee) + .swap( + recipient, + zeroForOne, + amountIn.toInt256(), + sqrtPriceLimitX96 == 0 + ? (zeroForOne ? TickMath.minSqrtRatio + 1 : TickMath.maxSqrtRatio - 1) + : sqrtPriceLimitX96, + abi.encode(data) + ); + + return uint256(-(zeroForOne ? amount1 : amount0)); + } + + function exactInputSingle(ExactInputSingleParams calldata params) + external + checkDeadline(params.deadline) + returns (uint256 amountOut) + { + amountOut = exactInputInternal( + params.amountIn, + params.recipient, + params.sqrtPriceLimitX96, + SwapCallbackData({path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), payer: msg.sender}) + ); + require(amountOut >= params.amountOutMinimum, "FluxRouter: TOO_LITTLE_OUT"); + } + + function exactInput(ExactInputParams calldata params) + external + checkDeadline(params.deadline) + returns (uint256 amountOut) + { + address payer = msg.sender; + bytes memory path = params.path; + uint256 amountIn = params.amountIn; + + while (true) { + bool hasMultiplePools = path.hasMultiplePools(); + amountIn = exactInputInternal( + amountIn, + hasMultiplePools ? address(this) : params.recipient, + 0, + SwapCallbackData({path: path.getFirstPool(), payer: payer}) + ); + if (hasMultiplePools) { + payer = address(this); + path = path.skipToken(); + } else { + amountOut = amountIn; + break; + } + } + + require(amountOut >= params.amountOutMinimum, "FluxRouter: TOO_LITTLE_OUT"); + } + + function exactOutputInternal( + uint256 amountOut, + address recipient, + uint160 sqrtPriceLimitX96, + SwapCallbackData memory data + ) private returns (uint256 amountIn) { + if (recipient == address(0)) recipient = address(this); + (address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool(); + bool zeroForOne = tokenIn < tokenOut; + + (int256 amount0Delta, int256 amount1Delta) = getPool(tokenIn, tokenOut, fee) + .swap( + recipient, + zeroForOne, + -amountOut.toInt256(), + sqrtPriceLimitX96 == 0 + ? (zeroForOne ? TickMath.minSqrtRatio + 1 : TickMath.maxSqrtRatio - 1) + : sqrtPriceLimitX96, + abi.encode(data) + ); + + uint256 amountOutReceived; + (amountIn, amountOutReceived) = zeroForOne + ? (uint256(amount0Delta), uint256(-amount1Delta)) + : (uint256(amount1Delta), uint256(-amount0Delta)); + + if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut, "FluxRouter: OUTPUT_SHORT"); + } + + function exactOutputSingle(ExactOutputSingleParams calldata params) + external + checkDeadline(params.deadline) + returns (uint256 amountIn) + { + amountIn = exactOutputInternal( + params.amountOut, + params.recipient, + params.sqrtPriceLimitX96, + SwapCallbackData({path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), payer: msg.sender}) + ); + require(amountIn <= params.amountInMaximum, "FluxRouter: TOO_MUCH_IN"); + amountInCached = amountInUnset; + } + + function exactOutput(ExactOutputParams calldata params) + external + checkDeadline(params.deadline) + returns (uint256 amountIn) + { + exactOutputInternal( + params.amountOut, params.recipient, 0, SwapCallbackData({path: params.path, payer: msg.sender}) + ); + amountIn = amountInCached; + require(amountIn <= params.amountInMaximum, "FluxRouter: TOO_MUCH_IN"); + amountInCached = amountInUnset; + } +}