diff --git a/.forge-snapshots/FullOracleObserve0After5Seconds.snap b/.forge-snapshots/FullOracleObserve0After5Seconds.snap index bc61a749..a08fb8e1 100644 --- a/.forge-snapshots/FullOracleObserve0After5Seconds.snap +++ b/.forge-snapshots/FullOracleObserve0After5Seconds.snap @@ -1 +1 @@ -2771 \ No newline at end of file +2687 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve200By13.snap b/.forge-snapshots/FullOracleObserve200By13.snap index 7706f4dd..bb219663 100644 --- a/.forge-snapshots/FullOracleObserve200By13.snap +++ b/.forge-snapshots/FullOracleObserve200By13.snap @@ -1 +1 @@ -23377 \ No newline at end of file +22933 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve200By13Plus5.snap b/.forge-snapshots/FullOracleObserve200By13Plus5.snap index 8afa5484..6eb59a1d 100644 --- a/.forge-snapshots/FullOracleObserve200By13Plus5.snap +++ b/.forge-snapshots/FullOracleObserve200By13Plus5.snap @@ -1 +1 @@ -23624 \ No newline at end of file +23180 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve5After5Seconds.snap b/.forge-snapshots/FullOracleObserve5After5Seconds.snap index f66ebbd5..94c197e9 100644 --- a/.forge-snapshots/FullOracleObserve5After5Seconds.snap +++ b/.forge-snapshots/FullOracleObserve5After5Seconds.snap @@ -1 +1 @@ -2798 \ No newline at end of file +2738 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveOldest.snap b/.forge-snapshots/FullOracleObserveOldest.snap index 9db3df4e..75080690 100644 --- a/.forge-snapshots/FullOracleObserveOldest.snap +++ b/.forge-snapshots/FullOracleObserveOldest.snap @@ -1 +1 @@ -22396 \ No newline at end of file +21892 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap b/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap index b2f26cf1..9b54c31b 100644 --- a/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap +++ b/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap @@ -1 +1 @@ -22695 \ No newline at end of file +22191 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveZero.snap b/.forge-snapshots/FullOracleObserveZero.snap index f91847e9..2a55d550 100644 --- a/.forge-snapshots/FullOracleObserveZero.snap +++ b/.forge-snapshots/FullOracleObserveZero.snap @@ -1 +1 @@ -2130 \ No newline at end of file +2070 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddInitialLiquidity.snap b/.forge-snapshots/FullRangeAddInitialLiquidity.snap index ef62f828..94ac0e08 100644 --- a/.forge-snapshots/FullRangeAddInitialLiquidity.snap +++ b/.forge-snapshots/FullRangeAddInitialLiquidity.snap @@ -1 +1 @@ -410761 \ No newline at end of file +407968 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddLiquidity.snap b/.forge-snapshots/FullRangeAddLiquidity.snap index b3688dfa..d1198e0f 100644 --- a/.forge-snapshots/FullRangeAddLiquidity.snap +++ b/.forge-snapshots/FullRangeAddLiquidity.snap @@ -1 +1 @@ -204683 \ No newline at end of file +201962 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeFirstSwap.snap b/.forge-snapshots/FullRangeFirstSwap.snap index 2b5ad7d2..aef75115 100644 --- a/.forge-snapshots/FullRangeFirstSwap.snap +++ b/.forge-snapshots/FullRangeFirstSwap.snap @@ -1 +1 @@ -156432 \ No newline at end of file +153306 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index c2b5d0ef..3b5a43d1 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -897565 \ No newline at end of file +1112212 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidity.snap b/.forge-snapshots/FullRangeRemoveLiquidity.snap index bc1c95e2..58273980 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidity.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidity.snap @@ -1 +1 @@ -200057 \ No newline at end of file +197519 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap index b5d7708e..8e473407 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap @@ -1 +1 @@ -386095 \ No newline at end of file +379147 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSecondSwap.snap b/.forge-snapshots/FullRangeSecondSwap.snap index 9e12e78d..3f185fb2 100644 --- a/.forge-snapshots/FullRangeSecondSwap.snap +++ b/.forge-snapshots/FullRangeSecondSwap.snap @@ -1 +1 @@ -114700 \ No newline at end of file +111940 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSwap.snap b/.forge-snapshots/FullRangeSwap.snap index d9365d02..68f6f4d2 100644 --- a/.forge-snapshots/FullRangeSwap.snap +++ b/.forge-snapshots/FullRangeSwap.snap @@ -1 +1 @@ -154641 \ No newline at end of file +151523 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10Slots.snap b/.forge-snapshots/OracleGrow10Slots.snap index 3aa3cfac..f484e31f 100644 --- a/.forge-snapshots/OracleGrow10Slots.snap +++ b/.forge-snapshots/OracleGrow10Slots.snap @@ -1 +1 @@ -254711 \ No newline at end of file +254660 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap index 50fc054a..83917a8d 100644 --- a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap +++ b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap @@ -1 +1 @@ -245393 \ No newline at end of file +245360 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1Slot.snap b/.forge-snapshots/OracleGrow1Slot.snap index 15a052b9..8f98b8b1 100644 --- a/.forge-snapshots/OracleGrow1Slot.snap +++ b/.forge-snapshots/OracleGrow1Slot.snap @@ -1 +1 @@ -54893 \ No newline at end of file +54869 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap index d6664238..ee2ae68d 100644 --- a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap +++ b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap @@ -1 +1 @@ -45575 \ No newline at end of file +45569 \ No newline at end of file diff --git a/.forge-snapshots/OracleInitialize.snap b/.forge-snapshots/OracleInitialize.snap index 3039612c..1e8b26e0 100644 --- a/.forge-snapshots/OracleInitialize.snap +++ b/.forge-snapshots/OracleInitialize.snap @@ -1 +1 @@ -72361 \ No newline at end of file +72316 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap b/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap index ae13ac3f..a695bf26 100644 --- a/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap +++ b/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap @@ -1 +1 @@ -6618 \ No newline at end of file +6492 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveCurrentTime.snap b/.forge-snapshots/OracleObserveCurrentTime.snap index f91847e9..2a55d550 100644 --- a/.forge-snapshots/OracleObserveCurrentTime.snap +++ b/.forge-snapshots/OracleObserveCurrentTime.snap @@ -1 +1 @@ -2130 \ No newline at end of file +2070 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap b/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap index f91847e9..2a55d550 100644 --- a/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap +++ b/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap @@ -1 +1 @@ -2130 \ No newline at end of file +2070 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLast20Seconds.snap b/.forge-snapshots/OracleObserveLast20Seconds.snap index b63da1de..5265bba3 100644 --- a/.forge-snapshots/OracleObserveLast20Seconds.snap +++ b/.forge-snapshots/OracleObserveLast20Seconds.snap @@ -1 +1 @@ -88543 \ No newline at end of file +86878 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLatestEqual.snap b/.forge-snapshots/OracleObserveLatestEqual.snap index f91847e9..2a55d550 100644 --- a/.forge-snapshots/OracleObserveLatestEqual.snap +++ b/.forge-snapshots/OracleObserveLatestEqual.snap @@ -1 +1 @@ -2130 \ No newline at end of file +2070 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLatestTransform.snap b/.forge-snapshots/OracleObserveLatestTransform.snap index bc61a749..a08fb8e1 100644 --- a/.forge-snapshots/OracleObserveLatestTransform.snap +++ b/.forge-snapshots/OracleObserveLatestTransform.snap @@ -1 +1 @@ -2771 \ No newline at end of file +2687 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveMiddle.snap b/.forge-snapshots/OracleObserveMiddle.snap index ba7fb703..d0974c4f 100644 --- a/.forge-snapshots/OracleObserveMiddle.snap +++ b/.forge-snapshots/OracleObserveMiddle.snap @@ -1 +1 @@ -6807 \ No newline at end of file +6684 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveOldest.snap b/.forge-snapshots/OracleObserveOldest.snap index 3ee11622..05796bbf 100644 --- a/.forge-snapshots/OracleObserveOldest.snap +++ b/.forge-snapshots/OracleObserveOldest.snap @@ -1 +1 @@ -6319 \ No newline at end of file +6193 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveSinceMostRecent.snap b/.forge-snapshots/OracleObserveSinceMostRecent.snap index 204ec243..ed8dd329 100644 --- a/.forge-snapshots/OracleObserveSinceMostRecent.snap +++ b/.forge-snapshots/OracleObserveSinceMostRecent.snap @@ -1 +1 @@ -3466 \ No newline at end of file +3382 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap index 0aef60be..1ba4a8d1 100644 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ b/.forge-snapshots/TWAMMSubmitOrder.snap @@ -1 +1 @@ -146158 \ No newline at end of file +145648 \ No newline at end of file diff --git a/contracts/interfaces/IQuoter.sol b/contracts/interfaces/IQuoter.sol new file mode 100644 index 00000000..90a390fc --- /dev/null +++ b/contracts/interfaces/IQuoter.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PathKey} from "../libraries/PathKey.sol"; + +/// @title Quoter Interface +/// @notice Supports quoting the delta amounts from exact input or exact output swaps. +/// @notice For each pool also tells you the number of initialized ticks loaded 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 IQuoter { + error InvalidLockAcquiredSender(); + error InvalidLockCaller(); + error InvalidQuoteBatchParams(); + error InsufficientAmountOut(); + error LockFailure(); + error NotSelf(); + error UnexpectedRevertBytes(bytes revertData); + + struct PoolDeltas { + int128 currency0Delta; + int128 currency1Delta; + } + + struct QuoteExactSingleParams { + PoolKey poolKey; + bool zeroForOne; + address recipient; + uint128 exactAmount; + uint160 sqrtPriceLimitX96; + bytes hookData; + } + + struct QuoteExactParams { + Currency exactCurrency; + PathKey[] path; + address recipient; + uint128 exactAmount; + } + + /// @notice Returns the delta amounts for a given exact input swap of a single pool + /// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` + /// poolKey The key for identifying a V4 pool + /// zeroForOne If the swap is from currency0 to currency1 + /// recipient The intended recipient of the output tokens + /// exactAmount The desired input amount + /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// hookData arbitrary hookData to pass into the associated hooks + /// @return deltaAmounts Delta amounts resulted from the swap + /// @return sqrtPriceX96After The sqrt price of the pool after the swap + /// @return initializedTicksLoaded The number of initialized ticks that the swap loaded + function quoteExactInputSingle(QuoteExactSingleParams calldata params) + external + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded); + + /// @notice Returns the delta amounts along the swap path for a given exact input swap + /// @param params the params for the quote, encoded as 'QuoteExactInputParams' + /// currencyIn The input currency of the swap + /// path The path of the swap encoded as PathKeys that contains currency, fee, tickSpacing, and hook info + /// recipient The intended recipient of the output tokens + /// exactAmount The desired input amount + /// @return deltaAmounts Delta amounts along the path resulted from the swap + /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path + /// @return initializedTicksLoadedList List of the initialized ticks that the swap loaded for each pool in the path + function quoteExactInput(QuoteExactParams memory params) + external + returns ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ); + + /// @notice Returns the delta amounts for a given exact output swap of a single pool + /// @param params The params for the quote, encoded as `QuoteExactOutputSingleParams` + /// poolKey The key for identifying a V4 pool + /// zeroForOne If the swap is from currency0 to currency1 + /// recipient The intended recipient of the output tokens + /// exactAmount The desired input amount + /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// hookData arbitrary hookData to pass into the associated hooks + /// @return deltaAmounts Delta amounts resulted from the swap + /// @return sqrtPriceX96After The sqrt price of the pool after the swap + /// @return initializedTicksLoaded The number of initialized ticks that the swap loaded + function quoteExactOutputSingle(QuoteExactSingleParams calldata params) + external + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded); + + /// @notice Returns the delta amounts along the swap path for a given exact output swap + /// @param params the params for the quote, encoded as 'QuoteExactOutputParams' + /// currencyOut The output currency of the swap + /// path The path of the swap encoded as PathKeys that contains currency, fee, tickSpacing, and hook info + /// recipient The intended recipient of the output tokens + /// exactAmount The desired output amount + /// @return deltaAmounts Delta amounts along the path resulted from the swap + /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path + /// @return initializedTicksLoadedList List of the initialized ticks that the swap loaded for each pool in the path + function quoteExactOutput(QuoteExactParams memory params) + external + returns ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ); +} diff --git a/contracts/lens/Quoter.sol b/contracts/lens/Quoter.sol new file mode 100644 index 00000000..8b2b16e0 --- /dev/null +++ b/contracts/lens/Quoter.sol @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {ILockCallback} from "@uniswap/v4-core/src/interfaces/callback/ILockCallback.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {IQuoter} from "../interfaces/IQuoter.sol"; +import {PoolTicksCounter} from "../libraries/PoolTicksCounter.sol"; +import {PathKey, PathKeyLib} from "../libraries/PathKey.sol"; + +contract Quoter is IQuoter, ILockCallback { + using Hooks for IHooks; + using PoolIdLibrary for PoolKey; + using PathKeyLib for PathKey; + + /// @dev cache used to check a safety condition in exact output swaps. + uint128 private amountOutCached; + + // v4 Singleton contract + IPoolManager public immutable manager; + + /// @dev min valid reason is 3-words long + /// @dev int128[2] + sqrtPriceX96After padded to 32bytes + intializeTicksLoaded padded to 32bytes + uint256 internal constant MINIMUM_VALID_RESPONSE_LENGTH = 96; + + /// @dev Only this address may call this function + modifier selfOnly() { + if (msg.sender != address(this)) revert NotSelf(); + _; + } + + constructor(address _poolManager) { + manager = IPoolManager(_poolManager); + } + + /// @inheritdoc IQuoter + function quoteExactInputSingle(QuoteExactSingleParams memory params) + public + override + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) + { + try manager.lock(address(this), abi.encodeWithSelector(this._quoteExactInputSingle.selector, params)) {} + catch (bytes memory reason) { + return _handleRevertSingle(reason); + } + } + + /// @inheritdoc IQuoter + function quoteExactInput(QuoteExactParams memory params) + external + returns ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) + { + try manager.lock(address(this), abi.encodeWithSelector(this._quoteExactInput.selector, params)) {} + catch (bytes memory reason) { + return _handleRevert(reason); + } + } + + /// @inheritdoc IQuoter + function quoteExactOutputSingle(QuoteExactSingleParams memory params) + public + override + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) + { + try manager.lock(address(this), abi.encodeWithSelector(this._quoteExactOutputSingle.selector, params)) {} + catch (bytes memory reason) { + if (params.sqrtPriceLimitX96 == 0) delete amountOutCached; + return _handleRevertSingle(reason); + } + } + + /// @inheritdoc IQuoter + function quoteExactOutput(QuoteExactParams memory params) + public + override + returns ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) + { + try manager.lock(address(this), abi.encodeWithSelector(this._quoteExactOutput.selector, params)) {} + catch (bytes memory reason) { + return _handleRevert(reason); + } + } + + /// @inheritdoc ILockCallback + function lockAcquired(address lockCaller, bytes calldata data) external returns (bytes memory) { + if (msg.sender != address(manager)) { + revert InvalidLockAcquiredSender(); + } + if (lockCaller != address(this)) { + revert InvalidLockCaller(); + } + + (bool success, bytes memory returnData) = address(this).call(data); + if (success) return returnData; + if (returnData.length == 0) revert LockFailure(); + // if the call failed, bubble up the reason + /// @solidity memory-safe-assembly + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } + + /// @dev check revert bytes and pass through if considered valid; otherwise revert with different message + function validateRevertReason(bytes memory reason) private pure returns (bytes memory) { + if (reason.length < MINIMUM_VALID_RESPONSE_LENGTH) { + revert UnexpectedRevertBytes(reason); + } + return reason; + } + + /// @dev parse revert bytes from a single-pool quote + function _handleRevertSingle(bytes memory reason) + private + pure + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) + { + reason = validateRevertReason(reason); + (deltaAmounts, sqrtPriceX96After, initializedTicksLoaded) = abi.decode(reason, (int128[], uint160, uint32)); + } + + /// @dev parse revert bytes from a potentially multi-hop quote and return the delta amounts, sqrtPriceX96After, and initializedTicksLoaded + function _handleRevert(bytes memory reason) + private + pure + returns ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) + { + reason = validateRevertReason(reason); + (deltaAmounts, sqrtPriceX96AfterList, initializedTicksLoadedList) = + abi.decode(reason, (int128[], uint160[], uint32[])); + } + + /// @dev quote an ExactInput swap along a path of tokens, then revert with the result + function _quoteExactInput(QuoteExactParams memory params) public selfOnly returns (bytes memory) { + uint256 pathLength = params.path.length; + + int128[] memory deltaAmounts = new int128[](pathLength + 1); + uint160[] memory sqrtPriceX96AfterList = new uint160[](pathLength); + uint32[] memory initializedTicksLoadedList = new uint32[](pathLength); + Currency prevCurrencyOut; + uint128 prevAmountOut; + + for (uint256 i = 0; i < pathLength; i++) { + (PoolKey memory poolKey, bool zeroForOne) = + params.path[i].getPoolAndSwapDirection(i == 0 ? params.exactCurrency : prevCurrencyOut); + (, int24 tickBefore,) = manager.getSlot0(poolKey.toId()); + + (BalanceDelta curDeltas, uint160 sqrtPriceX96After, int24 tickAfter) = _swap( + poolKey, + zeroForOne, + int256(int128(i == 0 ? params.exactAmount : prevAmountOut)), + 0, + params.path[i].hookData + ); + + (int128 deltaIn, int128 deltaOut) = + zeroForOne ? (curDeltas.amount0(), curDeltas.amount1()) : (curDeltas.amount1(), curDeltas.amount0()); + deltaAmounts[i] += deltaIn; + deltaAmounts[i + 1] += deltaOut; + + prevAmountOut = zeroForOne ? uint128(-curDeltas.amount1()) : uint128(-curDeltas.amount0()); + prevCurrencyOut = params.path[i].intermediateCurrency; + sqrtPriceX96AfterList[i] = sqrtPriceX96After; + initializedTicksLoadedList[i] = + PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, tickBefore, tickAfter); + } + bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96AfterList, initializedTicksLoadedList); + assembly { + revert(add(0x20, result), mload(result)) + } + } + + /// @dev quote an ExactInput swap on a pool, then revert with the result + function _quoteExactInputSingle(QuoteExactSingleParams memory params) public selfOnly returns (bytes memory) { + (, int24 tickBefore,) = manager.getSlot0(params.poolKey.toId()); + + (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) = _swap( + params.poolKey, + params.zeroForOne, + int256(int128(params.exactAmount)), + params.sqrtPriceLimitX96, + params.hookData + ); + + int128[] memory deltaAmounts = new int128[](2); + + deltaAmounts[0] = deltas.amount0(); + deltaAmounts[1] = deltas.amount1(); + + uint32 initializedTicksLoaded = + PoolTicksCounter.countInitializedTicksLoaded(manager, params.poolKey, tickBefore, tickAfter); + bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96After, initializedTicksLoaded); + assembly { + revert(add(0x20, result), mload(result)) + } + } + + /// @dev quote an ExactOutput swap along a path of tokens, then revert with the result + function _quoteExactOutput(QuoteExactParams memory params) public selfOnly returns (bytes memory) { + uint256 pathLength = params.path.length; + + int128[] memory deltaAmounts = new int128[](pathLength + 1); + uint160[] memory sqrtPriceX96AfterList = new uint160[](pathLength); + uint32[] memory initializedTicksLoadedList = new uint32[](pathLength); + Currency prevCurrencyIn; + uint128 prevAmountIn; + uint128 curAmountOut; + + for (uint256 i = pathLength; i > 0; i--) { + curAmountOut = i == pathLength ? params.exactAmount : prevAmountIn; + amountOutCached = curAmountOut; + + (PoolKey memory poolKey, bool oneForZero) = PathKeyLib.getPoolAndSwapDirection( + params.path[i - 1], i == pathLength ? params.exactCurrency : prevCurrencyIn + ); + + (, int24 tickBefore,) = manager.getSlot0(poolKey.toId()); + + (BalanceDelta curDeltas, uint160 sqrtPriceX96After, int24 tickAfter) = + _swap(poolKey, !oneForZero, -int256(uint256(curAmountOut)), 0, params.path[i - 1].hookData); + + // always clear because sqrtPriceLimitX96 is set to 0 always + delete amountOutCached; + (int128 deltaIn, int128 deltaOut) = + !oneForZero ? (curDeltas.amount0(), curDeltas.amount1()) : (curDeltas.amount1(), curDeltas.amount0()); + deltaAmounts[i - 1] += deltaIn; + deltaAmounts[i] += deltaOut; + + prevAmountIn = !oneForZero ? uint128(curDeltas.amount0()) : uint128(curDeltas.amount1()); + prevCurrencyIn = params.path[i - 1].intermediateCurrency; + sqrtPriceX96AfterList[i - 1] = sqrtPriceX96After; + initializedTicksLoadedList[i - 1] = + PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, tickBefore, tickAfter); + } + bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96AfterList, initializedTicksLoadedList); + assembly { + revert(add(0x20, result), mload(result)) + } + } + + /// @dev quote an ExactOutput swap on a pool, then revert with the result + function _quoteExactOutputSingle(QuoteExactSingleParams memory params) public selfOnly returns (bytes memory) { + // if no price limit has been specified, cache the output amount for comparison in the swap callback + if (params.sqrtPriceLimitX96 == 0) amountOutCached = params.exactAmount; + + (, int24 tickBefore,) = manager.getSlot0(params.poolKey.toId()); + (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) = _swap( + params.poolKey, + params.zeroForOne, + -int256(uint256(params.exactAmount)), + params.sqrtPriceLimitX96, + params.hookData + ); + + if (amountOutCached != 0) delete amountOutCached; + int128[] memory deltaAmounts = new int128[](2); + + deltaAmounts[0] = deltas.amount0(); + deltaAmounts[1] = deltas.amount1(); + + uint32 initializedTicksLoaded = + PoolTicksCounter.countInitializedTicksLoaded(manager, params.poolKey, tickBefore, tickAfter); + bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96After, initializedTicksLoaded); + assembly { + revert(add(0x20, result), mload(result)) + } + } + + /// @dev Execute a swap and return the amounts delta, as well as relevant pool state + /// @notice if amountSpecified > 0, the swap is exactInput, otherwise exactOutput + function _swap( + PoolKey memory poolKey, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + bytes memory hookData + ) private returns (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) { + deltas = manager.swap( + poolKey, + IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: _sqrtPriceLimitOrDefault(sqrtPriceLimitX96, zeroForOne) + }), + hookData + ); + // only exactOut case + if (amountOutCached != 0 && amountOutCached != uint128(zeroForOne ? -deltas.amount1() : -deltas.amount0())) { + revert InsufficientAmountOut(); + } + (sqrtPriceX96After, tickAfter,) = manager.getSlot0(poolKey.toId()); + } + + /// @dev return either the sqrtPriceLimit from user input, or the max/min value possible depending on trade direction + function _sqrtPriceLimitOrDefault(uint160 sqrtPriceLimitX96, bool zeroForOne) private pure returns (uint160) { + return sqrtPriceLimitX96 == 0 + ? zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1 + : sqrtPriceLimitX96; + } +} diff --git a/contracts/libraries/LiquidityAmounts.sol b/contracts/libraries/LiquidityAmounts.sol index 845cc6e0..742e48f5 100644 --- a/contracts/libraries/LiquidityAmounts.sol +++ b/contracts/libraries/LiquidityAmounts.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +pragma solidity ^0.8.20; import "@uniswap/v4-core/src/libraries/FullMath.sol"; import "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; diff --git a/contracts/libraries/PathKey.sol b/contracts/libraries/PathKey.sol new file mode 100644 index 00000000..f9d5da33 --- /dev/null +++ b/contracts/libraries/PathKey.sol @@ -0,0 +1,30 @@ +//SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.20; + +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; + +struct PathKey { + Currency intermediateCurrency; + uint24 fee; + int24 tickSpacing; + IHooks hooks; + bytes hookData; +} + +library PathKeyLib { + function getPoolAndSwapDirection(PathKey memory params, Currency currencyIn) + internal + pure + returns (PoolKey memory poolKey, bool zeroForOne) + { + (Currency currency0, Currency currency1) = currencyIn < params.intermediateCurrency + ? (currencyIn, params.intermediateCurrency) + : (params.intermediateCurrency, currencyIn); + + zeroForOne = currencyIn == currency0; + poolKey = PoolKey(currency0, currency1, params.fee, params.tickSpacing, params.hooks); + } +} diff --git a/contracts/libraries/PoolTicksCounter.sol b/contracts/libraries/PoolTicksCounter.sol new file mode 100644 index 00000000..b0e9ab5b --- /dev/null +++ b/contracts/libraries/PoolTicksCounter.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.8.20; + +import {PoolGetters} from "./PoolGetters.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; + +library PoolTicksCounter { + using PoolIdLibrary for PoolKey; + + /// @dev This function counts the number of initialized ticks that would incur a gas cost between tickBefore and tickAfter. + /// When tickBefore and/or tickAfter themselves are initialized, the logic over whether we should count them depends on the + /// direction of the swap. If we are swapping upwards (tickAfter > tickBefore) we don't want to count tickBefore but we do + /// want to count tickAfter. The opposite is true if we are swapping downwards. + function countInitializedTicksLoaded(IPoolManager self, PoolKey memory key, int24 tickBefore, int24 tickAfter) + internal + view + returns (uint32 initializedTicksLoaded) + { + int16 wordPosLower; + int16 wordPosHigher; + uint8 bitPosLower; + uint8 bitPosHigher; + bool tickBeforeInitialized; + bool tickAfterInitialized; + + { + // Get the key and offset in the tick bitmap of the active tick before and after the swap. + int16 wordPos = int16((tickBefore / key.tickSpacing) >> 8); + uint8 bitPos = uint8(uint24((tickBefore / key.tickSpacing) % 256)); + + int16 wordPosAfter = int16((tickAfter / key.tickSpacing) >> 8); + uint8 bitPosAfter = uint8(uint24((tickAfter / key.tickSpacing) % 256)); + + // In the case where tickAfter is initialized, we only want to count it if we are swapping downwards. + // If the initializable tick after the swap is initialized, our original tickAfter is a + // multiple of tick spacing, and we are swapping downwards we know that tickAfter is initialized + // and we shouldn't count it. + uint256 bmAfter = self.getPoolBitmapInfo(key.toId(), wordPosAfter); + //uint256 bmAfter = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPosAfter); + tickAfterInitialized = + ((bmAfter & (1 << bitPosAfter)) > 0) && ((tickAfter % key.tickSpacing) == 0) && (tickBefore > tickAfter); + + // In the case where tickBefore is initialized, we only want to count it if we are swapping upwards. + // Use the same logic as above to decide whether we should count tickBefore or not. + uint256 bmBefore = self.getPoolBitmapInfo(key.toId(), wordPos); + //uint256 bmBefore = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPos); + tickBeforeInitialized = + ((bmBefore & (1 << bitPos)) > 0) && ((tickBefore % key.tickSpacing) == 0) && (tickBefore < tickAfter); + + if (wordPos < wordPosAfter || (wordPos == wordPosAfter && bitPos <= bitPosAfter)) { + wordPosLower = wordPos; + bitPosLower = bitPos; + wordPosHigher = wordPosAfter; + bitPosHigher = bitPosAfter; + } else { + wordPosLower = wordPosAfter; + bitPosLower = bitPosAfter; + wordPosHigher = wordPos; + bitPosHigher = bitPos; + } + } + + // Count the number of initialized ticks crossed by iterating through the tick bitmap. + // Our first mask should include the lower tick and everything to its left. + uint256 mask = type(uint256).max << bitPosLower; + while (wordPosLower <= wordPosHigher) { + // If we're on the final tick bitmap page, ensure we only count up to our + // ending tick. + if (wordPosLower == wordPosHigher) { + mask = mask & (type(uint256).max >> (255 - bitPosHigher)); + } + + //uint256 bmLower = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPosLower); + uint256 bmLower = self.getPoolBitmapInfo(key.toId(), wordPosLower); + uint256 masked = bmLower & mask; + initializedTicksLoaded += countOneBits(masked); + wordPosLower++; + // Reset our mask so we consider all bits on the next iteration. + mask = type(uint256).max; + } + + if (tickAfterInitialized) { + initializedTicksLoaded -= 1; + } + + if (tickBeforeInitialized) { + initializedTicksLoaded -= 1; + } + + return initializedTicksLoaded; + } + + function countOneBits(uint256 x) private pure returns (uint16) { + uint16 bits = 0; + while (x != 0) { + bits++; + x &= (x - 1); + } + return bits; + } +} diff --git a/foundry.toml b/foundry.toml index 302fc02b..620d06a6 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,8 @@ src = 'contracts' out = 'foundry-out' solc_version = '0.8.20' -optimizer_runs = 800 +via_ir = true +optimizer_runs = 1000000 ffi = true fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] cancun = true diff --git a/test/Quoter.t.sol b/test/Quoter.t.sol new file mode 100644 index 00000000..056b0818 --- /dev/null +++ b/test/Quoter.t.sol @@ -0,0 +1,662 @@ +//SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {PathKey} from "../contracts/libraries/PathKey.sol"; +import {IQuoter} from "../contracts/interfaces/IQuoter.sol"; +import {Quoter} from "../contracts/lens/Quoter.sol"; +import {LiquidityAmounts} from "../contracts/libraries/LiquidityAmounts.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PoolModifyPositionTest} from "@uniswap/v4-core/src/test/PoolModifyPositionTest.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; + +contract QuoterTest is Test, Deployers { + using SafeCast for *; + using PoolIdLibrary for PoolKey; + + // Min tick for full range with tick spacing of 60 + int24 internal constant MIN_TICK = -887220; + // Max tick for full range with tick spacing of 60 + int24 internal constant MAX_TICK = -MIN_TICK; + + uint160 internal constant SQRT_RATIO_100_102 = 78447570448055484695608110440; + uint160 internal constant SQRT_RATIO_102_100 = 80016521857016594389520272648; + + uint256 internal constant CONTROLLER_GAS_LIMIT = 500000; + + Quoter quoter; + + PoolModifyPositionTest positionManager; + + MockERC20 token0; + MockERC20 token1; + MockERC20 token2; + + PoolKey key01; + PoolKey key02; + PoolKey key12; + + MockERC20[] tokenPath; + + function setUp() public { + deployFreshManagerAndRouters(); + quoter = new Quoter(address(manager)); + positionManager = new PoolModifyPositionTest(manager); + + // salts are chosen so that address(token0) < address(token2) && address(1) < address(token2) + bytes32 salt1 = "ffff"; + bytes32 salt2 = "gm"; + token0 = new MockERC20{salt: salt1}("Test0", "0", 18); + token0.mint(address(this), 2 ** 128); + token1 = new MockERC20{salt: salt2}("Test1", "1", 18); + token1.mint(address(this), 2 ** 128); + token2 = new MockERC20("Test2", "2", 18); + token2.mint(address(this), 2 ** 128); + + key01 = createPoolKey(token0, token1, address(0)); + key02 = createPoolKey(token0, token2, address(0)); + key12 = createPoolKey(token1, token2, address(0)); + setupPool(key01); + setupPool(key12); + setupPoolMultiplePositions(key02); + } + + function testQuoter_quoteExactInputSingle_ZeroForOne_MultiplePositions() public { + uint256 amountIn = 10000; + uint256 expectedAmountOut = 9871; + uint160 expectedSqrtPriceX96After = 78461846509168490764501028180; + + (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter + .quoteExactInputSingle( + IQuoter.QuoteExactSingleParams({ + poolKey: key02, + zeroForOne: true, + recipient: address(this), + exactAmount: uint128(amountIn), + sqrtPriceLimitX96: 0, + hookData: ZERO_BYTES + }) + ); + + assertEq(uint128(-deltaAmounts[1]), expectedAmountOut); + assertEq(sqrtPriceX96After, expectedSqrtPriceX96After); + assertEq(initializedTicksLoaded, 2); + } + + function testQuoter_quoteExactInputSingle_OneForZero_MultiplePositions() public { + uint256 amountIn = 10000; + uint256 expectedAmountOut = 9871; + uint160 expectedSqrtPriceX96After = 80001962924147897865541384515; + + (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter + .quoteExactInputSingle( + IQuoter.QuoteExactSingleParams({ + poolKey: key02, + zeroForOne: false, + recipient: address(this), + exactAmount: uint128(amountIn), + sqrtPriceLimitX96: 0, + hookData: ZERO_BYTES + }) + ); + + assertEq(uint128(-deltaAmounts[0]), expectedAmountOut); + assertEq(sqrtPriceX96After, expectedSqrtPriceX96After); + assertEq(initializedTicksLoaded, 2); + } + + // nested self-call into lockAcquired reverts + function testQuoter_callLockAcquired_reverts() public { + vm.expectRevert(IQuoter.InvalidLockAcquiredSender.selector); + vm.prank(address(manager)); + quoter.lockAcquired(address(quoter), abi.encodeWithSelector(quoter.lockAcquired.selector, address(this), "0x")); + } + + function testQuoter_quoteExactInput_0to2_2TicksLoaded() public { + tokenPath.push(token0); + tokenPath.push(token2); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(uint128(-deltaAmounts[1]), 9871); + assertEq(sqrtPriceX96AfterList[0], 78461846509168490764501028180); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactInput_0to2_2TicksLoaded_initialiedAfter() public { + tokenPath.push(token0); + tokenPath.push(token2); + + // The swap amount is set such that the active tick after the swap is -120. + // -120 is an initialized tick for this pool. We check that we don't count it. + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6200); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(uint128(-deltaAmounts[1]), 6143); + assertEq(sqrtPriceX96AfterList[0], 78757224507315167622282810783); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactInput_0to2_1TickLoaded() public { + tokenPath.push(token0); + tokenPath.push(token2); + + // The swap amount is set such that the active tick after the swap is -60. + // -60 is an initialized tick for this pool. We check that we don't count it. + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 4000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(uint128(-deltaAmounts[1]), 3971); + assertEq(sqrtPriceX96AfterList[0], 78926452400586371254602774705); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactInput_0to2_0TickLoaded_startingNotInitialized() public { + tokenPath.push(token0); + tokenPath.push(token2); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(uint128(-deltaAmounts[1]), 8); + assertEq(sqrtPriceX96AfterList[0], 79227483487511329217250071027); + assertEq(initializedTicksLoadedList[0], 0); + } + + function testQuoter_quoteExactInput_0to2_0TickLoaded_startingInitialized() public { + setupPoolWithZeroTickInitialized(key02); + tokenPath.push(token0); + tokenPath.push(token2); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(uint128(-deltaAmounts[1]), 8); + assertEq(sqrtPriceX96AfterList[0], 79227817515327498931091950511); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactInput_2to0_2TicksLoaded() public { + tokenPath.push(token2); + tokenPath.push(token0); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(-deltaAmounts[1], 9871); + assertEq(sqrtPriceX96AfterList[0], 80001962924147897865541384515); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactInput_2to0_2TicksLoaded_initialiedAfter() public { + tokenPath.push(token2); + tokenPath.push(token0); + + // The swap amount is set such that the active tick after the swap is 120. + // 120 is an initialized tick for this pool. We check that we don't count it. + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6250); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(-deltaAmounts[1], 6190); + assertEq(sqrtPriceX96AfterList[0], 79705728824507063507279123685); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactInput_2to0_0TickLoaded_startingInitialized() public { + setupPoolWithZeroTickInitialized(key02); + tokenPath.push(token2); + tokenPath.push(token0); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 200); + + // Tick 0 initialized. Tick after = 1 + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(-deltaAmounts[1], 198); + assertEq(sqrtPriceX96AfterList[0], 79235729830182478001034429156); + assertEq(initializedTicksLoadedList[0], 0); + } + + // 2->0 starting not initialized + function testQuoter_quoteExactInput_2to0_0TickLoaded_startingNotInitialized() public { + tokenPath.push(token2); + tokenPath.push(token0); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 103); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(-deltaAmounts[1], 101); + assertEq(sqrtPriceX96AfterList[0], 79235858216754624215638319723); + assertEq(initializedTicksLoadedList[0], 0); + } + + function testQuoter_quoteExactInput_2to1() public { + tokenPath.push(token2); + tokenPath.push(token1); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + assertEq(-deltaAmounts[1], 9871); + assertEq(sqrtPriceX96AfterList[0], 80018067294531553039351583520); + assertEq(initializedTicksLoadedList[0], 0); + } + + function testQuoter_quoteExactInput_0to2to1() public { + tokenPath.push(token0); + tokenPath.push(token2); + tokenPath.push(token1); + IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactInput(params); + + assertEq(-deltaAmounts[2], 9745); + assertEq(sqrtPriceX96AfterList[0], 78461846509168490764501028180); + assertEq(sqrtPriceX96AfterList[1], 80007846861567212939802016351); + assertEq(initializedTicksLoadedList[0], 2); + assertEq(initializedTicksLoadedList[1], 0); + } + + function testQuoter_quoteExactOutputSingle_0to1() public { + (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter + .quoteExactOutputSingle( + IQuoter.QuoteExactSingleParams({ + poolKey: key01, + zeroForOne: true, + recipient: address(this), + exactAmount: type(uint128).max, + sqrtPriceLimitX96: SQRT_RATIO_100_102, + hookData: ZERO_BYTES + }) + ); + + assertEq(deltaAmounts[0], 9981); + assertEq(sqrtPriceX96After, SQRT_RATIO_100_102); + assertEq(initializedTicksLoaded, 0); + } + + function testQuoter_quoteExactOutputSingle_1to0() public { + (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter + .quoteExactOutputSingle( + IQuoter.QuoteExactSingleParams({ + poolKey: key01, + zeroForOne: false, + recipient: address(this), + exactAmount: type(uint128).max, + sqrtPriceLimitX96: SQRT_RATIO_102_100, + hookData: ZERO_BYTES + }) + ); + + assertEq(deltaAmounts[1], 9981); + assertEq(sqrtPriceX96After, SQRT_RATIO_102_100); + assertEq(initializedTicksLoaded, 0); + } + + function testQuoter_quoteExactOutput_0to2_2TicksLoaded() public { + tokenPath.push(token0); + tokenPath.push(token2); + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 15273); + assertEq(sqrtPriceX96AfterList[0], 78055527257643669242286029831); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactOutput_0to2_1TickLoaded_initialiedAfter() public { + tokenPath.push(token0); + tokenPath.push(token2); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6143); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 6200); + assertEq(sqrtPriceX96AfterList[0], 78757225449310403327341205211); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactOutput_0to2_1TickLoaded() public { + tokenPath.push(token0); + tokenPath.push(token2); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 4000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 4029); + assertEq(sqrtPriceX96AfterList[0], 78924219757724709840818372098); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactOutput_0to2_0TickLoaded_startingInitialized() public { + setupPoolWithZeroTickInitialized(key02); + tokenPath.push(token0); + tokenPath.push(token2); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 100); + + // Tick 0 initialized. Tick after = 1 + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 102); + assertEq(sqrtPriceX96AfterList[0], 79224329176051641448521403903); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactOutput_0to2_0TickLoaded_startingNotInitialized() public { + tokenPath.push(token0); + tokenPath.push(token2); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 10); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 12); + assertEq(sqrtPriceX96AfterList[0], 79227408033628034983534698435); + assertEq(initializedTicksLoadedList[0], 0); + } + + function testQuoter_quoteExactOutput_2to0_2TicksLoaded() public { + tokenPath.push(token2); + tokenPath.push(token0); + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 15273); + assertEq(sqrtPriceX96AfterList[0], 80418414376567919517220409857); + assertEq(initializedTicksLoadedList.length, 1); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactOutput_2to0_2TicksLoaded_initialiedAfter() public { + tokenPath.push(token2); + tokenPath.push(token0); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6223); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 6283); + assertEq(sqrtPriceX96AfterList[0], 79708304437530892332449657932); + assertEq(initializedTicksLoadedList.length, 1); + assertEq(initializedTicksLoadedList[0], 2); + } + + function testQuoter_quoteExactOutput_2to0_1TickLoaded() public { + tokenPath.push(token2); + tokenPath.push(token0); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6000); + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 6055); + assertEq(sqrtPriceX96AfterList[0], 79690640184021170956740081887); + assertEq(initializedTicksLoadedList.length, 1); + assertEq(initializedTicksLoadedList[0], 1); + } + + function testQuoter_quoteExactOutput_2to1() public { + tokenPath.push(token2); + tokenPath.push(token1); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9871); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 10000); + assertEq(sqrtPriceX96AfterList[0], 80018020393569259756601362385); + assertEq(initializedTicksLoadedList.length, 1); + assertEq(initializedTicksLoadedList[0], 0); + } + + function testQuoter_quoteExactOutput_0to2to1() public { + tokenPath.push(token0); + tokenPath.push(token2); + tokenPath.push(token1); + + IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9745); + + ( + int128[] memory deltaAmounts, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksLoadedList + ) = quoter.quoteExactOutput(params); + + assertEq(deltaAmounts[0], 10000); + assertEq(deltaAmounts[1], 0); + assertEq(deltaAmounts[2], -9745); + assertEq(sqrtPriceX96AfterList[0], 78461888503179331029803316753); + assertEq(sqrtPriceX96AfterList[1], 80007838904387594703933785072); + assertEq(initializedTicksLoadedList.length, 2); + assertEq(initializedTicksLoadedList[0], 2); + assertEq(initializedTicksLoadedList[1], 0); + } + + function createPoolKey(MockERC20 tokenA, MockERC20 tokenB, address hookAddr) + internal + pure + returns (PoolKey memory) + { + if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA); + return PoolKey(Currency.wrap(address(tokenA)), Currency.wrap(address(tokenB)), 3000, 60, IHooks(hookAddr)); + } + + function setupPool(PoolKey memory poolKey) internal { + initializeRouter.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); + MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); + positionManager.modifyPosition( + poolKey, + IPoolManager.ModifyPositionParams( + MIN_TICK, + MAX_TICK, + calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + ), + ZERO_BYTES + ); + } + + function setupPoolMultiplePositions(PoolKey memory poolKey) internal { + initializeRouter.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); + MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); + positionManager.modifyPosition( + poolKey, + IPoolManager.ModifyPositionParams( + MIN_TICK, + MAX_TICK, + calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + ), + ZERO_BYTES + ); + positionManager.modifyPosition( + poolKey, + IPoolManager.ModifyPositionParams( + -60, 60, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -60, 60, 100, 100).toInt256() + ), + ZERO_BYTES + ); + positionManager.modifyPosition( + poolKey, + IPoolManager.ModifyPositionParams( + -120, 120, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -120, 120, 100, 100).toInt256() + ), + ZERO_BYTES + ); + } + + function setupPoolWithZeroTickInitialized(PoolKey memory poolKey) internal { + PoolId poolId = poolKey.toId(); + (uint160 sqrtPriceX96,,) = manager.getSlot0(poolId); + if (sqrtPriceX96 == 0) { + initializeRouter.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + } + + MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); + MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); + positionManager.modifyPosition( + poolKey, + IPoolManager.ModifyPositionParams( + MIN_TICK, + MAX_TICK, + calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256() + ), + ZERO_BYTES + ); + positionManager.modifyPosition( + poolKey, + IPoolManager.ModifyPositionParams( + 0, 60, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, 0, 60, 100, 100).toInt256() + ), + ZERO_BYTES + ); + positionManager.modifyPosition( + poolKey, + IPoolManager.ModifyPositionParams( + -120, 0, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -120, 0, 100, 100).toInt256() + ), + ZERO_BYTES + ); + } + + function calculateLiquidityFromAmounts( + uint160 sqrtRatioX96, + int24 tickLower, + int24 tickUpper, + uint256 amount0, + uint256 amount1 + ) internal pure returns (uint128 liquidity) { + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); + liquidity = + LiquidityAmounts.getLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1); + } + + function getExactInputParams(MockERC20[] memory _tokenPath, uint256 amountIn) + internal + view + returns (IQuoter.QuoteExactParams memory params) + { + PathKey[] memory path = new PathKey[](_tokenPath.length - 1); + for (uint256 i = 0; i < _tokenPath.length - 1; i++) { + path[i] = PathKey(Currency.wrap(address(_tokenPath[i + 1])), 3000, 60, IHooks(address(0)), bytes("")); + } + + params.exactCurrency = Currency.wrap(address(_tokenPath[0])); + params.path = path; + params.recipient = address(this); + params.exactAmount = uint128(amountIn); + } + + function getExactOutputParams(MockERC20[] memory _tokenPath, uint256 amountOut) + internal + view + returns (IQuoter.QuoteExactParams memory params) + { + PathKey[] memory path = new PathKey[](_tokenPath.length - 1); + for (uint256 i = _tokenPath.length - 1; i > 0; i--) { + path[i - 1] = PathKey(Currency.wrap(address(_tokenPath[i - 1])), 3000, 60, IHooks(address(0)), bytes("")); + } + + params.exactCurrency = Currency.wrap(address(_tokenPath[_tokenPath.length - 1])); + params.path = path; + params.recipient = address(this); + params.exactAmount = uint128(amountOut); + } +}