-
Notifications
You must be signed in to change notification settings - Fork 498
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
View-only quoter #296
View-only quoter #296
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// SPDX-License-Identifier: GPL-2.0-or-later | ||
pragma solidity 0.8.26; | ||
|
||
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; | ||
|
||
/// @title Quoter Interface | ||
/// @notice Supports quoting the calculated amounts from hookless swaps. | ||
interface IViewQuoter { | ||
/// @notice Returns the amount taken or received for a swap of a single pool | ||
/// @param poolKey The poolKey identifying the pool traded against | ||
/// currency0 | ||
/// currency1 | ||
/// fee | ||
/// tickSpacing | ||
/// hooks | ||
/// @param swapParams The parameters used for the swap | ||
/// zeroForOne | ||
/// amountSpecified | ||
/// sqrtPriceLimitX96 | ||
/// @return amount0 the amount of token0 sent in or out of the pool | ||
/// @return amount1 the amount of token1 sent in or out of the pool | ||
/// @return sqrtPriceAfterX96 the price of the pool after the swap | ||
/// @return initializedTicksCrossed the number of initialized ticks LOADED IN | ||
function quoteSingle(PoolKey calldata poolKey, IPoolManager.SwapParams calldata swapParams) | ||
external | ||
view | ||
returns (int256, int256, uint160, uint32); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.8.26; | ||
|
||
import {IViewQuoter} from "../interfaces/IViewQuoter.sol"; | ||
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; | ||
import {QuoterMath} from "../libraries/QuoterMath.sol"; | ||
|
||
contract ViewQuoter is IViewQuoter { | ||
IPoolManager public immutable poolManager; | ||
|
||
constructor(IPoolManager _poolManager) { | ||
poolManager = _poolManager; | ||
} | ||
|
||
function quoteSingle(PoolKey calldata poolKey, IPoolManager.SwapParams calldata swapParams) | ||
public | ||
view | ||
override | ||
returns (int256, int256, uint160, uint32) | ||
{ | ||
return QuoterMath.quote(poolManager, poolKey, swapParams); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity 0.8.26; | ||
|
||
import {IPoolManager} from "lib/v4-core/src/interfaces/IPoolManager.sol"; | ||
import {PoolId} from "lib/v4-core/src/types/PoolId.sol"; | ||
import {BitMath} from "@uniswap/v4-core/src/libraries/BitMath.sol"; | ||
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; | ||
|
||
/// @title Packed tick initialized state library | ||
/// @notice Stores a packed mapping of tick index to its initialized state | ||
/// @dev The mapping uses int16 for keys since ticks are represented as int24 and there are 256 (2^8) values per word. | ||
library PoolTickBitmap { | ||
using StateLibrary for IPoolManager; | ||
|
||
/// @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) { | ||
wordPos = int16(tick >> 8); | ||
bitPos = uint8(uint24(tick % 256)); | ||
} | ||
|
||
/// @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 poolId The pool id | ||
/// @param tickSpacing the tick spacing of the pool | ||
/// @param tick The starting tick | ||
/// @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 nextInitializedTickWithinOneWord( | ||
IPoolManager poolManager, | ||
PoolId poolId, | ||
int24 tickSpacing, | ||
int24 tick, | ||
bool lte | ||
) internal view returns (int24 next, bool initialized) { | ||
Comment on lines
+32
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it bothers me that we need to re-implement this again in periphery. Here's a draft PR on core which makes the library usable by periphery |
||
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 = poolManager.getTickBitmap(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.getTickBitmap(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; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
// SPDX-License-Identifier: GPL-2.0-or-later | ||
pragma solidity 0.8.26; | ||
|
||
import {SwapMath} from "@uniswap/v4-core/src/libraries/SwapMath.sol"; | ||
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; | ||
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; | ||
import {LiquidityMath} from "@uniswap/v4-core/src/libraries/LiquidityMath.sol"; | ||
import {PoolTickBitmap} from "./PoolTickBitmap.sol"; | ||
import {Slot0, Slot0Library} from "@uniswap/v4-core/src/types/Slot0.sol"; | ||
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; | ||
import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; | ||
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; | ||
|
||
library QuoterMath { | ||
using SafeCast for uint256; | ||
using SafeCast for int256; | ||
|
||
using Slot0Library for Slot0; | ||
using StateLibrary for IPoolManager; | ||
using PoolIdLibrary for PoolKey; | ||
|
||
struct Slot0Struct { | ||
// the current price | ||
uint160 sqrtPriceX96; | ||
// the current tick | ||
int24 tick; | ||
// tick spacing | ||
int24 tickSpacing; | ||
} | ||
|
||
// used for packing under the stack limit | ||
struct QuoteParams { | ||
bool zeroForOne; | ||
bool exactInput; | ||
uint24 fee; | ||
uint160 sqrtPriceLimitX96; | ||
} | ||
|
||
function fillSlot0(IPoolManager poolManager, PoolKey calldata poolKey) | ||
private | ||
view | ||
returns (Slot0Struct memory slot0) | ||
{ | ||
(slot0.sqrtPriceX96, slot0.tick,,) = poolManager.getSlot0(poolKey.toId()); | ||
slot0.tickSpacing = poolKey.tickSpacing; | ||
return slot0; | ||
} | ||
|
||
// the top level state of the swap, the results of which are recorded in storage at the end | ||
struct SwapState { | ||
// the amount remaining to be swapped in/out of the input/output asset | ||
int256 amountSpecifiedRemaining; | ||
// the amount already swapped out/in of the output/input asset | ||
int256 amountCalculated; | ||
// current sqrt(price) | ||
uint160 sqrtPriceX96; | ||
// the tick associated with the current price | ||
int24 tick; | ||
// the global fee growth of the input token | ||
uint256 feeGrowthGlobalX128; | ||
// amount of input token paid as protocol fee | ||
uint128 protocolFee; | ||
// the current liquidity in range | ||
uint128 liquidity; | ||
} | ||
|
||
struct StepComputations { | ||
// the price at the beginning of the step | ||
uint160 sqrtPriceStartX96; | ||
// the next tick to swap to from the current tick in the swap direction | ||
int24 tickNext; | ||
// whether tickNext is initialized or not | ||
bool initialized; | ||
// sqrt(price) for the next tick (1/0) | ||
uint160 sqrtPriceNextX96; | ||
// how much is being swapped in in this step | ||
uint256 amountIn; | ||
// how much is being swapped out | ||
uint256 amountOut; | ||
// how much fee is being paid in | ||
uint256 feeAmount; | ||
} | ||
|
||
/// @notice Utility function called by the quote functions to | ||
/// calculate the amounts in/out for a hookless v4 swap | ||
/// @param poolManager the Uniswap v4 pool manager | ||
/// @param poolKey The poolKey identifying the pool traded against | ||
/// @param swapParams The parameters used for the swap | ||
/// @return amount0 the amount of token0 sent in or out of the pool | ||
/// @return amount1 the amount of token1 sent in or out of the pool | ||
/// @return sqrtPriceAfterX96 the price of the pool after the swap | ||
/// @return initializedTicksCrossed the number of initialized ticks LOADED IN | ||
function quote(IPoolManager poolManager, PoolKey calldata poolKey, IPoolManager.SwapParams calldata swapParams) | ||
internal | ||
view | ||
returns (int256 amount0, int256 amount1, uint160 sqrtPriceAfterX96, uint32 initializedTicksCrossed) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. optional but I like the idea of making the return type a BalanceDelta 😎 |
||
{ | ||
QuoteParams memory quoteParams = QuoteParams( | ||
swapParams.zeroForOne, swapParams.amountSpecified < 0, poolKey.fee, swapParams.sqrtPriceLimitX96 | ||
); | ||
initializedTicksCrossed = 1; | ||
|
||
Slot0Struct memory slot0 = fillSlot0(poolManager, poolKey); | ||
|
||
SwapState memory state = SwapState({ | ||
amountSpecifiedRemaining: -swapParams.amountSpecified, | ||
amountCalculated: 0, | ||
sqrtPriceX96: slot0.sqrtPriceX96, | ||
tick: slot0.tick, | ||
feeGrowthGlobalX128: 0, | ||
protocolFee: 0, | ||
liquidity: poolManager.getLiquidity(poolKey.toId()) | ||
}); | ||
|
||
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != quoteParams.sqrtPriceLimitX96) { | ||
StepComputations memory step; | ||
|
||
step.sqrtPriceStartX96 = state.sqrtPriceX96; | ||
|
||
(step.tickNext, step.initialized) = PoolTickBitmap.nextInitializedTickWithinOneWord( | ||
poolManager, poolKey.toId(), slot0.tickSpacing, state.tick, quoteParams.zeroForOne | ||
); | ||
|
||
// ensure that we do not overshoot the min/max tick, as the tick bitmap is not aware of these bounds | ||
if (step.tickNext < TickMath.MIN_TICK) { | ||
step.tickNext = TickMath.MIN_TICK; | ||
} else if (step.tickNext > TickMath.MAX_TICK) { | ||
step.tickNext = TickMath.MAX_TICK; | ||
} | ||
|
||
// get the price for the next tick | ||
step.sqrtPriceNextX96 = TickMath.getSqrtPriceAtTick(step.tickNext); | ||
|
||
// compute values to swap to the target tick, price limit, or point where input/output amount is exhausted | ||
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep( | ||
state.sqrtPriceX96, | ||
( | ||
quoteParams.zeroForOne | ||
? step.sqrtPriceNextX96 < quoteParams.sqrtPriceLimitX96 | ||
: step.sqrtPriceNextX96 > quoteParams.sqrtPriceLimitX96 | ||
) ? quoteParams.sqrtPriceLimitX96 : step.sqrtPriceNextX96, | ||
state.liquidity, | ||
-state.amountSpecifiedRemaining, | ||
quoteParams.fee | ||
); | ||
|
||
if (quoteParams.exactInput) { | ||
state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256(); | ||
state.amountCalculated = state.amountCalculated + step.amountOut.toInt256(); | ||
} else { | ||
state.amountSpecifiedRemaining += step.amountOut.toInt256(); | ||
state.amountCalculated = state.amountCalculated - (step.amountIn + step.feeAmount).toInt256(); | ||
} | ||
|
||
// shift tick if we reached the next price | ||
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) { | ||
// if the tick is initialized, run the tick transition | ||
if (step.initialized) { | ||
(, int128 liquidityNet) = poolManager.getTickLiquidity(poolKey.toId(), step.tickNext); | ||
|
||
// if we're moving leftward, we interpret liquidityNet as the opposite sign | ||
// safe because liquidityNet cannot be type(int128).min | ||
if (quoteParams.zeroForOne) liquidityNet = -liquidityNet; | ||
|
||
state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet); | ||
|
||
initializedTicksCrossed++; | ||
} | ||
|
||
state.tick = quoteParams.zeroForOne ? step.tickNext - 1 : step.tickNext; | ||
} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) { | ||
// recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved | ||
state.tick = TickMath.getTickAtSqrtPrice(state.sqrtPriceX96); | ||
} | ||
|
||
(amount0, amount1) = quoteParams.zeroForOne == quoteParams.exactInput | ||
? (state.amountSpecifiedRemaining + swapParams.amountSpecified, state.amountCalculated) | ||
: (state.amountCalculated, state.amountSpecifiedRemaining + swapParams.amountSpecified); | ||
|
||
sqrtPriceAfterX96 = state.sqrtPriceX96; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
have we conventionally done natspec like this for struct params :thonk: