From 47e3c30ae8a0d7c086bf3e41bd0e7e3a854e280b Mon Sep 17 00:00:00 2001 From: Alice <34962750+hensha256@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:30:33 -0400 Subject: [PATCH] Quoter update (#349) * remove counting tick crossing * add gas estimate return * refactor into revert library * remove cache struct * cache path key * Change return types * remove sqrt price after logic * Revert with custom error * comments * typo * factor base contract * move cache into transient storage * Remove cache entirely * PR comments * Refactor QuoterRevert library * remove sqrtPriceLimit --- ...utSingle_oneForZero_multiplePositions.snap | 2 +- ...utSingle_zeroForOne_multiplePositions.snap | 2 +- .../Quoter_exactOutputSingle_oneForZero.snap | 2 +- .../Quoter_exactOutputSingle_zeroForOne.snap | 2 +- ...er_quoteExactInput_oneHop_1TickLoaded.snap | 2 +- ...oteExactInput_oneHop_initializedAfter.snap | 2 +- ...ExactInput_oneHop_startingInitialized.snap | 2 +- .../Quoter_quoteExactInput_twoHops.snap | 2 +- ...r_quoteExactOutput_oneHop_1TickLoaded.snap | 2 +- ..._quoteExactOutput_oneHop_2TicksLoaded.snap | 2 +- ...teExactOutput_oneHop_initializedAfter.snap | 2 +- ...xactOutput_oneHop_startingInitialized.snap | 2 +- .../Quoter_quoteExactOutput_twoHops.snap | 2 +- foundry.toml | 2 +- src/base/BaseV4Quoter.sol | 58 ++++ src/interfaces/IQuoter.sol | 53 +-- src/lens/Quoter.sol | 323 ++++------------- src/libraries/PoolTicksCounter.sol | 115 ------- src/libraries/QuoterRevert.sol | 49 +++ test/Quoter.t.sol | 324 ++++++------------ 20 files changed, 310 insertions(+), 640 deletions(-) create mode 100644 src/base/BaseV4Quoter.sol delete mode 100644 src/libraries/PoolTicksCounter.sol create mode 100644 src/libraries/QuoterRevert.sol diff --git a/.forge-snapshots/Quoter_exactInputSingle_oneForZero_multiplePositions.snap b/.forge-snapshots/Quoter_exactInputSingle_oneForZero_multiplePositions.snap index 0c97c3620..485e8f0d7 100644 --- a/.forge-snapshots/Quoter_exactInputSingle_oneForZero_multiplePositions.snap +++ b/.forge-snapshots/Quoter_exactInputSingle_oneForZero_multiplePositions.snap @@ -1 +1 @@ -159043 \ No newline at end of file +143930 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_exactInputSingle_zeroForOne_multiplePositions.snap b/.forge-snapshots/Quoter_exactInputSingle_zeroForOne_multiplePositions.snap index af5fa0fa6..f89390d96 100644 --- a/.forge-snapshots/Quoter_exactInputSingle_zeroForOne_multiplePositions.snap +++ b/.forge-snapshots/Quoter_exactInputSingle_zeroForOne_multiplePositions.snap @@ -1 +1 @@ -166396 \ No newline at end of file +149382 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_exactOutputSingle_oneForZero.snap b/.forge-snapshots/Quoter_exactOutputSingle_oneForZero.snap index f43e94582..a40f3f57a 100644 --- a/.forge-snapshots/Quoter_exactOutputSingle_oneForZero.snap +++ b/.forge-snapshots/Quoter_exactOutputSingle_oneForZero.snap @@ -1 +1 @@ -93637 \ No newline at end of file +78203 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_exactOutputSingle_zeroForOne.snap b/.forge-snapshots/Quoter_exactOutputSingle_zeroForOne.snap index 66a68794e..23153115b 100644 --- a/.forge-snapshots/Quoter_exactOutputSingle_zeroForOne.snap +++ b/.forge-snapshots/Quoter_exactOutputSingle_zeroForOne.snap @@ -1 +1 @@ -100303 \ No newline at end of file +82626 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactInput_oneHop_1TickLoaded.snap b/.forge-snapshots/Quoter_quoteExactInput_oneHop_1TickLoaded.snap index 87d96db17..a3ea8ad76 100644 --- a/.forge-snapshots/Quoter_quoteExactInput_oneHop_1TickLoaded.snap +++ b/.forge-snapshots/Quoter_quoteExactInput_oneHop_1TickLoaded.snap @@ -1 +1 @@ -141321 \ No newline at end of file +120491 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactInput_oneHop_initializedAfter.snap b/.forge-snapshots/Quoter_quoteExactInput_oneHop_initializedAfter.snap index 3315258ee..6dcb3b78d 100644 --- a/.forge-snapshots/Quoter_quoteExactInput_oneHop_initializedAfter.snap +++ b/.forge-snapshots/Quoter_quoteExactInput_oneHop_initializedAfter.snap @@ -1 +1 @@ -164528 \ No newline at end of file +145414 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactInput_oneHop_startingInitialized.snap b/.forge-snapshots/Quoter_quoteExactInput_oneHop_startingInitialized.snap index 954792b66..1f604e115 100644 --- a/.forge-snapshots/Quoter_quoteExactInput_oneHop_startingInitialized.snap +++ b/.forge-snapshots/Quoter_quoteExactInput_oneHop_startingInitialized.snap @@ -1 +1 @@ -98641 \ No newline at end of file +79437 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactInput_twoHops.snap b/.forge-snapshots/Quoter_quoteExactInput_twoHops.snap index b8ed8de2f..bb203fa98 100644 --- a/.forge-snapshots/Quoter_quoteExactInput_twoHops.snap +++ b/.forge-snapshots/Quoter_quoteExactInput_twoHops.snap @@ -1 +1 @@ -234806 \ No newline at end of file +201179 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_1TickLoaded.snap b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_1TickLoaded.snap index 8a6db3df9..e7385875b 100644 --- a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_1TickLoaded.snap +++ b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_1TickLoaded.snap @@ -1 +1 @@ -161346 \ No newline at end of file +119782 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_2TicksLoaded.snap b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_2TicksLoaded.snap index 7391d6be7..14b51340c 100644 --- a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_2TicksLoaded.snap +++ b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_2TicksLoaded.snap @@ -1 +1 @@ -191453 \ No newline at end of file +149919 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_initializedAfter.snap b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_initializedAfter.snap index cc4af5c55..c19a0a13c 100644 --- a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_initializedAfter.snap +++ b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_initializedAfter.snap @@ -1 +1 @@ -161661 \ No newline at end of file +119850 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_startingInitialized.snap b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_startingInitialized.snap index 5aed9cf81..c0333d8aa 100644 --- a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_startingInitialized.snap +++ b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_startingInitialized.snap @@ -1 +1 @@ -136430 \ No newline at end of file +96549 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactOutput_twoHops.snap b/.forge-snapshots/Quoter_quoteExactOutput_twoHops.snap index da4bedcf8..7acf5efcd 100644 --- a/.forge-snapshots/Quoter_quoteExactOutput_twoHops.snap +++ b/.forge-snapshots/Quoter_quoteExactOutput_twoHops.snap @@ -1 +1 @@ -275720 \ No newline at end of file +200630 \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 64572ba03..381668e8d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,7 +1,7 @@ [profile.default] out = 'foundry-out' solc_version = '0.8.26' -optimizer_runs = 1_000_000 +optimizer_runs = 44444444 via_ir = true ffi = true fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] diff --git a/src/base/BaseV4Quoter.sol b/src/base/BaseV4Quoter.sol new file mode 100644 index 000000000..312cf7d15 --- /dev/null +++ b/src/base/BaseV4Quoter.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {QuoterRevert} from "../libraries/QuoterRevert.sol"; +import {SafeCallback} from "../base/SafeCallback.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; + +abstract contract BaseV4Quoter is SafeCallback { + using QuoterRevert for *; + using PoolIdLibrary for PoolId; + + error NotEnoughLiquidity(PoolId poolId); + error NotSelf(); + error UnexpectedCallSuccess(); + + constructor(IPoolManager _poolManager) SafeCallback(_poolManager) {} + + /// @dev Only this address may call this function. Used to mimic internal functions, using an + /// external call to catch and parse revert reasons + modifier selfOnly() { + if (msg.sender != address(this)) revert NotSelf(); + _; + } + + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { + (bool success, bytes memory returnData) = address(this).call(data); + // Every quote path gathers a quote, and then reverts either with QuoteSwap(quoteAmount) or alternative error + if (success) revert UnexpectedCallSuccess(); + // Bubble the revert string, whether a valid quote or an alternative error + returnData.bubbleReason(); + } + + /// @dev Execute a swap and return the balance delta + /// @notice if amountSpecified < 0, the swap is exactInput, otherwise exactOutput + function _swap(PoolKey memory poolKey, bool zeroForOne, int256 amountSpecified, bytes calldata hookData) + internal + returns (BalanceDelta swapDelta) + { + swapDelta = poolManager.swap( + poolKey, + IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1 + }), + hookData + ); + + // Check that the pool was not illiquid. + int128 amountSpecifiedActual = (zeroForOne == (amountSpecified < 0)) ? swapDelta.amount0() : swapDelta.amount1(); + if (amountSpecifiedActual != amountSpecified) { + revert NotEnoughLiquidity(poolKey.toId()); + } + } +} diff --git a/src/interfaces/IQuoter.sol b/src/interfaces/IQuoter.sol index 8f113f4c6..9815d74b7 100644 --- a/src/interfaces/IQuoter.sol +++ b/src/interfaces/IQuoter.sol @@ -7,27 +7,14 @@ import {PathKey} from "../libraries/PathKey.sol"; /// @title Quoter Interface /// @notice Supports quoting the delta amounts for 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. +/// @notice For each pool also tells you 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 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; uint128 exactAmount; - uint160 sqrtPriceLimitX96; bytes hookData; } @@ -42,58 +29,44 @@ interface IQuoter { /// poolKey The key for identifying a V4 pool /// zeroForOne If the swap is from currency0 to currency1 /// 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 + /// @return amountOut The output quote for the exactIn swap + /// @return gasEstimate Estimated gas units used for the swap function quoteExactInputSingle(QuoteExactSingleParams memory params) external - returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded); + returns (uint256 amountOut, uint256 gasEstimate); /// @notice Returns the delta amounts along the swap path for a given exact input swap /// @param params the params for the quote, encoded as 'QuoteExactParams' /// currencyIn The input currency of the swap /// path The path of the swap encoded as PathKeys that contains currency, fee, tickSpacing, and hook info /// 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 + /// @return amountOut The output quote for the exactIn swap + /// @return gasEstimate Estimated gas units used for the swap function quoteExactInput(QuoteExactParams memory params) external - returns ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ); + returns (uint256 amountOut, uint256 gasEstimate); /// @notice Returns the delta amounts for a given exact output swap of a single pool /// @param params The params for the quote, encoded as `QuoteExactSingleParams` /// poolKey The key for identifying a V4 pool /// zeroForOne If the swap is from currency0 to currency1 /// exactAmount The desired output 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 + /// @return amountIn The input quote for the exactOut swap + /// @return gasEstimate Estimated gas units used for the swap function quoteExactOutputSingle(QuoteExactSingleParams memory params) external - returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded); + returns (uint256 amountIn, uint256 gasEstimate); /// @notice Returns the delta amounts along the swap path for a given exact output swap /// @param params the params for the quote, encoded as 'QuoteExactParams' /// currencyOut The output currency of the swap /// path The path of the swap encoded as PathKeys that contains currency, fee, tickSpacing, and hook info /// 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 + /// @return amountIn The input quote for the exactOut swap + /// @return gasEstimate Estimated gas units used for the swap function quoteExactOutput(QuoteExactParams memory params) external - returns ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ); + returns (uint256 amountIn, uint256 gasEstimate); } diff --git a/src/lens/Quoter.sol b/src/lens/Quoter.sol index 4f9741943..fd1597978 100644 --- a/src/lens/Quoter.sol +++ b/src/lens/Quoter.sol @@ -1,323 +1,138 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.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 {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {IQuoter} from "../interfaces/IQuoter.sol"; -import {PoolTicksCounter} from "../libraries/PoolTicksCounter.sol"; import {PathKey, PathKeyLibrary} from "../libraries/PathKey.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {SafeCallback} from "../base/SafeCallback.sol"; +import {QuoterRevert} from "../libraries/QuoterRevert.sol"; +import {BaseV4Quoter} from "../base/BaseV4Quoter.sol"; -contract Quoter is IQuoter, SafeCallback { - using PoolIdLibrary for PoolKey; +contract Quoter is IQuoter, BaseV4Quoter { using PathKeyLibrary for PathKey; - using StateLibrary for IPoolManager; - - /// @dev cache used to check a safety condition in exact output swaps. - uint128 private amountOutCached; - - /// @dev min valid reason is 6-words long (192 bytes) - /// @dev int128[2] includes 32 bytes for offset, 32 bytes for length, and 32 bytes for each element - /// @dev Plus sqrtPriceX96After padded to 32 bytes and initializedTicksLoaded padded to 32 bytes - uint256 internal constant MINIMUM_VALID_RESPONSE_LENGTH = 192; - - struct QuoteResult { - int128[] deltaAmounts; - uint160[] sqrtPriceX96AfterList; - uint32[] initializedTicksLoadedList; - } + using QuoterRevert for *; - struct QuoteCache { - BalanceDelta curDeltas; - uint128 prevAmount; - int128 deltaIn; - int128 deltaOut; - int24 tickBefore; - int24 tickAfter; - Currency prevCurrency; - uint160 sqrtPriceX96After; - } - - /// @dev Only this address may call this function - modifier selfOnly() { - if (msg.sender != address(this)) revert NotSelf(); - _; - } - - constructor(IPoolManager _poolManager) SafeCallback(_poolManager) {} + constructor(IPoolManager _poolManager) BaseV4Quoter(_poolManager) {} /// @inheritdoc IQuoter function quoteExactInputSingle(QuoteExactSingleParams memory params) - public - returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) + external + returns (uint256 amountOut, uint256 gasEstimate) { + uint256 gasBefore = gasleft(); try poolManager.unlock(abi.encodeCall(this._quoteExactInputSingle, (params))) {} catch (bytes memory reason) { - return _handleRevertSingle(reason); + gasEstimate = gasBefore - gasleft(); + // Extract the quote from QuoteSwap error, or throw if the quote failed + amountOut = reason.parseQuoteAmount(); } } /// @inheritdoc IQuoter function quoteExactInput(QuoteExactParams memory params) external - returns ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) + returns (uint256 amountOut, uint256 gasEstimate) { + uint256 gasBefore = gasleft(); try poolManager.unlock(abi.encodeCall(this._quoteExactInput, (params))) {} catch (bytes memory reason) { - return _handleRevert(reason); + gasEstimate = gasBefore - gasleft(); + // Extract the quote from QuoteSwap error, or throw if the quote failed + amountOut = reason.parseQuoteAmount(); } } /// @inheritdoc IQuoter function quoteExactOutputSingle(QuoteExactSingleParams memory params) - public - returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) + external + returns (uint256 amountIn, uint256 gasEstimate) { + uint256 gasBefore = gasleft(); try poolManager.unlock(abi.encodeCall(this._quoteExactOutputSingle, (params))) {} catch (bytes memory reason) { - if (params.sqrtPriceLimitX96 == 0) delete amountOutCached; - return _handleRevertSingle(reason); + gasEstimate = gasBefore - gasleft(); + // Extract the quote from QuoteSwap error, or throw if the quote failed + amountIn = reason.parseQuoteAmount(); } } /// @inheritdoc IQuoter function quoteExactOutput(QuoteExactParams memory params) - public - returns ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) + external + returns (uint256 amountIn, uint256 gasEstimate) { + uint256 gasBefore = gasleft(); try poolManager.unlock(abi.encodeCall(this._quoteExactOutput, (params))) {} catch (bytes memory reason) { - return _handleRevert(reason); - } - } - - function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { - (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 - assembly ("memory-safe") { - revert(add(returnData, 32), mload(returnData)) + gasEstimate = gasBefore - gasleft(); + // Extract the quote from QuoteSwap error, or throw if the quote failed + amountIn = reason.parseQuoteAmount(); } } - /// @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 calldata params) public selfOnly returns (bytes memory) { + /// @dev external function called within the _unlockCallback, to simulate an exact input swap, then revert with the result + function _quoteExactInput(QuoteExactParams calldata params) external selfOnly returns (bytes memory) { uint256 pathLength = params.path.length; - - QuoteResult memory result = QuoteResult({ - deltaAmounts: new int128[](pathLength + 1), - sqrtPriceX96AfterList: new uint160[](pathLength), - initializedTicksLoadedList: new uint32[](pathLength) - }); - QuoteCache memory cache; + BalanceDelta swapDelta; + uint128 amountIn = params.exactAmount; + Currency inputCurrency = params.exactCurrency; + PathKey calldata pathKey; for (uint256 i = 0; i < pathLength; i++) { - (PoolKey memory poolKey, bool zeroForOne) = - params.path[i].getPoolAndSwapDirection(i == 0 ? params.exactCurrency : cache.prevCurrency); - (, cache.tickBefore,,) = poolManager.getSlot0(poolKey.toId()); + pathKey = params.path[i]; + (PoolKey memory poolKey, bool zeroForOne) = pathKey.getPoolAndSwapDirection(inputCurrency); - (cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) = _swap( - poolKey, - zeroForOne, - -int256(int128(i == 0 ? params.exactAmount : cache.prevAmount)), - 0, - params.path[i].hookData - ); + swapDelta = _swap(poolKey, zeroForOne, -int256(int128(amountIn)), pathKey.hookData); - (cache.deltaIn, cache.deltaOut) = zeroForOne - ? (-cache.curDeltas.amount0(), -cache.curDeltas.amount1()) - : (-cache.curDeltas.amount1(), -cache.curDeltas.amount0()); - result.deltaAmounts[i] += cache.deltaIn; - result.deltaAmounts[i + 1] += cache.deltaOut; - - cache.prevAmount = zeroForOne ? uint128(cache.curDeltas.amount1()) : uint128(cache.curDeltas.amount0()); - cache.prevCurrency = params.path[i].intermediateCurrency; - result.sqrtPriceX96AfterList[i] = cache.sqrtPriceX96After; - result.initializedTicksLoadedList[i] = - PoolTicksCounter.countInitializedTicksLoaded(poolManager, poolKey, cache.tickBefore, cache.tickAfter); - } - bytes memory r = - abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); - assembly { - revert(add(0x20, r), mload(r)) + amountIn = zeroForOne ? uint128(swapDelta.amount1()) : uint128(swapDelta.amount0()); + inputCurrency = pathKey.intermediateCurrency; } + // amountIn after the loop actually holds the amountOut of the trade + amountIn.revertQuote(); } - /// @dev quote an ExactInput swap on a pool, then revert with the result - function _quoteExactInputSingle(QuoteExactSingleParams calldata params) public selfOnly returns (bytes memory) { - (, int24 tickBefore,,) = poolManager.getSlot0(params.poolKey.toId()); - - (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) = _swap( - params.poolKey, - params.zeroForOne, - -int256(int128(params.exactAmount)), - params.sqrtPriceLimitX96, - params.hookData - ); + /// @dev external function called within the _unlockCallback, to simulate a single-hop exact input swap, then revert with the result + function _quoteExactInputSingle(QuoteExactSingleParams calldata params) external selfOnly returns (bytes memory) { + BalanceDelta swapDelta = + _swap(params.poolKey, params.zeroForOne, -int256(int128(params.exactAmount)), params.hookData); - int128[] memory deltaAmounts = new int128[](2); - - deltaAmounts[0] = -deltas.amount0(); - deltaAmounts[1] = -deltas.amount1(); - - uint32 initializedTicksLoaded = - PoolTicksCounter.countInitializedTicksLoaded(poolManager, params.poolKey, tickBefore, tickAfter); - bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96After, initializedTicksLoaded); - assembly { - revert(add(0x20, result), mload(result)) - } + // the output delta of a swap is positive + uint256 amountOut = params.zeroForOne ? uint128(swapDelta.amount1()) : uint128(swapDelta.amount0()); + amountOut.revertQuote(); } - /// @dev quote an ExactOutput swap along a path of tokens, then revert with the result - function _quoteExactOutput(QuoteExactParams calldata params) public selfOnly returns (bytes memory) { + /// @dev external function called within the _unlockCallback, to simulate an exact output swap, then revert with the result + function _quoteExactOutput(QuoteExactParams calldata params) external selfOnly returns (bytes memory) { uint256 pathLength = params.path.length; - - QuoteResult memory result = QuoteResult({ - deltaAmounts: new int128[](pathLength + 1), - sqrtPriceX96AfterList: new uint160[](pathLength), - initializedTicksLoadedList: new uint32[](pathLength) - }); - QuoteCache memory cache; - uint128 curAmountOut; + BalanceDelta swapDelta; + uint128 amountOut = params.exactAmount; + Currency outputCurrency = params.exactCurrency; + PathKey calldata pathKey; for (uint256 i = pathLength; i > 0; i--) { - curAmountOut = i == pathLength ? params.exactAmount : cache.prevAmount; - amountOutCached = curAmountOut; - - (PoolKey memory poolKey, bool oneForZero) = PathKeyLibrary.getPoolAndSwapDirection( - params.path[i - 1], i == pathLength ? params.exactCurrency : cache.prevCurrency - ); + pathKey = params.path[i - 1]; + (PoolKey memory poolKey, bool oneForZero) = pathKey.getPoolAndSwapDirection(outputCurrency); - (, cache.tickBefore,,) = poolManager.getSlot0(poolKey.toId()); + swapDelta = _swap(poolKey, !oneForZero, int256(uint256(amountOut)), pathKey.hookData); - (cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) = - _swap(poolKey, !oneForZero, int256(uint256(curAmountOut)), 0, params.path[i - 1].hookData); + amountOut = oneForZero ? uint128(-swapDelta.amount1()) : uint128(-swapDelta.amount0()); - // always clear because sqrtPriceLimitX96 is set to 0 always - delete amountOutCached; - (cache.deltaIn, cache.deltaOut) = !oneForZero - ? (-cache.curDeltas.amount0(), -cache.curDeltas.amount1()) - : (-cache.curDeltas.amount1(), -cache.curDeltas.amount0()); - result.deltaAmounts[i - 1] += cache.deltaIn; - result.deltaAmounts[i] += cache.deltaOut; - - cache.prevAmount = !oneForZero ? uint128(-cache.curDeltas.amount0()) : uint128(-cache.curDeltas.amount1()); - cache.prevCurrency = params.path[i - 1].intermediateCurrency; - result.sqrtPriceX96AfterList[i - 1] = cache.sqrtPriceX96After; - result.initializedTicksLoadedList[i - 1] = - PoolTicksCounter.countInitializedTicksLoaded(poolManager, poolKey, cache.tickBefore, cache.tickAfter); - } - bytes memory r = - abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); - assembly { - revert(add(0x20, r), mload(r)) + outputCurrency = pathKey.intermediateCurrency; } + // amountOut after the loop exits actually holds the amountIn of the trade + amountOut.revertQuote(); } - /// @dev quote an ExactOutput swap on a pool, then revert with the result - function _quoteExactOutputSingle(QuoteExactSingleParams calldata params) public selfOnly returns (bytes memory) { - // if no price limit has been specified, cache the output amount for comparison inside the _swap function - if (params.sqrtPriceLimitX96 == 0) amountOutCached = params.exactAmount; - - (, int24 tickBefore,,) = poolManager.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(poolManager, 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 calldata hookData - ) private returns (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) { - deltas = poolManager.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,,) = poolManager.getSlot0(poolKey.toId()); - } + /// @dev external function called within the _unlockCallback, to simulate a single-hop exact output swap, then revert with the result + function _quoteExactOutputSingle(QuoteExactSingleParams calldata params) external selfOnly returns (bytes memory) { + BalanceDelta swapDelta = + _swap(params.poolKey, params.zeroForOne, int256(uint256(params.exactAmount)), params.hookData); - /// @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_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1 - : sqrtPriceLimitX96; + // the input delta of a swap is negative so we must flip it + uint256 amountIn = params.zeroForOne ? uint128(-swapDelta.amount0()) : uint128(-swapDelta.amount1()); + amountIn.revertQuote(); } } diff --git a/src/libraries/PoolTicksCounter.sol b/src/libraries/PoolTicksCounter.sol deleted file mode 100644 index c101a22fa..000000000 --- a/src/libraries/PoolTicksCounter.sol +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.0; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -/// @title Pool Ticks Counter -/// @notice Functions for counting the number of initialized ticks between two ticks -library PoolTicksCounter { - using PoolIdLibrary for PoolKey; - using StateLibrary for IPoolManager; - - struct TickCache { - int16 wordPosLower; - int16 wordPosHigher; - uint8 bitPosLower; - uint8 bitPosHigher; - bool tickBeforeInitialized; - bool tickAfterInitialized; - } - - /// @notice Count the number of initialized ticks between two ticks - /// @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. - /// @param self the IPoolManager - /// @param key the PoolKey of the pool - /// @param tickBefore the tick before the swap - /// @param tickAfter the tick after the swap - /// @return initializedTicksLoaded the number of initialized ticks loaded - function countInitializedTicksLoaded(IPoolManager self, PoolKey memory key, int24 tickBefore, int24 tickAfter) - internal - view - returns (uint32 initializedTicksLoaded) - { - TickCache memory cache; - - { - // 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.getTickBitmap(key.toId(), wordPosAfter); - cache.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.getTickBitmap(key.toId(), wordPos); - cache.tickBeforeInitialized = - ((bmBefore & (1 << bitPos)) > 0) && ((tickBefore % key.tickSpacing) == 0) && (tickBefore < tickAfter); - - if (wordPos < wordPosAfter || (wordPos == wordPosAfter && bitPos <= bitPosAfter)) { - cache.wordPosLower = wordPos; - cache.bitPosLower = bitPos; - cache.wordPosHigher = wordPosAfter; - cache.bitPosHigher = bitPosAfter; - } else { - cache.wordPosLower = wordPosAfter; - cache.bitPosLower = bitPosAfter; - cache.wordPosHigher = wordPos; - cache.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 << cache.bitPosLower; - while (cache.wordPosLower <= cache.wordPosHigher) { - // If we're on the final tick bitmap page, ensure we only count up to our - // ending tick. - if (cache.wordPosLower == cache.wordPosHigher) { - mask = mask & (type(uint256).max >> (255 - cache.bitPosHigher)); - } - - uint256 bmLower = self.getTickBitmap(key.toId(), cache.wordPosLower); - uint256 masked = bmLower & mask; - initializedTicksLoaded += countOneBits(masked); - cache.wordPosLower++; - // Reset our mask so we consider all bits on the next iteration. - mask = type(uint256).max; - } - - if (cache.tickAfterInitialized) { - initializedTicksLoaded -= 1; - } - - if (cache.tickBeforeInitialized) { - initializedTicksLoaded -= 1; - } - - return initializedTicksLoaded; - } - - /// @notice Count the number of set bits in a uint256 - /// @param x the uint256 to count the bits of - function countOneBits(uint256 x) private pure returns (uint16) { - uint16 bits = 0; - while (x != 0) { - bits++; - x &= (x - 1); - } - return bits; - } -} diff --git a/src/libraries/QuoterRevert.sol b/src/libraries/QuoterRevert.sol new file mode 100644 index 000000000..bb0eda905 --- /dev/null +++ b/src/libraries/QuoterRevert.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {ParseBytes} from "@uniswap/v4-core/src/libraries/ParseBytes.sol"; + +library QuoterRevert { + using QuoterRevert for bytes; + using ParseBytes for bytes; + + /// @notice error thrown when invalid revert bytes are thrown by the quote + error UnexpectedRevertBytes(bytes revertData); + + /// @notice error thrown containing the quote as the data, to be caught and parsed later + error QuoteSwap(uint256 amount); + + /// @notice reverts, where the revert data is the provided bytes + /// @dev called when quoting, to record the quote amount in an error + /// @dev QuoteSwap is used to differentiate this error from other errors thrown when simulating the swap + function revertQuote(uint256 quoteAmount) internal pure { + revert QuoteSwap(quoteAmount); + } + + /// @notice reverts using the revertData as the reason + /// @dev to bubble up both the valid QuoteSwap(amount) error, or an alternative error thrown during simulation + function bubbleReason(bytes memory revertData) internal pure { + // mload(revertData): the length of the revert data + // add(revertData, 0x20): a pointer to the start of the revert data + assembly ("memory-safe") { + revert(add(revertData, 0x20), mload(revertData)) + } + } + + /// @notice validates whether a revert reason is a valid swap quote or not + /// if valid, it decodes the quote to return. Otherwise it reverts. + function parseQuoteAmount(bytes memory reason) internal pure returns (uint256 quoteAmount) { + // If the error doesnt start with QuoteSwap, we know this isnt a valid quote to parse + // Instead it is another revert that was triggered somewhere in the simulation + if (reason.parseSelector() != QuoteSwap.selector) { + revert UnexpectedRevertBytes(reason); + } + + // reason -> reason+0x1f is the length of the reason string + // reason+0x20 -> reason+0x23 is the selector of QuoteSwap + // reason+0x24 -> reason+0x43 is the quoteAmount + assembly ("memory-safe") { + quoteAmount := mload(add(reason, 0x24)) + } + } +} diff --git a/test/Quoter.t.sol b/test/Quoter.t.sol index dbe7eeb07..8de8e4616 100644 --- a/test/Quoter.t.sol +++ b/test/Quoter.t.sol @@ -6,6 +6,7 @@ import {Test} from "forge-std/Test.sol"; import {PathKey} from "../src/libraries/PathKey.sol"; import {IQuoter} from "../src/interfaces/IQuoter.sol"; import {Quoter} from "../src/lens/Quoter.sol"; +import {BaseV4Quoter} from "../src/base/BaseV4Quoter.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; // v4-core @@ -83,52 +84,39 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function testQuoter_quoteExactInputSingle_ZeroForOne_MultiplePositions() public { uint256 amountIn = 10000; uint256 expectedAmountOut = 9871; - uint160 expectedSqrtPriceX96After = 78461846509168490764501028180; - (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter - .quoteExactInputSingle( + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInputSingle( IQuoter.QuoteExactSingleParams({ poolKey: key02, zeroForOne: true, exactAmount: uint128(amountIn), - sqrtPriceLimitX96: 0, hookData: ZERO_BYTES }) ); snapLastCall("Quoter_exactInputSingle_zeroForOne_multiplePositions"); - assertEq(uint128(-deltaAmounts[1]), expectedAmountOut); - assertEq(sqrtPriceX96After, expectedSqrtPriceX96After); - assertEq(initializedTicksLoaded, 2); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, expectedAmountOut); } function testQuoter_quoteExactInputSingle_OneForZero_MultiplePositions() public { uint256 amountIn = 10000; uint256 expectedAmountOut = 9871; - uint160 expectedSqrtPriceX96After = 80001962924147897865541384515; - (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter - .quoteExactInputSingle( + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInputSingle( IQuoter.QuoteExactSingleParams({ poolKey: key02, zeroForOne: false, exactAmount: uint128(amountIn), - sqrtPriceLimitX96: 0, hookData: ZERO_BYTES }) ); snapLastCall("Quoter_exactInputSingle_oneForZero_multiplePositions"); - assertEq(uint128(-deltaAmounts[0]), expectedAmountOut); - assertEq(sqrtPriceX96After, expectedSqrtPriceX96After); - assertEq(initializedTicksLoaded, 2); - } - - // nested self-call into unlockCallback reverts - function testQuoter_callUnlockCallback_reverts() public { - vm.expectRevert(IQuoter.LockFailure.selector); - vm.prank(address(manager)); - quoter.unlockCallback(abi.encodeWithSelector(quoter.unlockCallback.selector, address(this), "0x")); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, expectedAmountOut); } function testQuoter_quoteExactInput_0to2_2TicksLoaded() public { @@ -136,15 +124,11 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token2); IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactInput(params); + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); - assertEq(uint128(-deltaAmounts[1]), 9871); - assertEq(sqrtPriceX96AfterList[0], 78461846509168490764501028180); - assertEq(initializedTicksLoadedList[0], 2); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, 9871); } function testQuoter_quoteExactInput_0to2_2TicksLoaded_initializedAfter() public { @@ -155,15 +139,11 @@ contract QuoterTest is Test, Deployers, GasSnapshot { // -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); + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); - assertEq(uint128(-deltaAmounts[1]), 6143); - assertEq(sqrtPriceX96AfterList[0], 78757224507315167622282810783); - assertEq(initializedTicksLoadedList[0], 1); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, 6143); } function testQuoter_quoteExactInput_0to2_1TickLoaded() public { @@ -174,17 +154,13 @@ contract QuoterTest is Test, Deployers, GasSnapshot { // -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); + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); snapLastCall("Quoter_quoteExactInput_oneHop_1TickLoaded"); - assertEq(uint128(-deltaAmounts[1]), 3971); - assertEq(sqrtPriceX96AfterList[0], 78926452400586371254602774705); - assertEq(initializedTicksLoadedList[0], 1); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, 3971); } function testQuoter_quoteExactInput_0to2_0TickLoaded_startingNotInitialized() public { @@ -192,15 +168,11 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token2); IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactInput(params); + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); - assertEq(uint128(-deltaAmounts[1]), 8); - assertEq(sqrtPriceX96AfterList[0], 79227483487511329217250071027); - assertEq(initializedTicksLoadedList[0], 0); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, 8); } function testQuoter_quoteExactInput_0to2_0TickLoaded_startingInitialized() public { @@ -209,15 +181,11 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token2); IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactInput(params); + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); - assertEq(uint128(-deltaAmounts[1]), 8); - assertEq(sqrtPriceX96AfterList[0], 79227817515327498931091950511); - assertEq(initializedTicksLoadedList[0], 1); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, 8); } function testQuoter_quoteExactInput_2to0_2TicksLoaded() public { @@ -225,15 +193,11 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token0); IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactInput(params); + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); - assertEq(-deltaAmounts[1], 9871); - assertEq(sqrtPriceX96AfterList[0], 80001962924147897865541384515); - assertEq(initializedTicksLoadedList[0], 2); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, 9871); } function testQuoter_quoteExactInput_2to0_2TicksLoaded_initializedAfter() public { @@ -244,17 +208,13 @@ contract QuoterTest is Test, Deployers, GasSnapshot { // 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); + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); snapLastCall("Quoter_quoteExactInput_oneHop_initializedAfter"); - assertEq(-deltaAmounts[1], 6190); - assertEq(sqrtPriceX96AfterList[0], 79705728824507063507279123685); - assertEq(initializedTicksLoadedList[0], 2); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, 6190); } function testQuoter_quoteExactInput_2to0_0TickLoaded_startingInitialized() public { @@ -264,17 +224,13 @@ contract QuoterTest is Test, Deployers, GasSnapshot { 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); + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); snapLastCall("Quoter_quoteExactInput_oneHop_startingInitialized"); - assertEq(-deltaAmounts[1], 198); - assertEq(sqrtPriceX96AfterList[0], 79235729830182478001034429156); - assertEq(initializedTicksLoadedList[0], 0); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, 198); } // 2->0 starting not initialized @@ -283,15 +239,11 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token0); IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 103); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactInput(params); + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); - assertEq(-deltaAmounts[1], 101); - assertEq(sqrtPriceX96AfterList[0], 79235858216754624215638319723); - assertEq(initializedTicksLoadedList[0], 0); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, 101); } function testQuoter_quoteExactInput_2to1() public { @@ -299,14 +251,10 @@ contract QuoterTest is Test, Deployers, GasSnapshot { 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); + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, 9871); } function testQuoter_quoteExactInput_0to2to1() public { @@ -315,55 +263,47 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token1); IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactInput(params); + (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); snapLastCall("Quoter_quoteExactInput_twoHops"); - assertEq(-deltaAmounts[2], 9745); - assertEq(sqrtPriceX96AfterList[0], 78461846509168490764501028180); - assertEq(sqrtPriceX96AfterList[1], 80007846861567212939802016351); - assertEq(initializedTicksLoadedList[0], 2); - assertEq(initializedTicksLoadedList[1], 0); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountOut, 9745); } function testQuoter_quoteExactOutputSingle_0to1() public { - (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter - .quoteExactOutputSingle( + uint256 amountOut = 10000; + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutputSingle( IQuoter.QuoteExactSingleParams({ poolKey: key01, zeroForOne: true, - exactAmount: type(uint128).max, - sqrtPriceLimitX96: SQRT_PRICE_100_102, + exactAmount: uint128(amountOut), hookData: ZERO_BYTES }) ); snapLastCall("Quoter_exactOutputSingle_zeroForOne"); - assertEq(deltaAmounts[0], 9981); - assertEq(sqrtPriceX96After, SQRT_PRICE_100_102); - assertEq(initializedTicksLoaded, 0); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 10133); } function testQuoter_quoteExactOutputSingle_1to0() public { - (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter - .quoteExactOutputSingle( + uint256 amountOut = 10000; + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutputSingle( IQuoter.QuoteExactSingleParams({ poolKey: key01, zeroForOne: false, - exactAmount: type(uint128).max, - sqrtPriceLimitX96: SQRT_PRICE_102_100, + exactAmount: uint128(amountOut), hookData: ZERO_BYTES }) ); snapLastCall("Quoter_exactOutputSingle_oneForZero"); - assertEq(deltaAmounts[1], 9981); - assertEq(sqrtPriceX96After, SQRT_PRICE_102_100); - assertEq(initializedTicksLoaded, 0); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 10133); } function testQuoter_quoteExactOutput_0to2_2TicksLoaded() public { @@ -371,17 +311,12 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token2); IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactOutput(params); + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); snapLastCall("Quoter_quoteExactOutput_oneHop_2TicksLoaded"); - - assertEq(deltaAmounts[0], 15273); - assertEq(sqrtPriceX96AfterList[0], 78055527257643669242286029831); - assertEq(initializedTicksLoadedList[0], 2); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 15273); } function testQuoter_quoteExactOutput_0to2_1TickLoaded_initializedAfter() public { @@ -390,16 +325,12 @@ contract QuoterTest is Test, Deployers, GasSnapshot { IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6143); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactOutput(params); - snapLastCall("Quoter_quoteExactOutput_oneHop_initializedAfter"); + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); - assertEq(deltaAmounts[0], 6200); - assertEq(sqrtPriceX96AfterList[0], 78757225449310403327341205211); - assertEq(initializedTicksLoadedList[0], 1); + snapLastCall("Quoter_quoteExactOutput_oneHop_initializedAfter"); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 6200); } function testQuoter_quoteExactOutput_0to2_1TickLoaded() public { @@ -408,16 +339,12 @@ contract QuoterTest is Test, Deployers, GasSnapshot { IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 4000); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactOutput(params); - snapLastCall("Quoter_quoteExactOutput_oneHop_1TickLoaded"); + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); - assertEq(deltaAmounts[0], 4029); - assertEq(sqrtPriceX96AfterList[0], 78924219757724709840818372098); - assertEq(initializedTicksLoadedList[0], 1); + snapLastCall("Quoter_quoteExactOutput_oneHop_1TickLoaded"); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 4029); } function testQuoter_quoteExactOutput_0to2_0TickLoaded_startingInitialized() public { @@ -428,16 +355,12 @@ contract QuoterTest is Test, Deployers, GasSnapshot { 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); + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); snapLastCall("Quoter_quoteExactOutput_oneHop_startingInitialized"); - assertEq(deltaAmounts[0], 102); - assertEq(sqrtPriceX96AfterList[0], 79224329176051641448521403903); - assertEq(initializedTicksLoadedList[0], 1); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 102); } function testQuoter_quoteExactOutput_0to2_0TickLoaded_startingNotInitialized() public { @@ -446,15 +369,11 @@ contract QuoterTest is Test, Deployers, GasSnapshot { IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 10); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactOutput(params); + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); - assertEq(deltaAmounts[0], 12); - assertEq(sqrtPriceX96AfterList[0], 79227408033628034983534698435); - assertEq(initializedTicksLoadedList[0], 0); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 12); } function testQuoter_quoteExactOutput_2to0_2TicksLoaded() public { @@ -462,16 +381,11 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token0); IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactOutput(params); + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); - assertEq(deltaAmounts[0], 15273); - assertEq(sqrtPriceX96AfterList[0], 80418414376567919517220409857); - assertEq(initializedTicksLoadedList.length, 1); - assertEq(initializedTicksLoadedList[0], 2); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 15273); } function testQuoter_quoteExactOutput_2to0_2TicksLoaded_initializedAfter() public { @@ -480,16 +394,11 @@ contract QuoterTest is Test, Deployers, GasSnapshot { IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6223); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactOutput(params); + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); - assertEq(deltaAmounts[0], 6283); - assertEq(sqrtPriceX96AfterList[0], 79708304437530892332449657932); - assertEq(initializedTicksLoadedList.length, 1); - assertEq(initializedTicksLoadedList[0], 2); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 6283); } function testQuoter_quoteExactOutput_2to0_1TickLoaded() public { @@ -497,16 +406,11 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token0); IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6000); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactOutput(params); + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); - assertEq(deltaAmounts[0], 6055); - assertEq(sqrtPriceX96AfterList[0], 79690640184021170956740081887); - assertEq(initializedTicksLoadedList.length, 1); - assertEq(initializedTicksLoadedList[0], 1); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 6055); } function testQuoter_quoteExactOutput_2to1() public { @@ -515,16 +419,11 @@ contract QuoterTest is Test, Deployers, GasSnapshot { IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9871); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactOutput(params); + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); - assertEq(deltaAmounts[0], 10000); - assertEq(sqrtPriceX96AfterList[0], 80018020393569259756601362385); - assertEq(initializedTicksLoadedList.length, 1); - assertEq(initializedTicksLoadedList[0], 0); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 10000); } function testQuoter_quoteExactOutput_0to2to1() public { @@ -534,22 +433,13 @@ contract QuoterTest is Test, Deployers, GasSnapshot { IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9745); - ( - int128[] memory deltaAmounts, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksLoadedList - ) = quoter.quoteExactOutput(params); + (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); snapLastCall("Quoter_quoteExactOutput_twoHops"); - 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); + assertGt(gasEstimate, 50000); + assertLt(gasEstimate, 400000); + assertEq(amountIn, 10000); } function createPoolKey(MockERC20 tokenA, MockERC20 tokenB, address hookAddr)