feat(router): add multi-hop concentrated-liquidity swap router
This commit is contained in:
1 parent
54b5377369
commit
e1f3128e5b
1 file changed
+247
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user