-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8 from ronin-chain/feature/mixed-route-quoter
feat: add MixedRouteQuoterV1
- Loading branch information
Showing
14 changed files
with
733 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule v2-core
deleted from
4dd590
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ | ||
@uniswap/lib/contracts/=lib/solidity-lib/contracts/ | ||
base64-sol/=lib/base64/ | ||
@uniswap/v2-core/contracts/=lib/v2-core/contracts/ | ||
@katana/v3-contracts/=src/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// SPDX-License-Identifier: GPL-2.0-or-later | ||
pragma solidity =0.7.6; | ||
|
||
import "../interfaces/IImmutableState.sol"; | ||
|
||
/// @title Immutable state | ||
/// @notice Immutable state used by the swap router | ||
abstract contract ImmutableState is IImmutableState { | ||
/// @inheritdoc IImmutableState | ||
address public immutable override factoryV2; | ||
/// @inheritdoc IImmutableState | ||
address public immutable override positionManager; | ||
|
||
constructor(address _factoryV2, address _positionManager) { | ||
factoryV2 = _factoryV2; | ||
positionManager = _positionManager; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// SPDX-License-Identifier: GPL-2.0-or-later | ||
pragma solidity >=0.5.0; | ||
|
||
/// @title Immutable state | ||
/// @notice Functions that return immutable state of the router | ||
interface IImmutableState { | ||
/// @return Returns the address of the Katana V2 factory | ||
function factoryV2() external view returns (address); | ||
|
||
/// @return Returns the address of Katana V3 NFT position manager | ||
function positionManager() external view returns (address); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
pragma solidity >=0.5.0; | ||
|
||
interface IKatanaV2Pair { | ||
event Approval(address indexed owner, address indexed spender, uint256 value); | ||
event Transfer(address indexed from, address indexed to, uint256 value); | ||
|
||
function name() external pure returns (string memory); | ||
function symbol() external pure returns (string memory); | ||
function decimals() external pure returns (uint8); | ||
function totalSupply() external view returns (uint256); | ||
function balanceOf(address owner) external view returns (uint256); | ||
function allowance(address owner, address spender) external view returns (uint256); | ||
|
||
function approve(address spender, uint256 value) external returns (bool); | ||
function transfer(address to, uint256 value) external returns (bool); | ||
function transferFrom(address from, address to, uint256 value) external returns (bool); | ||
|
||
function DOMAIN_SEPARATOR() external view returns (bytes32); | ||
function PERMIT_TYPEHASH() external pure returns (bytes32); | ||
function nonces(address owner) external view returns (uint256); | ||
|
||
function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) | ||
external; | ||
|
||
event Mint(address indexed sender, uint256 amount0, uint256 amount1); | ||
event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); | ||
event Swap( | ||
address indexed sender, | ||
uint256 amount0In, | ||
uint256 amount1In, | ||
uint256 amount0Out, | ||
uint256 amount1Out, | ||
address indexed to | ||
); | ||
event Sync(uint112 reserve0, uint112 reserve1); | ||
|
||
function MINIMUM_LIQUIDITY() external pure returns (uint256); | ||
function factory() external view returns (address); | ||
function token0() external view returns (address); | ||
function token1() external view returns (address); | ||
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); | ||
function price0CumulativeLast() external view returns (uint256); | ||
function price1CumulativeLast() external view returns (uint256); | ||
function kLast() external view returns (uint256); | ||
|
||
function mint(address to) external returns (uint256 liquidity); | ||
function burn(address to) external returns (uint256 amount0, uint256 amount1); | ||
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; | ||
function skim(address to) external; | ||
function sync() external; | ||
|
||
function initialize(address, address) external; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// SPDX-License-Identifier: GPL-2.0-or-later | ||
pragma solidity >=0.7.5; | ||
pragma abicoder v2; | ||
|
||
/// @title MixedRouteQuoterV1 Interface | ||
/// @notice Supports quoting the calculated amounts for exact input swaps. Is specialized for routes containing a mix of V2 and V3 liquidity. | ||
/// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap. | ||
/// @dev These functions are not marked view because they rely on calling non-view functions and reverting | ||
/// to compute the result. They are also not gas efficient and should not be called on-chain. | ||
interface IMixedRouteQuoterV1 { | ||
/// @notice Returns the amount out received for a given exact input swap without executing the swap | ||
/// @param path The path of the swap, i.e. each token pair and the pool fee | ||
/// @param amountIn The amount of the first token to swap | ||
/// @return amountOut The amount of the last token that would be received | ||
/// @return v3SqrtPriceX96AfterList List of the sqrt price after the swap for each v3 pool in the path, 0 for v2 pools | ||
/// @return v3InitializedTicksCrossedList List of the initialized ticks that the swap crossed for each v3 pool in the path, 0 for v2 pools | ||
/// @return v3SwapGasEstimate The estimate of the gas that the v3 swaps in the path consume | ||
function quoteExactInput(bytes memory path, uint256 amountIn) | ||
external | ||
returns ( | ||
uint256 amountOut, | ||
uint160[] memory v3SqrtPriceX96AfterList, | ||
uint32[] memory v3InitializedTicksCrossedList, | ||
uint256 v3SwapGasEstimate | ||
); | ||
|
||
struct QuoteExactInputSingleV3Params { | ||
address tokenIn; | ||
address tokenOut; | ||
uint256 amountIn; | ||
uint24 fee; | ||
uint160 sqrtPriceLimitX96; | ||
} | ||
|
||
struct QuoteExactInputSingleV2Params { | ||
address tokenIn; | ||
address tokenOut; | ||
uint256 amountIn; | ||
} | ||
|
||
/// @notice Returns the amount out received for a given exact input but for a swap of a single pool | ||
/// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` | ||
/// tokenIn The token being swapped in | ||
/// tokenOut The token being swapped out | ||
/// fee The fee of the token pool to consider for the pair | ||
/// amountIn The desired input amount | ||
/// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap | ||
/// @return amountOut The amount of `tokenOut` that would be received | ||
/// @return sqrtPriceX96After The sqrt price of the pool after the swap | ||
/// @return initializedTicksCrossed The number of initialized ticks that the swap crossed | ||
/// @return gasEstimate The estimate of the gas that the swap consumes | ||
function quoteExactInputSingleV3(QuoteExactInputSingleV3Params memory params) | ||
external | ||
returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate); | ||
|
||
/// @notice Returns the amount out received for a given exact input but for a swap of a single V2 pool | ||
/// @param params The params for the quote, encoded as `QuoteExactInputSingleV2Params` | ||
/// tokenIn The token being swapped in | ||
/// tokenOut The token being swapped out | ||
/// amountIn The desired input amount | ||
/// @return amountOut The amount of `tokenOut` that would be received | ||
function quoteExactInputSingleV2(QuoteExactInputSingleV2Params memory params) external returns (uint256 amountOut); | ||
|
||
/// @dev ExactOutput swaps are not supported by this new Quoter which is specialized for supporting routes | ||
/// crossing both V2 liquidity pairs and V3 pools. | ||
/// @deprecated quoteExactOutputSingle and exactOutput. Use QuoterV2 instead. | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
// SPDX-License-Identifier: GPL-2.0-or-later | ||
pragma solidity =0.7.6; | ||
pragma abicoder v2; | ||
|
||
import "@katana/v3-contracts/periphery/base/PeripheryImmutableState.sol"; | ||
import "@katana/v3-contracts/core/libraries/SafeCast.sol"; | ||
import "@katana/v3-contracts/core/libraries/TickMath.sol"; | ||
import "@katana/v3-contracts/core/libraries/TickBitmap.sol"; | ||
import "@katana/v3-contracts/core/interfaces/IKatanaV3Pool.sol"; | ||
import "@katana/v3-contracts/core/interfaces/callback/IKatanaV3SwapCallback.sol"; | ||
import "@katana/v3-contracts/periphery/libraries/Path.sol"; | ||
import "@katana/v3-contracts/periphery/libraries/PoolAddress.sol"; | ||
import "@katana/v3-contracts/periphery/libraries/CallbackValidation.sol"; | ||
|
||
import "../base/ImmutableState.sol"; | ||
import "../interfaces/IMixedRouteQuoterV1.sol"; | ||
import "../libraries/PoolTicksCounter.sol"; | ||
import "../libraries/KatanaV2Library.sol"; | ||
|
||
/// @title Provides on chain quotes for V3, V2, and MixedRoute exact input swaps | ||
/// @notice Allows getting the expected amount out for a given swap without executing the swap | ||
/// @notice Does not support exact output swaps since using the contract balance between exactOut swaps is not supported | ||
/// @dev These functions are not gas efficient and should _not_ be called on chain. Instead, optimistically execute | ||
/// the swap and check the amounts in the callback. | ||
contract MixedRouteQuoterV1 is IMixedRouteQuoterV1, IKatanaV3SwapCallback, PeripheryImmutableState { | ||
using Path for bytes; | ||
using SafeCast for uint256; | ||
using PoolTicksCounter for IKatanaV3Pool; | ||
|
||
address public immutable factoryV2; | ||
/// @dev Value to bit mask with path fee to determine if V2 or V3 route | ||
// max V3 fee: 000011110100001001000000 (24 bits) | ||
// mask: 1 << 23 = 100000000000000000000000 = decimal value 8388608 | ||
uint24 private constant flagBitmask = 8388608; | ||
|
||
/// @dev Transient storage variable used to check a safety condition in exact output swaps. | ||
uint256 private amountOutCached; | ||
|
||
constructor(address _factory, address _factoryV2, address _WETH9) PeripheryImmutableState(_factory, _WETH9) { | ||
factoryV2 = _factoryV2; | ||
} | ||
|
||
function getPool(address tokenA, address tokenB, uint24 fee) private view returns (IKatanaV3Pool) { | ||
return IKatanaV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee))); | ||
} | ||
|
||
/// @dev Given an amountIn, fetch the reserves of the V2 pair and get the amountOut | ||
function getPairAmountOut(uint256 amountIn, address tokenIn, address tokenOut) private view returns (uint256) { | ||
(uint256 reserveIn, uint256 reserveOut) = KatanaV2Library.getReserves(factoryV2, tokenIn, tokenOut); | ||
return KatanaV2Library.getAmountOut(amountIn, reserveIn, reserveOut); | ||
} | ||
|
||
/// @inheritdoc IKatanaV3SwapCallback | ||
function katanaV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes memory path) external view override { | ||
require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported | ||
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); | ||
CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee); | ||
|
||
(bool isExactInput, uint256 amountReceived) = | ||
amount0Delta > 0 ? (tokenIn < tokenOut, uint256(-amount1Delta)) : (tokenOut < tokenIn, uint256(-amount0Delta)); | ||
|
||
IKatanaV3Pool pool = getPool(tokenIn, tokenOut, fee); | ||
(uint160 v3SqrtPriceX96After, int24 tickAfter,,,,,) = pool.slot0(); | ||
|
||
if (isExactInput) { | ||
assembly { | ||
let ptr := mload(0x40) | ||
mstore(ptr, amountReceived) | ||
mstore(add(ptr, 0x20), v3SqrtPriceX96After) | ||
mstore(add(ptr, 0x40), tickAfter) | ||
revert(ptr, 0x60) | ||
} | ||
} else { | ||
/// since we don't support exactOutput, revert here | ||
revert("Exact output quote not supported"); | ||
} | ||
} | ||
|
||
/// @dev Parses a revert reason that should contain the numeric quote | ||
function parseRevertReason(bytes memory reason) | ||
private | ||
pure | ||
returns (uint256 amount, uint160 sqrtPriceX96After, int24 tickAfter) | ||
{ | ||
if (reason.length != 0x60) { | ||
if (reason.length < 0x44) revert("Unexpected error"); | ||
assembly { | ||
reason := add(reason, 0x04) | ||
} | ||
revert(abi.decode(reason, (string))); | ||
} | ||
return abi.decode(reason, (uint256, uint160, int24)); | ||
} | ||
|
||
function handleV3Revert(bytes memory reason, IKatanaV3Pool pool, uint256 gasEstimate) | ||
private | ||
view | ||
returns (uint256 amount, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256) | ||
{ | ||
int24 tickBefore; | ||
int24 tickAfter; | ||
(, tickBefore,,,,,) = pool.slot0(); | ||
(amount, sqrtPriceX96After, tickAfter) = parseRevertReason(reason); | ||
|
||
initializedTicksCrossed = pool.countInitializedTicksCrossed(tickBefore, tickAfter); | ||
|
||
return (amount, sqrtPriceX96After, initializedTicksCrossed, gasEstimate); | ||
} | ||
|
||
/// @dev Fetch an exactIn quote for a V3 Pool on chain | ||
function quoteExactInputSingleV3(QuoteExactInputSingleV3Params memory params) | ||
public | ||
override | ||
returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate) | ||
{ | ||
bool zeroForOne = params.tokenIn < params.tokenOut; | ||
IKatanaV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee); | ||
|
||
uint256 gasBefore = gasleft(); | ||
try pool.swap( | ||
address(this), // address(0) might cause issues with some tokens | ||
zeroForOne, | ||
params.amountIn.toInt256(), | ||
params.sqrtPriceLimitX96 == 0 | ||
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) | ||
: params.sqrtPriceLimitX96, | ||
abi.encodePacked(params.tokenIn, params.fee, params.tokenOut) | ||
) { } catch (bytes memory reason) { | ||
gasEstimate = gasBefore - gasleft(); | ||
return handleV3Revert(reason, pool, gasEstimate); | ||
} | ||
} | ||
|
||
/// @dev Fetch an exactIn quote for a V2 pair on chain | ||
function quoteExactInputSingleV2(QuoteExactInputSingleV2Params memory params) | ||
public | ||
view | ||
override | ||
returns (uint256 amountOut) | ||
{ | ||
amountOut = getPairAmountOut(params.amountIn, params.tokenIn, params.tokenOut); | ||
} | ||
|
||
/// @dev Get the quote for an exactIn swap between an array of V2 and/or V3 pools | ||
/// @notice To encode a V2 pair within the path, use 0x800000 (hex value of 8388608) for the fee between the two token addresses | ||
function quoteExactInput(bytes memory path, uint256 amountIn) | ||
public | ||
override | ||
returns ( | ||
uint256 amountOut, | ||
uint160[] memory v3SqrtPriceX96AfterList, | ||
uint32[] memory v3InitializedTicksCrossedList, | ||
uint256 v3SwapGasEstimate | ||
) | ||
{ | ||
v3SqrtPriceX96AfterList = new uint160[](path.numPools()); | ||
v3InitializedTicksCrossedList = new uint32[](path.numPools()); | ||
|
||
uint256 i = 0; | ||
while (true) { | ||
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); | ||
|
||
if (fee & flagBitmask != 0) { | ||
amountIn = quoteExactInputSingleV2( | ||
QuoteExactInputSingleV2Params({ tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn }) | ||
); | ||
} else { | ||
/// the outputs of prior swaps become the inputs to subsequent ones | ||
(uint256 _amountOut, uint160 _sqrtPriceX96After, uint32 _initializedTicksCrossed, uint256 _gasEstimate) = | ||
quoteExactInputSingleV3( | ||
QuoteExactInputSingleV3Params({ | ||
tokenIn: tokenIn, | ||
tokenOut: tokenOut, | ||
fee: fee, | ||
amountIn: amountIn, | ||
sqrtPriceLimitX96: 0 | ||
}) | ||
); | ||
v3SqrtPriceX96AfterList[i] = _sqrtPriceX96After; | ||
v3InitializedTicksCrossedList[i] = _initializedTicksCrossed; | ||
v3SwapGasEstimate += _gasEstimate; | ||
amountIn = _amountOut; | ||
} | ||
i++; | ||
|
||
/// decide whether to continue or terminate | ||
if (path.hasMultiplePools()) { | ||
path = path.skipToken(); | ||
} else { | ||
return (amountIn, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList, v3SwapGasEstimate); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.