Skip to content
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

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/interfaces/IQuoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ interface IQuoter {
error NotSelf();
error UnexpectedRevertBytes(bytes revertData);

struct PoolDeltas {
int128 currency0Delta;
int128 currency1Delta;
}

struct QuoteExactSingleParams {
PoolKey poolKey;
bool zeroForOne;
Expand Down
29 changes: 29 additions & 0 deletions src/interfaces/IViewQuoter.sol
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
Comment on lines +11 to +20
Copy link
Collaborator

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:

/// @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);
}
1 change: 0 additions & 1 deletion src/lens/Quoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pragma solidity ^0.8.20;
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
Expand Down
24 changes: 24 additions & 0 deletions src/lens/ViewQuoter.sol
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);
}
}
69 changes: 69 additions & 0 deletions src/libraries/PoolTickBitmap.sol
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Uniswap/v4-core#832

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;
}
}
}
185 changes: 185 additions & 0 deletions src/libraries/QuoterMath.sol
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
}
}
}
Loading
Loading