Skip to content

Commit

Permalink
Merge pull request #8 from ronin-chain/feature/mixed-route-quoter
Browse files Browse the repository at this point in the history
feat: add MixedRouteQuoterV1
  • Loading branch information
thaixuandang committed Aug 26, 2024
2 parents df1ce8f + 92643d4 commit e04a962
Show file tree
Hide file tree
Showing 14 changed files with 733 additions and 10 deletions.
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
[submodule "lib/base64"]
path = lib/base64
url = https://github.com/Brechtpd/base64
[submodule "lib/v2-core"]
path = lib/v2-core
url = https://github.com/Uniswap/v2-core
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
1 change: 0 additions & 1 deletion lib/v2-core
Submodule v2-core deleted from 4dd590
1 change: 0 additions & 1 deletion remappings.txt
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/
6 changes: 3 additions & 3 deletions src/periphery/V3Migrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ pragma solidity =0.7.6;
pragma abicoder v2;

import "@katana/v3-contracts/core/libraries/LowGasSafeMath.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";

import "./interfaces/INonfungiblePositionManager.sol";

import "./libraries/TransferHelper.sol";

import "./interfaces/IV3Migrator.sol";
import "./interfaces/IKatanaV2Pair.sol";
import "./base/PeripheryImmutableState.sol";
import "./base/Multicall.sol";
import "./base/SelfPermit.sol";
Expand Down Expand Up @@ -37,8 +37,8 @@ contract V3Migrator is IV3Migrator, PeripheryImmutableState, PoolInitializer, Mu
require(params.percentageToMigrate <= 100, "Percentage too large");

// burn v2 liquidity to this address
IUniswapV2Pair(params.pair).transferFrom(msg.sender, params.pair, params.liquidityToMigrate);
(uint256 amount0V2, uint256 amount1V2) = IUniswapV2Pair(params.pair).burn(address(this));
IKatanaV2Pair(params.pair).transferFrom(msg.sender, params.pair, params.liquidityToMigrate);
(uint256 amount0V2, uint256 amount1V2) = IKatanaV2Pair(params.pair).burn(address(this));

// calculate the amounts to migrate to v3
uint256 amount0V2ToMigrate = amount0V2.mul(params.percentageToMigrate) / 100;
Expand Down
18 changes: 18 additions & 0 deletions src/periphery/base/ImmutableState.sol
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;
}
}
12 changes: 12 additions & 0 deletions src/periphery/interfaces/IImmutableState.sol
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);
}
53 changes: 53 additions & 0 deletions src/periphery/interfaces/IKatanaV2Pair.sol
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;
}
67 changes: 67 additions & 0 deletions src/periphery/interfaces/IMixedRouteQuoterV1.sol
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.
}
194 changes: 194 additions & 0 deletions src/periphery/lens/MixedRouteQuoterV1.sol
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);
}
}
}
}
Loading

0 comments on commit e04a962

Please sign in to comment.