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