From 86bbcbf8affe60b1291e62ebdb5802ff5ad91645 Mon Sep 17 00:00:00 2001 From: ChefCupcake Date: Thu, 13 Jun 2024 10:02:59 +0800 Subject: [PATCH 1/5] feat: Add Quoter for pool-cl --- src/pool-cl/interfaces/IQuoter.sol | 106 ++++ src/pool-cl/lens/Quoter.sol | 339 ++++++++++ src/pool-cl/libraries/PathKey.sol | 36 ++ src/pool-cl/libraries/PoolGetters.sol | 101 +++ src/pool-cl/libraries/PoolTicksCounter.sol | 111 ++++ test/helpers/PoolModifyPositionTest.sol | 72 +++ test/pool-cl/Quoter.t.sol | 688 +++++++++++++++++++++ 7 files changed, 1453 insertions(+) create mode 100644 src/pool-cl/interfaces/IQuoter.sol create mode 100644 src/pool-cl/lens/Quoter.sol create mode 100644 src/pool-cl/libraries/PathKey.sol create mode 100644 src/pool-cl/libraries/PoolGetters.sol create mode 100644 src/pool-cl/libraries/PoolTicksCounter.sol create mode 100644 test/helpers/PoolModifyPositionTest.sol create mode 100644 test/pool-cl/Quoter.t.sol diff --git a/src/pool-cl/interfaces/IQuoter.sol b/src/pool-cl/interfaces/IQuoter.sol new file mode 100644 index 0000000..a859e2f --- /dev/null +++ b/src/pool-cl/interfaces/IQuoter.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import {Currency} from "pancake-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 + ); +} \ No newline at end of file diff --git a/src/pool-cl/lens/Quoter.sol b/src/pool-cl/lens/Quoter.sol new file mode 100644 index 0000000..d1ce200 --- /dev/null +++ b/src/pool-cl/lens/Quoter.sol @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {Hooks} from "pancake-v4-core/src/libraries/Hooks.sol"; +import {TickMath} from "pancake-v4-core/src/pool-cl/libraries/TickMath.sol"; +import {IHooks} from "pancake-v4-core/src/interfaces/IHooks.sol"; +import {ILockCallback} from "pancake-v4-core/src/interfaces/ILockCallback.sol"; +import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol"; +import {ICLPoolManager} from "pancake-v4-core/src/pool-cl/interfaces/ICLPoolManager.sol"; +import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "pancake-v4-core/src/types/Currency.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import {PoolIdLibrary} from "pancake-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; + + IVault public immutable vault; + ICLPoolManager 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; + + struct QuoteResult { + int128[] deltaAmounts; + uint160[] sqrtPriceX96AfterList; + uint32[] initializedTicksLoadedList; + } + + 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(IVault _vault, address _poolManager) { + vault = _vault; + manager = ICLPoolManager(_poolManager); + } + + /// @inheritdoc IQuoter + function quoteExactInputSingle(QuoteExactSingleParams memory params) + public + override + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) + { + try vault.lock(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 vault.lock(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 vault.lock(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 vault.lock(abi.encodeWithSelector(this._quoteExactOutput.selector, params)) {} + catch (bytes memory reason) { + return _handleRevert(reason); + } + } + + /// @inheritdoc ILockCallback + function lockAcquired(bytes calldata data) external returns (bytes memory) { + if (msg.sender != address(vault)) { + revert InvalidLockAcquiredSender(); + } + + (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; + + QuoteResult memory result = QuoteResult({ + deltaAmounts: new int128[](pathLength + 1), + sqrtPriceX96AfterList: new uint160[](pathLength), + initializedTicksLoadedList: new uint32[](pathLength) + }); + QuoteCache memory cache; + + for (uint256 i = 0; i < pathLength; i++) { + (PoolKey memory poolKey, bool zeroForOne) = + params.path[i].getPoolAndSwapDirection(i == 0 ? params.exactCurrency : cache.prevCurrency); + (, cache.tickBefore,,) = manager.getSlot0(poolKey.toId()); + + (cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) = _swap( + poolKey, + zeroForOne, + -int256(int128(i == 0 ? params.exactAmount : cache.prevAmount)), + 0, + params.path[i].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(manager, poolKey, cache.tickBefore, cache.tickAfter); + } + bytes memory r = + abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); + assembly { + revert(add(0x20, r), mload(r)) + } + } + + /// @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; + + QuoteResult memory result = QuoteResult({ + deltaAmounts: new int128[](pathLength + 1), + sqrtPriceX96AfterList: new uint160[](pathLength), + initializedTicksLoadedList: new uint32[](pathLength) + }); + QuoteCache memory cache; + uint128 curAmountOut; + + for (uint256 i = pathLength; i > 0; i--) { + curAmountOut = i == pathLength ? params.exactAmount : cache.prevAmount; + amountOutCached = curAmountOut; + + (PoolKey memory poolKey, bool oneForZero) = PathKeyLib.getPoolAndSwapDirection( + params.path[i - 1], i == pathLength ? params.exactCurrency : cache.prevCurrency + ); + + (, cache.tickBefore,,) = manager.getSlot0(poolKey.toId()); + + (cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) = + _swap(poolKey, !oneForZero, int256(uint256(curAmountOut)), 0, params.path[i - 1].hookData); + + // 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(manager, poolKey, cache.tickBefore, cache.tickAfter); + } + bytes memory r = + abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); + assembly { + revert(add(0x20, r), mload(r)) + } + } + + /// @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, + ICLPoolManager.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; + } +} \ No newline at end of file diff --git a/src/pool-cl/libraries/PathKey.sol b/src/pool-cl/libraries/PathKey.sol new file mode 100644 index 0000000..1524079 --- /dev/null +++ b/src/pool-cl/libraries/PathKey.sol @@ -0,0 +1,36 @@ +//SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.24; + +import {Currency} from "pancake-v4-core/src/types/Currency.sol"; +import {IHooks} from "pancake-v4-core/src/interfaces/IHooks.sol"; +import {IPoolManager} from "pancake-v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import "pancake-v4-core/src/libraries/SafeCast.sol"; + +struct PathKey { + Currency intermediateCurrency; + uint24 fee; + int24 tickSpacing; + IHooks hooks; + IPoolManager poolManager; + bytes hookData; + bytes32 parameters; +} + +library PathKeyLib { + using SafeCast for int24; + + 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.hooks, params.poolManager, params.fee, params.parameters); + } +} diff --git a/src/pool-cl/libraries/PoolGetters.sol b/src/pool-cl/libraries/PoolGetters.sol new file mode 100644 index 0000000..a5ad153 --- /dev/null +++ b/src/pool-cl/libraries/PoolGetters.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IPoolManager} from "pancake-v4-core/src/interfaces/IPoolManager.sol"; +import {CLPool} from "pancake-v4-core/src/pool-cl/libraries/CLPool.sol"; +import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol"; +import {BitMath} from "pancake-v4-core/src/pool-cl/libraries/BitMath.sol"; + +/// @title Helper functions to access pool information +/// TODO: Expose other getters on core with extsload. Only use when extsload is available and storage layout is frozen. +library PoolGetters { + uint256 constant POOL_SLOT = 10; + uint256 constant TICKS_OFFSET = 4; + uint256 constant TICK_BITMAP_OFFSET = 5; + + function getNetLiquidityAtTick(IPoolManager poolManager, PoolId poolId, int24 tick) + internal + view + returns (int128 l) + { +// bytes32 value = poolManager.extsload( +// keccak256(abi.encode(tick, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICKS_OFFSET)) +// ); +// +// assembly { +// l := shr(128, and(value, shl(128, sub(shl(128, 1), 1)))) +// } + } + + function getTickBitmapAtWord(IPoolManager poolManager, PoolId poolId, int16 word) + internal + view + returns (uint256 bm) + { +// bm = uint256( +// poolManager.extsload( +// keccak256(abi.encode(word, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICK_BITMAP_OFFSET)) +// ) +// ); + } + + /// @notice Returns the next initialized tick contained in the same word (or adjacent word) as the tick that is either + /// to the left (less than or equal to) or right (greater than) of the given tick + /// @param poolManager The mapping in which to compute the next initialized tick + /// @param tick The starting tick + /// @param tickSpacing The spacing between usable ticks + /// @param lte Whether to search for the next initialized tick to the left (less than or equal to the starting tick) + /// @return next The next initialized or uninitialized tick up to 256 ticks away from the current tick + /// @return initialized Whether the next tick is initialized, as the function only searches within up to 256 ticks + function getNextInitializedTickWithinOneWord( + IPoolManager poolManager, + PoolId poolId, + int24 tick, + int24 tickSpacing, + bool lte + ) internal view returns (int24 next, bool initialized) { +// unchecked { +// int24 compressed = tick / tickSpacing; +// if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity +// +// if (lte) { +// (int16 wordPos, uint8 bitPos) = position(compressed); +// // all the 1s at or to the right of the current bitPos +// uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); +// // uint256 masked = self[wordPos] & mask; +// uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; +// +// // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word +// initialized = masked != 0; +// // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick +// next = initialized +// ? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing +// : (compressed - int24(uint24(bitPos))) * tickSpacing; +// } else { +// // start from the word of the next tick, since the current tick state doesn't matter +// (int16 wordPos, uint8 bitPos) = position(compressed + 1); +// // all the 1s at or to the left of the bitPos +// uint256 mask = ~((1 << bitPos) - 1); +// uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; +// +// // if there are no initialized ticks to the left of the current tick, return leftmost in the word +// initialized = masked != 0; +// // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick +// next = initialized +// ? (compressed + 1 + int24(uint24(BitMath.leastSignificantBit(masked) - bitPos))) * tickSpacing +// : (compressed + 1 + int24(uint24(type(uint8).max - bitPos))) * tickSpacing; +// } +// } + } + + /// @notice Computes the position in the mapping where the initialized bit for a tick lives + /// @param tick The tick for which to compute the position + /// @return wordPos The key in the mapping containing the word in which the bit is stored + /// @return bitPos The bit position in the word where the flag is stored + function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) { + unchecked { + wordPos = int16(tick >> 8); + bitPos = uint8(int8(tick % 256)); + } + } +} diff --git a/src/pool-cl/libraries/PoolTicksCounter.sol b/src/pool-cl/libraries/PoolTicksCounter.sol new file mode 100644 index 0000000..134d4ab --- /dev/null +++ b/src/pool-cl/libraries/PoolTicksCounter.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.8.24; + +//import {PoolGetters} from "./PoolGetters.sol"; +import {CLPoolParametersHelper} from "pancake-v4-core/src/pool-cl/libraries/CLPoolParametersHelper.sol"; +import {ICLPoolManager} from "pancake-v4-core/src/pool-cl/interfaces/ICLPoolManager.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol"; + +library PoolTicksCounter { + using PoolIdLibrary for PoolKey; + using CLPoolParametersHelper for bytes32; + + struct TickCache { + int16 wordPosLower; + int16 wordPosHigher; + uint8 bitPosLower; + uint8 bitPosHigher; + bool tickBeforeInitialized; + bool tickAfterInitialized; + } + + /// @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(ICLPoolManager self, PoolKey memory key, int24 tickBefore, int24 tickAfter) + internal + view + returns (uint32 initializedTicksLoaded) + { + TickCache memory cache; + + { + int24 tickSpacing = key.parameters.getTickSpacing(); + + // Get the key and offset in the tick bitmap of the active tick before and after the swap. + int16 wordPos = int16((tickBefore / tickSpacing) >> 8); + uint8 bitPos = uint8(uint24((tickBefore / tickSpacing) % 256)); + + int16 wordPosAfter = int16((tickAfter / tickSpacing) >> 8); + uint8 bitPosAfter = uint8(uint24((tickAfter / 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); + cache.tickAfterInitialized = + ((bmAfter & (1 << bitPosAfter)) > 0) && ((tickAfter % 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); + cache.tickBeforeInitialized = + ((bmBefore & (1 << bitPos)) > 0) && ((tickBefore % 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 = PoolGetters.getTickBitmapAtWord(self, key.toId(), cache.wordPosLower); + uint256 bmLower = self.getPoolBitmapInfo(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; + } + + function countOneBits(uint256 x) private pure returns (uint16) { + uint16 bits = 0; + while (x != 0) { + bits++; + x &= (x - 1); + } + return bits; + } +} \ No newline at end of file diff --git a/test/helpers/PoolModifyPositionTest.sol b/test/helpers/PoolModifyPositionTest.sol new file mode 100644 index 0000000..32dbd6a --- /dev/null +++ b/test/helpers/PoolModifyPositionTest.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {console2} from "forge-std/console2.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {CurrencyLibrary, Currency} from "pancake-v4-core/src/types/Currency.sol"; +import {CurrencySettlement} from "pancake-v4-core/test/helpers/CurrencySettlement.sol"; +import {ILockCallback} from "pancake-v4-core/src/interfaces/ILockCallback.sol"; +import {ICLPoolManager} from "pancake-v4-core/src/pool-cl/interfaces/ICLPoolManager.sol"; +import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol"; + +import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; + +contract PoolModifyPositionTest is ILockCallback { + using CurrencySettlement for Currency; + + IVault public immutable vault; + ICLPoolManager public immutable manager; + + constructor(IVault _vault, ICLPoolManager _manager) { + vault = _vault; + manager = _manager; + } + + struct CallbackData { + address sender; + PoolKey key; + ICLPoolManager.ModifyLiquidityParams params; + bytes hookData; + } + + function modifyPosition( + PoolKey memory key, + ICLPoolManager.ModifyLiquidityParams memory params, + bytes memory hookData + ) external payable returns (BalanceDelta delta) { + delta = abi.decode(vault.lock(abi.encode(CallbackData(msg.sender, key, params, hookData))), (BalanceDelta)); + + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) { + CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); + } + } + + function lockAcquired(bytes calldata rawData) external returns (bytes memory) { + require(msg.sender == address(vault)); + + CallbackData memory data = abi.decode(rawData, (CallbackData)); + + (BalanceDelta delta, BalanceDelta feeDelta) = manager.modifyLiquidity(data.key, data.params, data.hookData); + + // For now assume to always settle feeDelta in the same way as delta + BalanceDelta totalDelta = delta + feeDelta; + + if (totalDelta.amount0() < 0) { + data.key.currency0.settle(vault, data.sender, uint128(-totalDelta.amount0()), false); + } + if (totalDelta.amount1() < 0) { + data.key.currency1.settle(vault, data.sender, uint128(-totalDelta.amount1()), false); + } + + if (totalDelta.amount0() > 0) { + data.key.currency0.take(vault, data.sender, uint128(totalDelta.amount0()), false); + } + if (totalDelta.amount1() > 0) { + data.key.currency1.take(vault, data.sender, uint128(totalDelta.amount0()), false); + } + + return abi.encode(totalDelta); + } +} diff --git a/test/pool-cl/Quoter.t.sol b/test/pool-cl/Quoter.t.sol new file mode 100644 index 0000000..ae03cdb --- /dev/null +++ b/test/pool-cl/Quoter.t.sol @@ -0,0 +1,688 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PathKey} from "../../src/pool-cl/libraries/PathKey.sol"; +import {IQuoter} from "../../src/pool-cl/interfaces/IQuoter.sol"; +import {Quoter} from "../../src/pool-cl/lens/Quoter.sol"; +import {LiquidityAmounts} from "../../src/pool-cl/libraries/LiquidityAmounts.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol"; +import {SafeCast} from "pancake-v4-core/src/libraries/SafeCast.sol"; +import {Deployers} from "pancake-v4-core/test/pool-cl/helpers/Deployers.sol"; +import {IHooks} from "pancake-v4-core/src/interfaces/IHooks.sol"; +import {PoolModifyPositionTest} from "../helpers/PoolModifyPositionTest.sol"; +import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import {CLPoolManager} from "pancake-v4-core/src/pool-cl/CLPoolManager.sol"; +import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol"; +import {ICLPoolManager} from "pancake-v4-core/src/pool-cl/interfaces/ICLPoolManager.sol"; +import {CLPoolManagerRouter} from "pancake-v4-core/test/pool-cl/helpers/CLPoolManagerRouter.sol"; +import {ProtocolFeeControllerTest} from "pancake-v4-core/test/pool-cl/helpers/ProtocolFeeControllerTest.sol"; +import {IProtocolFeeController} from "pancake-v4-core/src/interfaces/IProtocolFeeController.sol"; +import {Currency, CurrencyLibrary} from "pancake-v4-core/src/types/Currency.sol"; +import {TickMath} from "pancake-v4-core/src/pool-cl/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; + + IVault public vault; + CLPoolManager public manager; + CLPoolManagerRouter public router; + ProtocolFeeControllerTest public feeController; + + Quoter quoter; + + PoolModifyPositionTest positionManager; + + MockERC20 token0; + MockERC20 token1; + MockERC20 token2; + + PoolKey key01; + PoolKey key02; + PoolKey key12; + + MockERC20[] tokenPath; + + function setUp() public { + (vault, manager) = createFreshManager(); + router = new CLPoolManagerRouter(vault, manager); + feeController = new ProtocolFeeControllerTest(); + manager.setProtocolFeeController(IProtocolFeeController(address(feeController))); + quoter = new Quoter(vault, address(manager)); + positionManager = new PoolModifyPositionTest(vault, manager); + + // salts are chosen so that address(token0) < address(token1) && address(token1) < address(token2) + token0 = new MockERC20("Test0", "0", 18); + vm.etch(address(0x1111), address(token0).code); + token0 = MockERC20(address(0x1111)); + token0.mint(address(this), 2 ** 128); + + vm.etch(address(0x2222), address(token0).code); + token1 = MockERC20(address(0x2222)); + token1.mint(address(this), 2 ** 128); + + vm.etch(address(0x3333), address(token0).code); + token2 = MockERC20(address(0x3333)); + 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.LockFailure.selector); + vm.prank(address(vault)); + quoter.lockAcquired(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 + view + returns (PoolKey memory) + { + if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA); + return PoolKey({ + currency0: Currency.wrap(address(tokenA)), + currency1: Currency.wrap(address(tokenB)), + hooks: IHooks(hookAddr), + poolManager: manager, + fee: uint24(3000), + parameters: bytes32(uint256(0x3c0000)) + }); + } + + function setupPool(PoolKey memory poolKey) internal { + manager.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, + ICLPoolManager.ModifyLiquidityParams( + MIN_TICK, + MAX_TICK, + calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(), + bytes32(0) + ), + ZERO_BYTES + ); + } + + function setupPoolMultiplePositions(PoolKey memory poolKey) internal { + manager.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, + ICLPoolManager.ModifyLiquidityParams( + MIN_TICK, + MAX_TICK, + calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(), + bytes32(0) + ), + ZERO_BYTES + ); + positionManager.modifyPosition( + poolKey, + ICLPoolManager.ModifyLiquidityParams( + -60, 60, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -60, 60, 100, 100).toInt256(), bytes32(0) + ), + ZERO_BYTES + ); + positionManager.modifyPosition( + poolKey, + ICLPoolManager.ModifyLiquidityParams( + -120, 120, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -120, 120, 100, 100).toInt256(), bytes32(0) + ), + ZERO_BYTES + ); + } + + function setupPoolWithZeroTickInitialized(PoolKey memory poolKey) internal { + PoolId poolId = poolKey.toId(); + (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); + if (sqrtPriceX96 == 0) { + manager.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, + ICLPoolManager.ModifyLiquidityParams({ + tickLower: MIN_TICK, + tickUpper: MAX_TICK, + liquidityDelta: calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(), + salt: bytes32(0) + }), + ZERO_BYTES + ); + positionManager.modifyPosition( + poolKey, + ICLPoolManager.ModifyLiquidityParams( + 0, 60, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, 0, 60, 100, 100).toInt256(), bytes32(0) + ), + ZERO_BYTES + ); + positionManager.modifyPosition( + poolKey, + ICLPoolManager.ModifyLiquidityParams( + -120, 0, calculateLiquidityFromAmounts(SQRT_RATIO_1_1, -120, 0, 100, 100).toInt256(), bytes32(0) + ), + 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)), ICLPoolManager(manager), bytes(""), bytes32(uint256(0x3c0000))); + } + + 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)), ICLPoolManager(manager), bytes(""), bytes32(uint256(0x3c0000))); + } + + params.exactCurrency = Currency.wrap(address(_tokenPath[_tokenPath.length - 1])); + params.path = path; + params.recipient = address(this); + params.exactAmount = uint128(amountOut); + } +} From 21c21ac0e4c9e26e8c43b4d7de99f53c58986d12 Mon Sep 17 00:00:00 2001 From: ChefCupcake Date: Mon, 17 Jun 2024 10:05:03 +0800 Subject: [PATCH 2/5] chore: Code prettier --- src/pool-cl/interfaces/IQuoter.sol | 2 +- src/pool-cl/lens/Quoter.sol | 4 +- src/pool-cl/libraries/PoolGetters.sol | 88 +++++++++++----------- src/pool-cl/libraries/PoolTicksCounter.sol | 2 +- test/pool-cl/Quoter.t.sol | 23 +++++- 5 files changed, 68 insertions(+), 51 deletions(-) diff --git a/src/pool-cl/interfaces/IQuoter.sol b/src/pool-cl/interfaces/IQuoter.sol index a859e2f..1283408 100644 --- a/src/pool-cl/interfaces/IQuoter.sol +++ b/src/pool-cl/interfaces/IQuoter.sol @@ -103,4 +103,4 @@ interface IQuoter { uint160[] memory sqrtPriceX96AfterList, uint32[] memory initializedTicksLoadedList ); -} \ No newline at end of file +} diff --git a/src/pool-cl/lens/Quoter.sol b/src/pool-cl/lens/Quoter.sol index d1ce200..9f0a089 100644 --- a/src/pool-cl/lens/Quoter.sol +++ b/src/pool-cl/lens/Quoter.sol @@ -62,7 +62,7 @@ contract Quoter is IQuoter, ILockCallback { function quoteExactInputSingle(QuoteExactSingleParams memory params) public override - returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) + returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) { try vault.lock(abi.encodeWithSelector(this._quoteExactInputSingle.selector, params)) {} catch (bytes memory reason) { @@ -336,4 +336,4 @@ contract Quoter is IQuoter, ILockCallback { ? zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1 : sqrtPriceLimitX96; } -} \ No newline at end of file +} diff --git a/src/pool-cl/libraries/PoolGetters.sol b/src/pool-cl/libraries/PoolGetters.sol index a5ad153..cb0b173 100644 --- a/src/pool-cl/libraries/PoolGetters.sol +++ b/src/pool-cl/libraries/PoolGetters.sol @@ -18,13 +18,13 @@ library PoolGetters { view returns (int128 l) { -// bytes32 value = poolManager.extsload( -// keccak256(abi.encode(tick, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICKS_OFFSET)) -// ); -// -// assembly { -// l := shr(128, and(value, shl(128, sub(shl(128, 1), 1)))) -// } + // bytes32 value = poolManager.extsload( + // keccak256(abi.encode(tick, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICKS_OFFSET)) + // ); + // + // assembly { + // l := shr(128, and(value, shl(128, sub(shl(128, 1), 1)))) + // } } function getTickBitmapAtWord(IPoolManager poolManager, PoolId poolId, int16 word) @@ -32,11 +32,11 @@ library PoolGetters { view returns (uint256 bm) { -// bm = uint256( -// poolManager.extsload( -// keccak256(abi.encode(word, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICK_BITMAP_OFFSET)) -// ) -// ); + // bm = uint256( + // poolManager.extsload( + // keccak256(abi.encode(word, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICK_BITMAP_OFFSET)) + // ) + // ); } /// @notice Returns the next initialized tick contained in the same word (or adjacent word) as the tick that is either @@ -54,38 +54,38 @@ library PoolGetters { int24 tickSpacing, bool lte ) internal view returns (int24 next, bool initialized) { -// unchecked { -// int24 compressed = tick / tickSpacing; -// if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity -// -// if (lte) { -// (int16 wordPos, uint8 bitPos) = position(compressed); -// // all the 1s at or to the right of the current bitPos -// uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); -// // uint256 masked = self[wordPos] & mask; -// uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; -// -// // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word -// initialized = masked != 0; -// // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick -// next = initialized -// ? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing -// : (compressed - int24(uint24(bitPos))) * tickSpacing; -// } else { -// // start from the word of the next tick, since the current tick state doesn't matter -// (int16 wordPos, uint8 bitPos) = position(compressed + 1); -// // all the 1s at or to the left of the bitPos -// uint256 mask = ~((1 << bitPos) - 1); -// uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; -// -// // if there are no initialized ticks to the left of the current tick, return leftmost in the word -// initialized = masked != 0; -// // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick -// next = initialized -// ? (compressed + 1 + int24(uint24(BitMath.leastSignificantBit(masked) - bitPos))) * tickSpacing -// : (compressed + 1 + int24(uint24(type(uint8).max - bitPos))) * tickSpacing; -// } -// } + // unchecked { + // int24 compressed = tick / tickSpacing; + // if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity + // + // if (lte) { + // (int16 wordPos, uint8 bitPos) = position(compressed); + // // all the 1s at or to the right of the current bitPos + // uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); + // // uint256 masked = self[wordPos] & mask; + // uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; + // + // // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word + // initialized = masked != 0; + // // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick + // next = initialized + // ? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing + // : (compressed - int24(uint24(bitPos))) * tickSpacing; + // } else { + // // start from the word of the next tick, since the current tick state doesn't matter + // (int16 wordPos, uint8 bitPos) = position(compressed + 1); + // // all the 1s at or to the left of the bitPos + // uint256 mask = ~((1 << bitPos) - 1); + // uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; + // + // // if there are no initialized ticks to the left of the current tick, return leftmost in the word + // initialized = masked != 0; + // // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick + // next = initialized + // ? (compressed + 1 + int24(uint24(BitMath.leastSignificantBit(masked) - bitPos))) * tickSpacing + // : (compressed + 1 + int24(uint24(type(uint8).max - bitPos))) * tickSpacing; + // } + // } } /// @notice Computes the position in the mapping where the initialized bit for a tick lives diff --git a/src/pool-cl/libraries/PoolTicksCounter.sol b/src/pool-cl/libraries/PoolTicksCounter.sol index 134d4ab..5ba38e2 100644 --- a/src/pool-cl/libraries/PoolTicksCounter.sol +++ b/src/pool-cl/libraries/PoolTicksCounter.sol @@ -108,4 +108,4 @@ library PoolTicksCounter { } return bits; } -} \ No newline at end of file +} diff --git a/test/pool-cl/Quoter.t.sol b/test/pool-cl/Quoter.t.sol index ae03cdb..f1994d0 100644 --- a/test/pool-cl/Quoter.t.sol +++ b/test/pool-cl/Quoter.t.sol @@ -620,7 +620,8 @@ contract QuoterTest is Test, Deployers { ICLPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, - liquidityDelta: calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256(), + liquidityDelta: calculateLiquidityFromAmounts(SQRT_RATIO_1_1, MIN_TICK, MAX_TICK, 1000000, 1000000).toInt256( + ), salt: bytes32(0) }), ZERO_BYTES @@ -661,7 +662,15 @@ contract QuoterTest is Test, Deployers { { 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)), ICLPoolManager(manager), bytes(""), bytes32(uint256(0x3c0000))); + path[i] = PathKey( + Currency.wrap(address(_tokenPath[i + 1])), + 3000, + 60, + IHooks(address(0)), + ICLPoolManager(manager), + bytes(""), + bytes32(uint256(0x3c0000)) + ); } params.exactCurrency = Currency.wrap(address(_tokenPath[0])); @@ -677,7 +686,15 @@ contract QuoterTest is Test, Deployers { { 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)), ICLPoolManager(manager), bytes(""), bytes32(uint256(0x3c0000))); + path[i - 1] = PathKey( + Currency.wrap(address(_tokenPath[i - 1])), + 3000, + 60, + IHooks(address(0)), + ICLPoolManager(manager), + bytes(""), + bytes32(uint256(0x3c0000)) + ); } params.exactCurrency = Currency.wrap(address(_tokenPath[_tokenPath.length - 1])); From dfac8459b3cd99582ea0baea54bae570846979f3 Mon Sep 17 00:00:00 2001 From: ChefCupcake Date: Tue, 18 Jun 2024 18:31:27 +0800 Subject: [PATCH 3/5] chore: Rename quoter contract name and update with other optimize suggestion --- .../interfaces/{IQuoter.sol => ICLQuoter.sol} | 6 +- src/pool-cl/lens/{Quoter.sol => CLQuoter.sol} | 24 ++--- src/pool-cl/libraries/PoolGetters.sol | 101 ------------------ test/pool-cl/{Quoter.t.sol => CLQuoter.t.sol} | 66 ++++++------ 4 files changed, 47 insertions(+), 150 deletions(-) rename src/pool-cl/interfaces/{IQuoter.sol => ICLQuoter.sol} (97%) rename src/pool-cl/lens/{Quoter.sol => CLQuoter.sol} (96%) delete mode 100644 src/pool-cl/libraries/PoolGetters.sol rename test/pool-cl/{Quoter.t.sol => CLQuoter.t.sol} (91%) diff --git a/src/pool-cl/interfaces/IQuoter.sol b/src/pool-cl/interfaces/ICLQuoter.sol similarity index 97% rename from src/pool-cl/interfaces/IQuoter.sol rename to src/pool-cl/interfaces/ICLQuoter.sol index 1283408..3dc1ded 100644 --- a/src/pool-cl/interfaces/IQuoter.sol +++ b/src/pool-cl/interfaces/ICLQuoter.sol @@ -5,15 +5,13 @@ import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; import {Currency} from "pancake-v4-core/src/types/Currency.sol"; import {PathKey} from "../libraries/PathKey.sol"; -/// @title Quoter Interface +/// @title CLQuoter 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 { +interface ICLQuoter { error InvalidLockAcquiredSender(); - error InvalidLockCaller(); - error InvalidQuoteBatchParams(); error InsufficientAmountOut(); error LockFailure(); error NotSelf(); diff --git a/src/pool-cl/lens/Quoter.sol b/src/pool-cl/lens/CLQuoter.sol similarity index 96% rename from src/pool-cl/lens/Quoter.sol rename to src/pool-cl/lens/CLQuoter.sol index 9f0a089..62f56d8 100644 --- a/src/pool-cl/lens/Quoter.sol +++ b/src/pool-cl/lens/CLQuoter.sol @@ -11,11 +11,11 @@ import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol"; import {Currency} from "pancake-v4-core/src/types/Currency.sol"; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol"; -import {IQuoter} from "../interfaces/IQuoter.sol"; +import {ICLQuoter} from "../interfaces/ICLQuoter.sol"; import {PoolTicksCounter} from "../libraries/PoolTicksCounter.sol"; import {PathKey, PathKeyLib} from "../libraries/PathKey.sol"; -contract Quoter is IQuoter, ILockCallback { +contract CLQuoter is ICLQuoter, ILockCallback { using Hooks for IHooks; using PoolIdLibrary for PoolKey; using PathKeyLib for PathKey; @@ -58,7 +58,7 @@ contract Quoter is IQuoter, ILockCallback { manager = ICLPoolManager(_poolManager); } - /// @inheritdoc IQuoter + /// @inheritdoc ICLQuoter function quoteExactInputSingle(QuoteExactSingleParams memory params) public override @@ -70,7 +70,7 @@ contract Quoter is IQuoter, ILockCallback { } } - /// @inheritdoc IQuoter + /// @inheritdoc ICLQuoter function quoteExactInput(QuoteExactParams memory params) external returns ( @@ -85,7 +85,7 @@ contract Quoter is IQuoter, ILockCallback { } } - /// @inheritdoc IQuoter + /// @inheritdoc ICLQuoter function quoteExactOutputSingle(QuoteExactSingleParams memory params) public override @@ -98,7 +98,7 @@ contract Quoter is IQuoter, ILockCallback { } } - /// @inheritdoc IQuoter + /// @inheritdoc ICLQuoter function quoteExactOutput(QuoteExactParams memory params) public override @@ -125,7 +125,7 @@ contract Quoter is IQuoter, ILockCallback { if (returnData.length == 0) revert LockFailure(); // if the call failed, bubble up the reason /// @solidity memory-safe-assembly - assembly { + assembly ("memory-safe") { revert(add(returnData, 32), mload(returnData)) } } @@ -201,7 +201,7 @@ contract Quoter is IQuoter, ILockCallback { } bytes memory r = abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); - assembly { + assembly ("memory-safe") { revert(add(0x20, r), mload(r)) } } @@ -226,7 +226,7 @@ contract Quoter is IQuoter, ILockCallback { uint32 initializedTicksLoaded = PoolTicksCounter.countInitializedTicksLoaded(manager, params.poolKey, tickBefore, tickAfter); bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96After, initializedTicksLoaded); - assembly { + assembly ("memory-safe") { revert(add(0x20, result), mload(result)) } } @@ -272,7 +272,7 @@ contract Quoter is IQuoter, ILockCallback { } bytes memory r = abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); - assembly { + assembly ("memory-safe") { revert(add(0x20, r), mload(r)) } } @@ -300,13 +300,13 @@ contract Quoter is IQuoter, ILockCallback { uint32 initializedTicksLoaded = PoolTicksCounter.countInitializedTicksLoaded(manager, params.poolKey, tickBefore, tickAfter); bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96After, initializedTicksLoaded); - assembly { + assembly ("memory-safe") { 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 + /// @notice if amountSpecified < 0, the swap is exactInput, otherwise exactOutput function _swap( PoolKey memory poolKey, bool zeroForOne, diff --git a/src/pool-cl/libraries/PoolGetters.sol b/src/pool-cl/libraries/PoolGetters.sol deleted file mode 100644 index cb0b173..0000000 --- a/src/pool-cl/libraries/PoolGetters.sol +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {IPoolManager} from "pancake-v4-core/src/interfaces/IPoolManager.sol"; -import {CLPool} from "pancake-v4-core/src/pool-cl/libraries/CLPool.sol"; -import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol"; -import {BitMath} from "pancake-v4-core/src/pool-cl/libraries/BitMath.sol"; - -/// @title Helper functions to access pool information -/// TODO: Expose other getters on core with extsload. Only use when extsload is available and storage layout is frozen. -library PoolGetters { - uint256 constant POOL_SLOT = 10; - uint256 constant TICKS_OFFSET = 4; - uint256 constant TICK_BITMAP_OFFSET = 5; - - function getNetLiquidityAtTick(IPoolManager poolManager, PoolId poolId, int24 tick) - internal - view - returns (int128 l) - { - // bytes32 value = poolManager.extsload( - // keccak256(abi.encode(tick, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICKS_OFFSET)) - // ); - // - // assembly { - // l := shr(128, and(value, shl(128, sub(shl(128, 1), 1)))) - // } - } - - function getTickBitmapAtWord(IPoolManager poolManager, PoolId poolId, int16 word) - internal - view - returns (uint256 bm) - { - // bm = uint256( - // poolManager.extsload( - // keccak256(abi.encode(word, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICK_BITMAP_OFFSET)) - // ) - // ); - } - - /// @notice Returns the next initialized tick contained in the same word (or adjacent word) as the tick that is either - /// to the left (less than or equal to) or right (greater than) of the given tick - /// @param poolManager The mapping in which to compute the next initialized tick - /// @param tick The starting tick - /// @param tickSpacing The spacing between usable ticks - /// @param lte Whether to search for the next initialized tick to the left (less than or equal to the starting tick) - /// @return next The next initialized or uninitialized tick up to 256 ticks away from the current tick - /// @return initialized Whether the next tick is initialized, as the function only searches within up to 256 ticks - function getNextInitializedTickWithinOneWord( - IPoolManager poolManager, - PoolId poolId, - int24 tick, - int24 tickSpacing, - bool lte - ) internal view returns (int24 next, bool initialized) { - // unchecked { - // int24 compressed = tick / tickSpacing; - // if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity - // - // if (lte) { - // (int16 wordPos, uint8 bitPos) = position(compressed); - // // all the 1s at or to the right of the current bitPos - // uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); - // // uint256 masked = self[wordPos] & mask; - // uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; - // - // // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word - // initialized = masked != 0; - // // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick - // next = initialized - // ? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing - // : (compressed - int24(uint24(bitPos))) * tickSpacing; - // } else { - // // start from the word of the next tick, since the current tick state doesn't matter - // (int16 wordPos, uint8 bitPos) = position(compressed + 1); - // // all the 1s at or to the left of the bitPos - // uint256 mask = ~((1 << bitPos) - 1); - // uint256 masked = poolManager.getPoolBitmapInfo(poolId, wordPos) & mask; - // - // // if there are no initialized ticks to the left of the current tick, return leftmost in the word - // initialized = masked != 0; - // // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick - // next = initialized - // ? (compressed + 1 + int24(uint24(BitMath.leastSignificantBit(masked) - bitPos))) * tickSpacing - // : (compressed + 1 + int24(uint24(type(uint8).max - bitPos))) * tickSpacing; - // } - // } - } - - /// @notice Computes the position in the mapping where the initialized bit for a tick lives - /// @param tick The tick for which to compute the position - /// @return wordPos The key in the mapping containing the word in which the bit is stored - /// @return bitPos The bit position in the word where the flag is stored - function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) { - unchecked { - wordPos = int16(tick >> 8); - bitPos = uint8(int8(tick % 256)); - } - } -} diff --git a/test/pool-cl/Quoter.t.sol b/test/pool-cl/CLQuoter.t.sol similarity index 91% rename from test/pool-cl/Quoter.t.sol rename to test/pool-cl/CLQuoter.t.sol index f1994d0..2dff76a 100644 --- a/test/pool-cl/Quoter.t.sol +++ b/test/pool-cl/CLQuoter.t.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {PathKey} from "../../src/pool-cl/libraries/PathKey.sol"; -import {IQuoter} from "../../src/pool-cl/interfaces/IQuoter.sol"; -import {Quoter} from "../../src/pool-cl/lens/Quoter.sol"; +import {ICLQuoter} from "../../src/pool-cl/interfaces/ICLQuoter.sol"; +import {CLQuoter} from "../../src/pool-cl/lens/CLQuoter.sol"; import {LiquidityAmounts} from "../../src/pool-cl/libraries/LiquidityAmounts.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol"; @@ -24,7 +24,7 @@ import {IProtocolFeeController} from "pancake-v4-core/src/interfaces/IProtocolFe import {Currency, CurrencyLibrary} from "pancake-v4-core/src/types/Currency.sol"; import {TickMath} from "pancake-v4-core/src/pool-cl/libraries/TickMath.sol"; -contract QuoterTest is Test, Deployers { +contract CLQuoterTest is Test, Deployers { using SafeCast for *; using PoolIdLibrary for PoolKey; @@ -43,7 +43,7 @@ contract QuoterTest is Test, Deployers { CLPoolManagerRouter public router; ProtocolFeeControllerTest public feeController; - Quoter quoter; + CLQuoter quoter; PoolModifyPositionTest positionManager; @@ -62,7 +62,7 @@ contract QuoterTest is Test, Deployers { router = new CLPoolManagerRouter(vault, manager); feeController = new ProtocolFeeControllerTest(); manager.setProtocolFeeController(IProtocolFeeController(address(feeController))); - quoter = new Quoter(vault, address(manager)); + quoter = new CLQuoter(vault, address(manager)); positionManager = new PoolModifyPositionTest(vault, manager); // salts are chosen so that address(token0) < address(token1) && address(token1) < address(token2) @@ -94,7 +94,7 @@ contract QuoterTest is Test, Deployers { (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter .quoteExactInputSingle( - IQuoter.QuoteExactSingleParams({ + ICLQuoter.QuoteExactSingleParams({ poolKey: key02, zeroForOne: true, recipient: address(this), @@ -116,7 +116,7 @@ contract QuoterTest is Test, Deployers { (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter .quoteExactInputSingle( - IQuoter.QuoteExactSingleParams({ + ICLQuoter.QuoteExactSingleParams({ poolKey: key02, zeroForOne: false, recipient: address(this), @@ -133,7 +133,7 @@ contract QuoterTest is Test, Deployers { // nested self-call into lockAcquired reverts function testQuoter_callLockAcquired_reverts() public { - vm.expectRevert(IQuoter.LockFailure.selector); + vm.expectRevert(ICLQuoter.LockFailure.selector); vm.prank(address(vault)); quoter.lockAcquired(abi.encodeWithSelector(quoter.lockAcquired.selector, address(this), "0x")); } @@ -141,7 +141,7 @@ contract QuoterTest is Test, Deployers { function testQuoter_quoteExactInput_0to2_2TicksLoaded() public { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); ( int128[] memory deltaAmounts, @@ -160,7 +160,7 @@ contract QuoterTest is Test, Deployers { // 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); + ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6200); ( int128[] memory deltaAmounts, @@ -179,7 +179,7 @@ contract QuoterTest is Test, Deployers { // 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); + ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 4000); ( int128[] memory deltaAmounts, @@ -195,7 +195,7 @@ contract QuoterTest is Test, Deployers { function testQuoter_quoteExactInput_0to2_0TickLoaded_startingNotInitialized() public { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); + ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); ( int128[] memory deltaAmounts, @@ -212,7 +212,7 @@ contract QuoterTest is Test, Deployers { setupPoolWithZeroTickInitialized(key02); tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); + ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); ( int128[] memory deltaAmounts, @@ -228,7 +228,7 @@ contract QuoterTest is Test, Deployers { function testQuoter_quoteExactInput_2to0_2TicksLoaded() public { tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); ( int128[] memory deltaAmounts, @@ -247,7 +247,7 @@ contract QuoterTest is Test, Deployers { // 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); + ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6250); ( int128[] memory deltaAmounts, @@ -264,7 +264,7 @@ contract QuoterTest is Test, Deployers { setupPoolWithZeroTickInitialized(key02); tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 200); + ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 200); // Tick 0 initialized. Tick after = 1 ( @@ -282,7 +282,7 @@ contract QuoterTest is Test, Deployers { function testQuoter_quoteExactInput_2to0_0TickLoaded_startingNotInitialized() public { tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 103); + ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 103); ( int128[] memory deltaAmounts, @@ -298,7 +298,7 @@ contract QuoterTest is Test, Deployers { function testQuoter_quoteExactInput_2to1() public { tokenPath.push(token2); tokenPath.push(token1); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); ( int128[] memory deltaAmounts, @@ -314,7 +314,7 @@ contract QuoterTest is Test, Deployers { tokenPath.push(token0); tokenPath.push(token2); tokenPath.push(token1); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); ( int128[] memory deltaAmounts, @@ -332,7 +332,7 @@ contract QuoterTest is Test, Deployers { function testQuoter_quoteExactOutputSingle_0to1() public { (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter .quoteExactOutputSingle( - IQuoter.QuoteExactSingleParams({ + ICLQuoter.QuoteExactSingleParams({ poolKey: key01, zeroForOne: true, recipient: address(this), @@ -350,7 +350,7 @@ contract QuoterTest is Test, Deployers { function testQuoter_quoteExactOutputSingle_1to0() public { (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter .quoteExactOutputSingle( - IQuoter.QuoteExactSingleParams({ + ICLQuoter.QuoteExactSingleParams({ poolKey: key01, zeroForOne: false, recipient: address(this), @@ -368,7 +368,7 @@ contract QuoterTest is Test, Deployers { function testQuoter_quoteExactOutput_0to2_2TicksLoaded() public { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); + ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); ( int128[] memory deltaAmounts, @@ -385,7 +385,7 @@ contract QuoterTest is Test, Deployers { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6143); + ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6143); ( int128[] memory deltaAmounts, @@ -402,7 +402,7 @@ contract QuoterTest is Test, Deployers { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 4000); + ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 4000); ( int128[] memory deltaAmounts, @@ -420,7 +420,7 @@ contract QuoterTest is Test, Deployers { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 100); + ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 100); // Tick 0 initialized. Tick after = 1 ( @@ -438,7 +438,7 @@ contract QuoterTest is Test, Deployers { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 10); + ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 10); ( int128[] memory deltaAmounts, @@ -454,7 +454,7 @@ contract QuoterTest is Test, Deployers { function testQuoter_quoteExactOutput_2to0_2TicksLoaded() public { tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); + ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); ( int128[] memory deltaAmounts, @@ -472,7 +472,7 @@ contract QuoterTest is Test, Deployers { tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6223); + ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6223); ( int128[] memory deltaAmounts, @@ -490,7 +490,7 @@ contract QuoterTest is Test, Deployers { tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6000); + ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6000); ( int128[] memory deltaAmounts, uint160[] memory sqrtPriceX96AfterList, @@ -507,7 +507,7 @@ contract QuoterTest is Test, Deployers { tokenPath.push(token2); tokenPath.push(token1); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9871); + ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9871); ( int128[] memory deltaAmounts, @@ -526,7 +526,7 @@ contract QuoterTest is Test, Deployers { tokenPath.push(token2); tokenPath.push(token1); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9745); + ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9745); ( int128[] memory deltaAmounts, @@ -658,7 +658,7 @@ contract QuoterTest is Test, Deployers { function getExactInputParams(MockERC20[] memory _tokenPath, uint256 amountIn) internal view - returns (IQuoter.QuoteExactParams memory params) + returns (ICLQuoter.QuoteExactParams memory params) { PathKey[] memory path = new PathKey[](_tokenPath.length - 1); for (uint256 i = 0; i < _tokenPath.length - 1; i++) { @@ -682,7 +682,7 @@ contract QuoterTest is Test, Deployers { function getExactOutputParams(MockERC20[] memory _tokenPath, uint256 amountOut) internal view - returns (IQuoter.QuoteExactParams memory params) + returns (ICLQuoter.QuoteExactParams memory params) { PathKey[] memory path = new PathKey[](_tokenPath.length - 1); for (uint256 i = _tokenPath.length - 1; i > 0; i--) { From 0adfeeadabfd2e905fd5567f3a7eeb218c3d7ab9 Mon Sep 17 00:00:00 2001 From: ChefCupcake Date: Wed, 19 Jun 2024 09:13:48 +0800 Subject: [PATCH 4/5] chore: Removed unused parameter in QuoteExactSingleParams --- src/pool-cl/interfaces/ICLQuoter.sol | 1 - test/pool-cl/CLQuoter.t.sol | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/pool-cl/interfaces/ICLQuoter.sol b/src/pool-cl/interfaces/ICLQuoter.sol index 3dc1ded..ee2ffee 100644 --- a/src/pool-cl/interfaces/ICLQuoter.sol +++ b/src/pool-cl/interfaces/ICLQuoter.sol @@ -25,7 +25,6 @@ interface ICLQuoter { struct QuoteExactSingleParams { PoolKey poolKey; bool zeroForOne; - address recipient; uint128 exactAmount; uint160 sqrtPriceLimitX96; bytes hookData; diff --git a/test/pool-cl/CLQuoter.t.sol b/test/pool-cl/CLQuoter.t.sol index 2dff76a..3154b97 100644 --- a/test/pool-cl/CLQuoter.t.sol +++ b/test/pool-cl/CLQuoter.t.sol @@ -97,7 +97,6 @@ contract CLQuoterTest is Test, Deployers { ICLQuoter.QuoteExactSingleParams({ poolKey: key02, zeroForOne: true, - recipient: address(this), exactAmount: uint128(amountIn), sqrtPriceLimitX96: 0, hookData: ZERO_BYTES @@ -119,7 +118,6 @@ contract CLQuoterTest is Test, Deployers { ICLQuoter.QuoteExactSingleParams({ poolKey: key02, zeroForOne: false, - recipient: address(this), exactAmount: uint128(amountIn), sqrtPriceLimitX96: 0, hookData: ZERO_BYTES @@ -335,7 +333,6 @@ contract CLQuoterTest is Test, Deployers { ICLQuoter.QuoteExactSingleParams({ poolKey: key01, zeroForOne: true, - recipient: address(this), exactAmount: type(uint128).max, sqrtPriceLimitX96: SQRT_RATIO_100_102, hookData: ZERO_BYTES @@ -353,7 +350,6 @@ contract CLQuoterTest is Test, Deployers { ICLQuoter.QuoteExactSingleParams({ poolKey: key01, zeroForOne: false, - recipient: address(this), exactAmount: type(uint128).max, sqrtPriceLimitX96: SQRT_RATIO_102_100, hookData: ZERO_BYTES From 39356d6b5a5a99ed3332981499f6c7f38fac071c Mon Sep 17 00:00:00 2001 From: ChefCupcake Date: Thu, 20 Jun 2024 15:01:27 +0800 Subject: [PATCH 5/5] feat: Add BinQuoter and start to write testcases --- src/pool-bin/interfaces/IBinQuoter.sol | 100 ++++++++++ src/pool-bin/lens/BinQuoter.sol | 241 +++++++++++++++++++++++++ src/pool-bin/libraries/PathKey.sol | 35 ++++ src/pool-cl/interfaces/ICLQuoter.sol | 2 +- test/pool-bin/BinQuoter.t.sol | 68 +++++++ test/pool-cl/CLQuoter.t.sol | 54 +++--- 6 files changed, 471 insertions(+), 29 deletions(-) create mode 100644 src/pool-bin/interfaces/IBinQuoter.sol create mode 100644 src/pool-bin/lens/BinQuoter.sol create mode 100644 src/pool-bin/libraries/PathKey.sol create mode 100644 test/pool-bin/BinQuoter.t.sol diff --git a/src/pool-bin/interfaces/IBinQuoter.sol b/src/pool-bin/interfaces/IBinQuoter.sol new file mode 100644 index 0000000..b8bcbdd --- /dev/null +++ b/src/pool-bin/interfaces/IBinQuoter.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap +pragma solidity ^0.8.19; + +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import {Currency} from "pancake-v4-core/src/types/Currency.sol"; +import {PathKey} from "../libraries/PathKey.sol"; + +/// @title BinQuoter 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 IBinQuoter { + error TooLittleReceived(); + error TooMuchRequested(); + error TransactionTooOld(); + error NotSelf(); + error NotVault(); + error InvalidSwapType(); + + enum SwapType { + ExactInput, + ExactInputSingle, + ExactOutput, + ExactOutputSingle + } + + struct SwapInfo { + SwapType swapType; + address msgSender; + bytes params; + } + + struct V4BinExactInputSingleParams { + PoolKey poolKey; + bool swapForY; + address recipient; + uint128 amountIn; + uint128 amountOutMinimum; + bytes hookData; + } + + struct V4BinExactInputParams { + Currency currencyIn; + PathKey[] path; + address recipient; + uint128 amountIn; + uint128 amountOutMinimum; + } + + struct V4BinExactOutputSingleParams { + PoolKey poolKey; + bool swapForY; + address recipient; + uint128 amountOut; + uint128 amountInMaximum; + bytes hookData; + } + + struct V4BinExactOutputParams { + Currency currencyOut; + PathKey[] path; + address recipient; + uint128 amountOut; + uint128 amountInMaximum; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another token + /// @param params The parameters necessary for the swap, encoded as `V4BinExactInputSingleParams` in calldata + /// @return amountOut The amount of the received token + function exactInputSingle(V4BinExactInputSingleParams calldata params, uint256 deadline) + external + payable + returns (uint256 amountOut); + + // / @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + // / @param params The parameters necessary for the multi-hop swap, encoded as `V4BinExactInputParams` in calldata + // / @return amountOut The amount of the received token + function exactInput(V4BinExactInputParams calldata params, uint256 deadline) + external + payable + returns (uint256 amountOut); + + /// @notice Swaps as little as possible of one token for `amountOut` of another token + /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata + /// @return amountIn The amount of the input token + function exactOutputSingle(V4BinExactOutputSingleParams calldata params, uint256 deadline) + external + payable + returns (uint256 amountIn); + + /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata + /// @return amountIn The amount of the input token + function exactOutput(V4BinExactOutputParams calldata params, uint256 deadline) + external + payable + returns (uint256 amountIn); +} \ No newline at end of file diff --git a/src/pool-bin/lens/BinQuoter.sol b/src/pool-bin/lens/BinQuoter.sol new file mode 100644 index 0000000..88f3392 --- /dev/null +++ b/src/pool-bin/lens/BinQuoter.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap +pragma solidity ^0.8.19; + +import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import {ILockCallback} from "pancake-v4-core/src/interfaces/ILockCallback.sol"; +import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol"; +import {IBinPoolManager} from "pancake-v4-core/src/pool-bin/interfaces/IBinPoolManager.sol"; +import {Currency, CurrencyLibrary} from "pancake-v4-core/src/types/Currency.sol"; +import {IBinPoolManager} from "pancake-v4-core/src/pool-bin/interfaces/IBinPoolManager.sol"; +import {SafeCast} from "pancake-v4-core/src/pool-bin/libraries/math/SafeCast.sol"; +import {IBinQuoter} from "../interfaces/IBinQuoter.sol"; +import {PathKey, PathKeyLib} from "../libraries/PathKey.sol"; + +contract BinQuoter is IBinQuoter, ILockCallback { + using CurrencyLibrary for Currency; + using SafeCast for uint128; + using PathKeyLib for PathKey; + + IVault public immutable vault; + IBinPoolManager public immutable binPoolManager; + + /// @dev Only this address may call this function + modifier selfOnly() { + if (msg.sender != address(this)) revert NotSelf(); + _; + } + + modifier vaultOnly() { + if (msg.sender != address(vault)) revert NotVault(); + _; + } + + modifier checkDeadline(uint256 deadline) { + if (block.timestamp > deadline) revert TransactionTooOld(); + _; + } + + constructor(IVault _vault, IBinPoolManager _binPoolManager) { + vault = _vault; + binPoolManager = _binPoolManager; + } + + function exactInputSingle(V4BinExactInputSingleParams calldata params, uint256 deadline) + external + payable + override + checkDeadline(deadline) + returns (uint256 amountOut) + { + amountOut = abi.decode( + vault.lock(abi.encode(SwapInfo(SwapType.ExactInputSingle, msg.sender, abi.encode(params)))), (uint256) + ); + } + + function exactInput(V4BinExactInputParams calldata params, uint256 deadline) + external + payable + override + checkDeadline(deadline) + returns (uint256 amountOut) + { + amountOut = abi.decode( + vault.lock(abi.encode(SwapInfo(SwapType.ExactInput, msg.sender, abi.encode(params)))), (uint256) + ); + } + + function exactOutputSingle(V4BinExactOutputSingleParams calldata params, uint256 deadline) + external + payable + override + checkDeadline(deadline) + returns (uint256 amountIn) + { + amountIn = abi.decode( + vault.lock(abi.encode(SwapInfo(SwapType.ExactOutputSingle, msg.sender, abi.encode(params)))), (uint256) + ); + } + + function exactOutput(V4BinExactOutputParams calldata params, uint256 deadline) + external + payable + override + checkDeadline(deadline) + returns (uint256 amountIn) + { + amountIn = abi.decode( + vault.lock(abi.encode(SwapInfo(SwapType.ExactOutput, msg.sender, abi.encode(params)))), (uint256) + ); + } + + function lockAcquired(bytes calldata data) external override vaultOnly returns (bytes memory) { + SwapInfo memory swapInfo = abi.decode(data, (SwapInfo)); + + if (swapInfo.swapType == SwapType.ExactInput) { + return abi.encode( + _quoteExactInput(abi.decode(swapInfo.params, (V4BinExactInputParams))) + ); + } else if (swapInfo.swapType == SwapType.ExactInputSingle) { + return abi.encode( + _quoteExactInputSingle(abi.decode(swapInfo.params, (V4BinExactInputSingleParams))) + ); + } else if (swapInfo.swapType == SwapType.ExactOutput) { + return abi.encode( + _quoteExactOutput(abi.decode(swapInfo.params, (V4BinExactOutputParams))) + ); + } else if (swapInfo.swapType == SwapType.ExactOutputSingle) { + return abi.encode( + _quoteExactOutputSingle(abi.decode(swapInfo.params, (V4BinExactOutputSingleParams))) + ); + } else { + revert InvalidSwapType(); + } + } + + /// @dev quote an ExactInput swap on a pool, then revert with the result + function _quoteExactInputSingle(V4BinExactInputSingleParams memory params) + internal + returns (uint256 amountOut) + { + amountOut = uint256( + _swap( + params.poolKey, + params.swapForY, + -(params.amountIn.safeInt128()), + params.hookData + ) + ); + + if (amountOut < params.amountOutMinimum) revert TooLittleReceived(); + } + + struct V4BinExactInputState { + uint256 pathLength; + PoolKey poolKey; + bool swapForY; + uint128 amountOut; + } + + /// @notice Perform a swap with `amountIn` in and ensure at least `amountOutMinimum` out + function _quoteExactInput(V4BinExactInputParams memory params) + internal + returns (uint256 amountOut) + { + V4BinExactInputState memory state; + state.pathLength = params.path.length; + + for (uint256 i = 0; i < state.pathLength; i++) { + (state.poolKey, state.swapForY) = params.path[i].getPoolAndSwapDirection(params.currencyIn); + + state.amountOut = _swap( + state.poolKey, + state.swapForY, + -(params.amountIn.safeInt128()), + params.path[i].hookData + ); + + params.amountIn = state.amountOut; + params.currencyIn = params.path[i].intermediateCurrency; + } + + if (state.amountOut < params.amountOutMinimum) revert TooLittleReceived(); + return uint256(state.amountOut); + } + + /// @notice Perform a swap that ensure at least `amountOut` tokens with `amountInMaximum` tokens + function _quoteExactOutputSingle(V4BinExactOutputSingleParams memory params) + internal + returns (uint256 amountIn) + { + amountIn = uint256( + _swap( + params.poolKey, + params.swapForY, + params.amountOut.safeInt128(), + params.hookData + ) + ); + + if (amountIn > params.amountInMaximum) revert TooMuchRequested(); + } + + struct V4BinExactOutputState { + uint256 pathLength; + PoolKey poolKey; + bool swapForY; + uint128 amountIn; + } + + /// @notice Perform a swap that ensure at least `amountOut` tokens with `amountInMaximum` tokens + function _quoteExactOutput(V4BinExactOutputParams memory params) + internal + returns (uint256 amountIn) + { + V4BinExactOutputState memory state; + state.pathLength = params.path.length; + + /// @dev Iterate backward from last path to first path + for (uint256 i = state.pathLength; i > 0;) { + // Step 1: Find out poolKey and how much amountIn required to get amountOut + (state.poolKey, state.swapForY) = params.path[i - 1].getPoolAndSwapDirection(params.currencyOut); + + state.amountIn = _swap( + state.poolKey, + !state.swapForY, + params.amountOut.safeInt128(), + params.path[i - 1].hookData + ); + + params.amountOut = state.amountIn; + params.currencyOut = params.path[i - 1].intermediateCurrency; + + unchecked { + --i; + } + } + + if (state.amountIn > params.amountInMaximum) revert TooMuchRequested(); + amountIn = uint256(state.amountIn); + } + + /// @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 swapForY, + int128 amountSpecified, + bytes memory hookData + ) private returns (uint128 reciprocalAmount) { + BalanceDelta delta = binPoolManager.swap(poolKey, swapForY, amountSpecified, hookData); + + if (swapForY) { + /// @dev amountSpecified < 0 indicate exactInput, so reciprocal token is token1 and positive + /// amountSpecified > 0 indicate exactOutput, so reciprocal token is token0 but is negative + reciprocalAmount = amountSpecified < 0 ? uint128(delta.amount1()) : uint128(-delta.amount0()); + } else { + reciprocalAmount = amountSpecified < 0 ? uint128(delta.amount0()) : uint128(-delta.amount1()); + } + } +} \ No newline at end of file diff --git a/src/pool-bin/libraries/PathKey.sol b/src/pool-bin/libraries/PathKey.sol new file mode 100644 index 0000000..bd7125b --- /dev/null +++ b/src/pool-bin/libraries/PathKey.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap +pragma solidity ^0.8.19; + +import {Currency} from "pancake-v4-core/src/types/Currency.sol"; +import {IHooks} from "pancake-v4-core/src/interfaces/IHooks.sol"; +import {IPoolManager} from "pancake-v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import "pancake-v4-core/src/libraries/SafeCast.sol"; + +struct PathKey { + Currency intermediateCurrency; + uint24 fee; + IHooks hooks; + IPoolManager poolManager; + bytes hookData; + bytes32 parameters; +} + +library PathKeyLib { + using SafeCast for int24; + + 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.hooks, params.poolManager, params.fee, params.parameters); + } +} \ No newline at end of file diff --git a/src/pool-cl/interfaces/ICLQuoter.sol b/src/pool-cl/interfaces/ICLQuoter.sol index ee2ffee..d60a95a 100644 --- a/src/pool-cl/interfaces/ICLQuoter.sol +++ b/src/pool-cl/interfaces/ICLQuoter.sol @@ -5,7 +5,7 @@ import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; import {Currency} from "pancake-v4-core/src/types/Currency.sol"; import {PathKey} from "../libraries/PathKey.sol"; -/// @title CLQuoter Interface +/// @title ICLQuoter 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 diff --git a/test/pool-bin/BinQuoter.t.sol b/test/pool-bin/BinQuoter.t.sol new file mode 100644 index 0000000..ff35323 --- /dev/null +++ b/test/pool-bin/BinQuoter.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; + +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {WETH} from "solmate/tokens/WETH.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import {Currency, CurrencyLibrary} from "pancake-v4-core/src/types/Currency.sol"; +import {IHooks} from "pancake-v4-core/src/interfaces/IHooks.sol"; +import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol"; +import {BinHelper} from "pancake-v4-core/src/pool-bin/libraries/BinHelper.sol"; +import {IBinPoolManager} from "pancake-v4-core/src/pool-bin/interfaces/IBinPoolManager.sol"; +import {BinPoolManager} from "pancake-v4-core/src/pool-bin/BinPoolManager.sol"; +import {BinPoolParametersHelper} from "pancake-v4-core/src/pool-bin/libraries/BinPoolParametersHelper.sol"; +import {Vault} from "pancake-v4-core/src/Vault.sol"; +import {BinFungiblePositionManager} from "../../src/pool-bin/BinFungiblePositionManager.sol"; +import {IBinFungiblePositionManager} from "../../src/pool-bin/interfaces/IBinFungiblePositionManager.sol"; +import {LiquidityParamsHelper} from "./helpers/LiquidityParamsHelper.sol"; +import {SafeCast} from "pancake-v4-core/src/pool-bin/libraries/math/SafeCast.sol"; +import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol"; +import {PackedUint128Math} from "pancake-v4-core/src/pool-bin/libraries/math/PackedUint128Math.sol"; +import {BinSwapRouter} from "../../src/pool-bin/BinSwapRouter.sol"; +import {BinSwapRouterBase} from "../../src/pool-bin/BinSwapRouterBase.sol"; +import {IBinSwapRouterBase} from "../../src/pool-bin/interfaces/IBinSwapRouterBase.sol"; +import {ISwapRouterBase} from "../../src/interfaces/ISwapRouterBase.sol"; +import {SwapRouterBase} from "../../src/SwapRouterBase.sol"; +import {IBinQuoter} from "../../src/pool-bin/interfaces/IBinQuoter.sol"; +import {BinQuoter} from "../../src/pool-bin/lens/BinQuoter.sol"; + +contract BinQuoterTest is Test, GasSnapshot { + using BinPoolParametersHelper for bytes32; + using SafeCast for uint256; + using PoolIdLibrary for PoolKey; + + bytes constant ZERO_BYTES = new bytes(0); + + PoolKey key; + PoolKey key2; + PoolKey key3; + Vault vault; + BinPoolManager poolManager; + BinFungiblePositionManager binFungiblePositionManager; + MockERC20 token0; + MockERC20 token1; + MockERC20 token2; + bytes32 poolParam; + BinSwapRouter router; + WETH weth; + + address alice = makeAddr("alice"); + address bob = makeAddr("bob"); + uint24 activeId = 2 ** 23; // where token0 and token1 price is the same + + function setUp() public { + weth = new WETH(); + vault = new Vault(); + poolManager = new BinPoolManager(IVault(address(vault)), 500000); + vault.registerApp(address(poolManager)); + router = new BinSwapRouter(vault, poolManager, address(weth)); + } + + function testBinQuoter_LockAcquired_VaultOnly() public { + vm.expectRevert(SwapRouterBase.NotVault.selector); + router.lockAcquired(new bytes(0)); + } +} \ No newline at end of file diff --git a/test/pool-cl/CLQuoter.t.sol b/test/pool-cl/CLQuoter.t.sol index 3154b97..b6760d3 100644 --- a/test/pool-cl/CLQuoter.t.sol +++ b/test/pool-cl/CLQuoter.t.sol @@ -40,7 +40,6 @@ contract CLQuoterTest is Test, Deployers { IVault public vault; CLPoolManager public manager; - CLPoolManagerRouter public router; ProtocolFeeControllerTest public feeController; CLQuoter quoter; @@ -59,7 +58,6 @@ contract CLQuoterTest is Test, Deployers { function setUp() public { (vault, manager) = createFreshManager(); - router = new CLPoolManagerRouter(vault, manager); feeController = new ProtocolFeeControllerTest(); manager.setProtocolFeeController(IProtocolFeeController(address(feeController))); quoter = new CLQuoter(vault, address(manager)); @@ -87,7 +85,7 @@ contract CLQuoterTest is Test, Deployers { setupPoolMultiplePositions(key02); } - function testQuoter_quoteExactInputSingle_ZeroForOne_MultiplePositions() public { + function testCLQuoter_quoteExactInputSingle_ZeroForOne_MultiplePositions() public { uint256 amountIn = 10000; uint256 expectedAmountOut = 9871; uint160 expectedSqrtPriceX96After = 78461846509168490764501028180; @@ -108,7 +106,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoaded, 2); } - function testQuoter_quoteExactInputSingle_OneForZero_MultiplePositions() public { + function testCLQuoter_quoteExactInputSingle_OneForZero_MultiplePositions() public { uint256 amountIn = 10000; uint256 expectedAmountOut = 9871; uint160 expectedSqrtPriceX96After = 80001962924147897865541384515; @@ -130,13 +128,13 @@ contract CLQuoterTest is Test, Deployers { } // nested self-call into lockAcquired reverts - function testQuoter_callLockAcquired_reverts() public { + function testCLQuoter_callLockAcquired_reverts() public { vm.expectRevert(ICLQuoter.LockFailure.selector); vm.prank(address(vault)); quoter.lockAcquired(abi.encodeWithSelector(quoter.lockAcquired.selector, address(this), "0x")); } - function testQuoter_quoteExactInput_0to2_2TicksLoaded() public { + function testCLQuoter_quoteExactInput_0to2_2TicksLoaded() public { tokenPath.push(token0); tokenPath.push(token2); ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); @@ -152,7 +150,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 2); } - function testQuoter_quoteExactInput_0to2_2TicksLoaded_initialiedAfter() public { + function testCLQuoter_quoteExactInput_0to2_2TicksLoaded_initialiedAfter() public { tokenPath.push(token0); tokenPath.push(token2); @@ -171,7 +169,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 1); } - function testQuoter_quoteExactInput_0to2_1TickLoaded() public { + function testCLQuoter_quoteExactInput_0to2_1TickLoaded() public { tokenPath.push(token0); tokenPath.push(token2); @@ -190,7 +188,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 1); } - function testQuoter_quoteExactInput_0to2_0TickLoaded_startingNotInitialized() public { + function testCLQuoter_quoteExactInput_0to2_0TickLoaded_startingNotInitialized() public { tokenPath.push(token0); tokenPath.push(token2); ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); @@ -206,7 +204,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 0); } - function testQuoter_quoteExactInput_0to2_0TickLoaded_startingInitialized() public { + function testCLQuoter_quoteExactInput_0to2_0TickLoaded_startingInitialized() public { setupPoolWithZeroTickInitialized(key02); tokenPath.push(token0); tokenPath.push(token2); @@ -223,7 +221,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 1); } - function testQuoter_quoteExactInput_2to0_2TicksLoaded() public { + function testCLQuoter_quoteExactInput_2to0_2TicksLoaded() public { tokenPath.push(token2); tokenPath.push(token0); ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); @@ -239,7 +237,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 2); } - function testQuoter_quoteExactInput_2to0_2TicksLoaded_initialiedAfter() public { + function testCLQuoter_quoteExactInput_2to0_2TicksLoaded_initialiedAfter() public { tokenPath.push(token2); tokenPath.push(token0); @@ -258,7 +256,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 2); } - function testQuoter_quoteExactInput_2to0_0TickLoaded_startingInitialized() public { + function testCLQuoter_quoteExactInput_2to0_0TickLoaded_startingInitialized() public { setupPoolWithZeroTickInitialized(key02); tokenPath.push(token2); tokenPath.push(token0); @@ -277,7 +275,7 @@ contract CLQuoterTest is Test, Deployers { } // 2->0 starting not initialized - function testQuoter_quoteExactInput_2to0_0TickLoaded_startingNotInitialized() public { + function testCLQuoter_quoteExactInput_2to0_0TickLoaded_startingNotInitialized() public { tokenPath.push(token2); tokenPath.push(token0); ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 103); @@ -293,7 +291,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 0); } - function testQuoter_quoteExactInput_2to1() public { + function testCLQuoter_quoteExactInput_2to1() public { tokenPath.push(token2); tokenPath.push(token1); ICLQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); @@ -308,7 +306,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 0); } - function testQuoter_quoteExactInput_0to2to1() public { + function testCLQuoter_quoteExactInput_0to2to1() public { tokenPath.push(token0); tokenPath.push(token2); tokenPath.push(token1); @@ -327,7 +325,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[1], 0); } - function testQuoter_quoteExactOutputSingle_0to1() public { + function testCLQuoter_quoteExactOutputSingle_0to1() public { (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter .quoteExactOutputSingle( ICLQuoter.QuoteExactSingleParams({ @@ -344,7 +342,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoaded, 0); } - function testQuoter_quoteExactOutputSingle_1to0() public { + function testCLQuoter_quoteExactOutputSingle_1to0() public { (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) = quoter .quoteExactOutputSingle( ICLQuoter.QuoteExactSingleParams({ @@ -361,7 +359,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoaded, 0); } - function testQuoter_quoteExactOutput_0to2_2TicksLoaded() public { + function testCLQuoter_quoteExactOutput_0to2_2TicksLoaded() public { tokenPath.push(token0); tokenPath.push(token2); ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); @@ -377,7 +375,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 2); } - function testQuoter_quoteExactOutput_0to2_1TickLoaded_initialiedAfter() public { + function testCLQuoter_quoteExactOutput_0to2_1TickLoaded_initialiedAfter() public { tokenPath.push(token0); tokenPath.push(token2); @@ -394,7 +392,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 1); } - function testQuoter_quoteExactOutput_0to2_1TickLoaded() public { + function testCLQuoter_quoteExactOutput_0to2_1TickLoaded() public { tokenPath.push(token0); tokenPath.push(token2); @@ -411,7 +409,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 1); } - function testQuoter_quoteExactOutput_0to2_0TickLoaded_startingInitialized() public { + function testCLQuoter_quoteExactOutput_0to2_0TickLoaded_startingInitialized() public { setupPoolWithZeroTickInitialized(key02); tokenPath.push(token0); tokenPath.push(token2); @@ -430,7 +428,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 1); } - function testQuoter_quoteExactOutput_0to2_0TickLoaded_startingNotInitialized() public { + function testCLQuoter_quoteExactOutput_0to2_0TickLoaded_startingNotInitialized() public { tokenPath.push(token0); tokenPath.push(token2); @@ -447,7 +445,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 0); } - function testQuoter_quoteExactOutput_2to0_2TicksLoaded() public { + function testCLQuoter_quoteExactOutput_2to0_2TicksLoaded() public { tokenPath.push(token2); tokenPath.push(token0); ICLQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); @@ -464,7 +462,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 2); } - function testQuoter_quoteExactOutput_2to0_2TicksLoaded_initialiedAfter() public { + function testCLQuoter_quoteExactOutput_2to0_2TicksLoaded_initialiedAfter() public { tokenPath.push(token2); tokenPath.push(token0); @@ -482,7 +480,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 2); } - function testQuoter_quoteExactOutput_2to0_1TickLoaded() public { + function testCLQuoter_quoteExactOutput_2to0_1TickLoaded() public { tokenPath.push(token2); tokenPath.push(token0); @@ -499,7 +497,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 1); } - function testQuoter_quoteExactOutput_2to1() public { + function testCLQuoter_quoteExactOutput_2to1() public { tokenPath.push(token2); tokenPath.push(token1); @@ -517,7 +515,7 @@ contract CLQuoterTest is Test, Deployers { assertEq(initializedTicksLoadedList[0], 0); } - function testQuoter_quoteExactOutput_0to2to1() public { + function testCLQuoter_quoteExactOutput_0to2to1() public { tokenPath.push(token0); tokenPath.push(token2); tokenPath.push(token1);