From 251d69811ae3917d10d981f06aab195a42aa36a2 Mon Sep 17 00:00:00 2001 From: Thai Xuan Dang Date: Thu, 25 Jul 2024 10:31:31 +0700 Subject: [PATCH 1/3] feat: add MixedRouteQuoterV1 --- src/periphery/base/ImmutableState.sol | 18 ++ src/periphery/interfaces/IImmutableState.sol | 12 ++ .../interfaces/IMixedRouteQuoterV1.sol | 67 ++++++ src/periphery/lens/MixedRouteQuoterV1.sol | 195 ++++++++++++++++++ .../lens/MixedRouteQuoterV1Testnet.sol | 195 ++++++++++++++++++ src/periphery/libraries/KatanaV2Library.sol | 86 ++++++++ .../libraries/KatanaV2LibraryTestnet.sol | 86 ++++++++ src/periphery/libraries/PoolAddress.sol | 5 +- test/core/PoolProxyInitCodeHash.t.sol | 17 ++ 9 files changed, 679 insertions(+), 2 deletions(-) create mode 100644 src/periphery/base/ImmutableState.sol create mode 100644 src/periphery/interfaces/IImmutableState.sol create mode 100644 src/periphery/interfaces/IMixedRouteQuoterV1.sol create mode 100644 src/periphery/lens/MixedRouteQuoterV1.sol create mode 100644 src/periphery/lens/MixedRouteQuoterV1Testnet.sol create mode 100644 src/periphery/libraries/KatanaV2Library.sol create mode 100644 src/periphery/libraries/KatanaV2LibraryTestnet.sol create mode 100644 test/core/PoolProxyInitCodeHash.t.sol diff --git a/src/periphery/base/ImmutableState.sol b/src/periphery/base/ImmutableState.sol new file mode 100644 index 0000000..a180762 --- /dev/null +++ b/src/periphery/base/ImmutableState.sol @@ -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; + } +} diff --git a/src/periphery/interfaces/IImmutableState.sol b/src/periphery/interfaces/IImmutableState.sol new file mode 100644 index 0000000..b02a1bf --- /dev/null +++ b/src/periphery/interfaces/IImmutableState.sol @@ -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); +} diff --git a/src/periphery/interfaces/IMixedRouteQuoterV1.sol b/src/periphery/interfaces/IMixedRouteQuoterV1.sol new file mode 100644 index 0000000..93ed24d --- /dev/null +++ b/src/periphery/interfaces/IMixedRouteQuoterV1.sol @@ -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. +} diff --git a/src/periphery/lens/MixedRouteQuoterV1.sol b/src/periphery/lens/MixedRouteQuoterV1.sol new file mode 100644 index 0000000..e15eaf5 --- /dev/null +++ b/src/periphery/lens/MixedRouteQuoterV1.sol @@ -0,0 +1,195 @@ +// 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 "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.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); + } + } + } +} diff --git a/src/periphery/lens/MixedRouteQuoterV1Testnet.sol b/src/periphery/lens/MixedRouteQuoterV1Testnet.sol new file mode 100644 index 0000000..c9eade8 --- /dev/null +++ b/src/periphery/lens/MixedRouteQuoterV1Testnet.sol @@ -0,0 +1,195 @@ +// 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 "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; + +import "../base/ImmutableState.sol"; +import "../interfaces/IMixedRouteQuoterV1.sol"; +import "../libraries/PoolTicksCounter.sol"; +import "../libraries/KatanaV2LibraryTestnet.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); + } + } + } +} diff --git a/src/periphery/libraries/KatanaV2Library.sol b/src/periphery/libraries/KatanaV2Library.sol new file mode 100644 index 0000000..c6803f9 --- /dev/null +++ b/src/periphery/libraries/KatanaV2Library.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; +import "@katana/v3-contracts/core/libraries/LowGasSafeMath.sol"; + +library KatanaV2Library { + using LowGasSafeMath for uint256; + + // returns sorted token addresses, used to handle return values from pairs sorted in this order + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0)); + } + + // calculates the CREATE2 address for a pair without making any external calls + function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + pair = address( + uint256( + keccak256( + abi.encodePacked( + hex"ff", + factory, + keccak256(abi.encodePacked(token0, token1)), + hex"e85772d2fe4ad93037659afaee57751696456eb5dd99987e43f3cf11c6e255a2" // init code hash + ) + ) + ) + ); + } + + // fetches and sorts the reserves for a pair + function getReserves(address factory, address tokenA, address tokenB) + internal + view + returns (uint256 reserveA, uint256 reserveB) + { + (address token0,) = sortTokens(tokenA, tokenB); + (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + } + + // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) + internal + pure + returns (uint256 amountOut) + { + require(amountIn > 0, "INSUFFICIENT_INPUT_AMOUNT"); + require(reserveIn > 0 && reserveOut > 0); + uint256 amountInWithFee = amountIn.mul(997); + uint256 numerator = amountInWithFee.mul(reserveOut); + uint256 denominator = reserveIn.mul(1000).add(amountInWithFee); + amountOut = numerator / denominator; + } + + // given an output amount of an asset and pair reserves, returns a required input amount of the other asset + function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) + internal + pure + returns (uint256 amountIn) + { + require(amountOut > 0, "INSUFFICIENT_OUTPUT_AMOUNT"); + require(reserveIn > 0 && reserveOut > 0); + uint256 numerator = reserveIn.mul(amountOut).mul(1000); + uint256 denominator = reserveOut.sub(amountOut).mul(997); + amountIn = (numerator / denominator).add(1); + } + + // performs chained getAmountIn calculations on any number of pairs + function getAmountsIn(address factory, uint256 amountOut, address[] memory path) + internal + view + returns (uint256[] memory amounts) + { + require(path.length >= 2); + amounts = new uint256[](path.length); + amounts[amounts.length - 1] = amountOut; + for (uint256 i = path.length - 1; i > 0; i--) { + (uint256 reserveIn, uint256 reserveOut) = getReserves(factory, path[i - 1], path[i]); + amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); + } + } +} diff --git a/src/periphery/libraries/KatanaV2LibraryTestnet.sol b/src/periphery/libraries/KatanaV2LibraryTestnet.sol new file mode 100644 index 0000000..702a8c2 --- /dev/null +++ b/src/periphery/libraries/KatanaV2LibraryTestnet.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; +import "@katana/v3-contracts/core/libraries/LowGasSafeMath.sol"; + +library KatanaV2Library { + using LowGasSafeMath for uint256; + + // returns sorted token addresses, used to handle return values from pairs sorted in this order + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0)); + } + + // calculates the CREATE2 address for a pair without making any external calls + function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + pair = address( + uint256( + keccak256( + abi.encodePacked( + hex"ff", + factory, + keccak256(abi.encodePacked(token0, token1)), + hex"1cc97ead4d6949b7a6ecb28652b21159b9fd5608ae51a1960224099caab07dca" // init code hash + ) + ) + ) + ); + } + + // fetches and sorts the reserves for a pair + function getReserves(address factory, address tokenA, address tokenB) + internal + view + returns (uint256 reserveA, uint256 reserveB) + { + (address token0,) = sortTokens(tokenA, tokenB); + (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + } + + // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) + internal + pure + returns (uint256 amountOut) + { + require(amountIn > 0, "INSUFFICIENT_INPUT_AMOUNT"); + require(reserveIn > 0 && reserveOut > 0); + uint256 amountInWithFee = amountIn.mul(997); + uint256 numerator = amountInWithFee.mul(reserveOut); + uint256 denominator = reserveIn.mul(1000).add(amountInWithFee); + amountOut = numerator / denominator; + } + + // given an output amount of an asset and pair reserves, returns a required input amount of the other asset + function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) + internal + pure + returns (uint256 amountIn) + { + require(amountOut > 0, "INSUFFICIENT_OUTPUT_AMOUNT"); + require(reserveIn > 0 && reserveOut > 0); + uint256 numerator = reserveIn.mul(amountOut).mul(1000); + uint256 denominator = reserveOut.sub(amountOut).mul(997); + amountIn = (numerator / denominator).add(1); + } + + // performs chained getAmountIn calculations on any number of pairs + function getAmountsIn(address factory, uint256 amountOut, address[] memory path) + internal + view + returns (uint256[] memory amounts) + { + require(path.length >= 2); + amounts = new uint256[](path.length); + amounts[amounts.length - 1] = amountOut; + for (uint256 i = path.length - 1; i > 0; i--) { + (uint256 reserveIn, uint256 reserveOut) = getReserves(factory, path[i - 1], path[i]); + amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); + } + } +} diff --git a/src/periphery/libraries/PoolAddress.sol b/src/periphery/libraries/PoolAddress.sol index 9fe42a2..827e423 100644 --- a/src/periphery/libraries/PoolAddress.sol +++ b/src/periphery/libraries/PoolAddress.sol @@ -3,7 +3,8 @@ pragma solidity >=0.5.0; /// @title Provides functions for deriving a pool address from the factory, tokens, and the fee library PoolAddress { - bytes32 internal constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; + bytes32 internal constant POOL_PROXY_INIT_CODE_HASH = + 0xb551f2bebbf8042ae4c9d7e54a12b69cb2cc37160be9f57c225c869192146d2a; /// @notice The identifying key of the pool struct PoolKey { @@ -32,7 +33,7 @@ library PoolAddress { uint256( keccak256( abi.encodePacked( - hex"ff", factory, keccak256(abi.encode(key.token0, key.token1, key.fee)), POOL_INIT_CODE_HASH + hex"ff", factory, keccak256(abi.encode(key.token0, key.token1, key.fee)), POOL_PROXY_INIT_CODE_HASH ) ) ) diff --git a/test/core/PoolProxyInitCodeHash.t.sol b/test/core/PoolProxyInitCodeHash.t.sol new file mode 100644 index 0000000..3832486 --- /dev/null +++ b/test/core/PoolProxyInitCodeHash.t.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { Test, console } from "forge-std/Test.sol"; +import { PoolAddress } from "@katana/v3-contracts/periphery/libraries/PoolAddress.sol"; +import { KatanaV3PoolProxy } from "@katana/v3-contracts/core/KatanaV3PoolProxy.sol"; + +contract PoolProxyInitCodeHashTest is Test { + function test_POOL_PROXY_INIT_CODE_HASH() public pure { + assertEq( + PoolAddress.POOL_PROXY_INIT_CODE_HASH, + keccak256(type(KatanaV3PoolProxy).creationCode), + "PoolAddress.POOL_PROXY_INIT_CODE_HASH should match the creation code of the pool proxy" + ); + } +} From c55d7314cf2529324f04d126a051e1b6d907938a Mon Sep 17 00:00:00 2001 From: Thai Xuan Dang Date: Thu, 25 Jul 2024 11:13:33 +0700 Subject: [PATCH 2/3] feat: remove Uniswap/v2-core --- .gitmodules | 3 -- lib/v2-core | 1 - src/periphery/V3Migrator.sol | 6 +-- src/periphery/interfaces/IKatanaV2Pair.sol | 53 +++++++++++++++++++ src/periphery/lens/MixedRouteQuoterV1.sol | 1 - .../lens/MixedRouteQuoterV1Testnet.sol | 1 - src/periphery/libraries/KatanaV2Library.sol | 4 +- .../libraries/KatanaV2LibraryTestnet.sol | 4 +- src/periphery/libraries/PoolAddress.sol | 2 +- 9 files changed, 61 insertions(+), 14 deletions(-) delete mode 160000 lib/v2-core create mode 100644 src/periphery/interfaces/IKatanaV2Pair.sol diff --git a/.gitmodules b/.gitmodules index 08a41ad..f8a863c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/lib/v2-core b/lib/v2-core deleted file mode 160000 index 4dd5906..0000000 --- a/lib/v2-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4dd59067c76dea4a0e8e4bfdda41877a6b16dedc diff --git a/src/periphery/V3Migrator.sol b/src/periphery/V3Migrator.sol index 04c9980..fc98d15 100644 --- a/src/periphery/V3Migrator.sol +++ b/src/periphery/V3Migrator.sol @@ -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"; @@ -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; diff --git a/src/periphery/interfaces/IKatanaV2Pair.sol b/src/periphery/interfaces/IKatanaV2Pair.sol new file mode 100644 index 0000000..c1b8b7c --- /dev/null +++ b/src/periphery/interfaces/IKatanaV2Pair.sol @@ -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; +} diff --git a/src/periphery/lens/MixedRouteQuoterV1.sol b/src/periphery/lens/MixedRouteQuoterV1.sol index e15eaf5..328246b 100644 --- a/src/periphery/lens/MixedRouteQuoterV1.sol +++ b/src/periphery/lens/MixedRouteQuoterV1.sol @@ -11,7 +11,6 @@ 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 "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; import "../base/ImmutableState.sol"; import "../interfaces/IMixedRouteQuoterV1.sol"; diff --git a/src/periphery/lens/MixedRouteQuoterV1Testnet.sol b/src/periphery/lens/MixedRouteQuoterV1Testnet.sol index c9eade8..5adca13 100644 --- a/src/periphery/lens/MixedRouteQuoterV1Testnet.sol +++ b/src/periphery/lens/MixedRouteQuoterV1Testnet.sol @@ -11,7 +11,6 @@ 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 "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; import "../base/ImmutableState.sol"; import "../interfaces/IMixedRouteQuoterV1.sol"; diff --git a/src/periphery/libraries/KatanaV2Library.sol b/src/periphery/libraries/KatanaV2Library.sol index c6803f9..e6bb521 100644 --- a/src/periphery/libraries/KatanaV2Library.sol +++ b/src/periphery/libraries/KatanaV2Library.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; -import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; import "@katana/v3-contracts/core/libraries/LowGasSafeMath.sol"; +import "../interfaces/IKatanaV2Pair.sol"; library KatanaV2Library { using LowGasSafeMath for uint256; @@ -38,7 +38,7 @@ library KatanaV2Library { returns (uint256 reserveA, uint256 reserveB) { (address token0,) = sortTokens(tokenA, tokenB); - (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); + (uint256 reserve0, uint256 reserve1,) = IKatanaV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); } diff --git a/src/periphery/libraries/KatanaV2LibraryTestnet.sol b/src/periphery/libraries/KatanaV2LibraryTestnet.sol index 702a8c2..4181665 100644 --- a/src/periphery/libraries/KatanaV2LibraryTestnet.sol +++ b/src/periphery/libraries/KatanaV2LibraryTestnet.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; -import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; import "@katana/v3-contracts/core/libraries/LowGasSafeMath.sol"; +import "../interfaces/IKatanaV2Pair.sol"; library KatanaV2Library { using LowGasSafeMath for uint256; @@ -38,7 +38,7 @@ library KatanaV2Library { returns (uint256 reserveA, uint256 reserveB) { (address token0,) = sortTokens(tokenA, tokenB); - (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); + (uint256 reserve0, uint256 reserve1,) = IKatanaV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); } diff --git a/src/periphery/libraries/PoolAddress.sol b/src/periphery/libraries/PoolAddress.sol index 827e423..2e1d886 100644 --- a/src/periphery/libraries/PoolAddress.sol +++ b/src/periphery/libraries/PoolAddress.sol @@ -4,7 +4,7 @@ pragma solidity >=0.5.0; /// @title Provides functions for deriving a pool address from the factory, tokens, and the fee library PoolAddress { bytes32 internal constant POOL_PROXY_INIT_CODE_HASH = - 0xb551f2bebbf8042ae4c9d7e54a12b69cb2cc37160be9f57c225c869192146d2a; + 0x7ea3309dc602e4f5ac7e136ec42577d64c38801f15c8eeeea38c3f6e79ae689e; /// @notice The identifying key of the pool struct PoolKey { From fcb7bbf05c9127a0768280ef784f0531eff39814 Mon Sep 17 00:00:00 2001 From: Thai Xuan Dang Date: Thu, 25 Jul 2024 11:21:36 +0700 Subject: [PATCH 3/3] chore: remove @uniswap/v2-core in remappings --- remappings.txt | 1 - src/periphery/libraries/PoolAddress.sol | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/remappings.txt b/remappings.txt index 44f694d..b3eae3c 100644 --- a/remappings.txt +++ b/remappings.txt @@ -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/ \ No newline at end of file diff --git a/src/periphery/libraries/PoolAddress.sol b/src/periphery/libraries/PoolAddress.sol index 2e1d886..c74f4a2 100644 --- a/src/periphery/libraries/PoolAddress.sol +++ b/src/periphery/libraries/PoolAddress.sol @@ -4,7 +4,7 @@ pragma solidity >=0.5.0; /// @title Provides functions for deriving a pool address from the factory, tokens, and the fee library PoolAddress { bytes32 internal constant POOL_PROXY_INIT_CODE_HASH = - 0x7ea3309dc602e4f5ac7e136ec42577d64c38801f15c8eeeea38c3f6e79ae689e; + 0xd07ca541bf6075ecbda10494b7aec9a7cd9e513f7cf99ae88194d3ee27016912; /// @notice The identifying key of the pool struct PoolKey {