feat(router): add multi-hop concentrated-liquidity swap router

This commit is contained in:
dualfroz committed 2026-08-25 14:28:00 +02:00
1 parent 54b5377369
commit e1f3128e5b
1 file changed
+247
+247
View File
@@ -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;
}
}