4

I am trying to compute tick liquidity for the ETH/USDC pair in Uniswap V3 within a range of +-10 tick spaces from the current tick. I'm querying pool and tick data using The Graph API. However, I've run into an issue: my computed liquidity for some ticks is negative, which contradicts my understanding that liquidity should always be positive.

Data Gathering

I initially query the pool data using the following GraphQL query:

pools(where: {or: [{id:"0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"}]}) {
  id
  feeTier
  sqrtPrice
  tick
  liquidity
  token0 {
    symbol
  }
  token1 {
    symbol
  }
}

Response:

{
  id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
  feeTier: '500',
  sqrtPrice: '1995791025043001032341104211323643',
  tick: '202694',
  liquidity: '23916677833605040001',
  token0: { symbol: 'USDC' },
  token1: { symbol: 'WETH' }
}

This provides me with a current tick of 202690 for the ETH/USDC pair.

Tick Data and Computation

Next, I query tick data within +-4 tick spaces from the current tick (202690) and attempt to compute liquidity.

  {
    ticks(where: {or: [{poolAddress:"0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",tickIdx_gte:202490,tickIdx_lte:202690}]}) {
      id
      poolAddress
      tickIdx
      liquidityGross
      liquidityNet
    }
  }
[
  {
    id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202650',
    poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
    tickIdx: '202650',
    liquidityGross: '2336388181864090595566',
    liquidityNet: '492712989890016420068',
    liquidity: 1441630710471656258600n
  },
  {
    id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202660',
    poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
    tickIdx: '202660',
    liquidityGross: '371131701748117601992',
    liquidityNet: '-2458079249156612806780',
    liquidity: -1016448538684956548180n
  },
  {
    id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202670',
    poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
    tickIdx: '202670',
    liquidityGross: '-1044298283082905453039',
    liquidityNet: '1046161038635907397955',
    liquidity: 29712499950950849775n
  },
  {
    id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202680',
    poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
    tickIdx: '202680',
    liquidityGross: '624539598789633437',
    liquidityNet: '-624539598789633437',
    liquidity: 29087960352161216338n
  },
  {
    id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202690',
    poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
    tickIdx: '202690',
    liquidityGross: '5171245502515484072',
    liquidityNet: '-5171245502515484072',
    liquidity: 23916714849645732266n
  },
  {
    id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202700',
    poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
    tickIdx: '202700',
    liquidityGross: '24478130685888414',
    liquidityNet: '-15412937749140902',
    liquidity: 23901301911896591364n
  },
  {
    id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202710',
    poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
    tickIdx: '202710',
    liquidityGross: '5028095487507403',
    liquidityNet: '-5028095487507403',
    liquidity: 23896273816409083961n
  },
  {
    id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202720',
    poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
    tickIdx: '202720',
    liquidityGross: '70992889483135757',
    liquidityNet: '-70181736824935037',
    liquidity: 23826092079584148924n
  },
  {
    id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202730',
    poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
    tickIdx: '202730',
    liquidityGross: '2136174279021464652',
    liquidityNet: '-1947978297530543726',
    liquidity: 21878113782053605198n
  }
]

I'm using the following formula to compute the liquidity for tick 202660:

tick[202660].liquidity = tick[262690].liquidity - tick[262690].liquidityNet - tick[262680].liquidityNet - tick[262670].liquidityNet
= -1016448538684956548180n

The result is a negative liquidity (-1016448538684956548180n), which seems incorrect.

Question

Is my computation for tick liquidity incorrect? If so, what is the correct way to calculate tick liquidity in Uniswap V3 for a given range?

========== 2023/09/27 updated ==========

I have compared the liquidityNet between ethscan and thegraphapi? The liquidityNet is difference in tick 202650~202670

thegraphapi

  {
    ticks(where: {or: [{poolAddress:"0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",tickIdx_gte:202600,tickIdx_lte:202700}]}) {
      id
      poolAddress
      tickIdx
      liquidityGross
      liquidityNet
    }
  }

{
  "data": {
    "ticks": [
      {
        "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202600",
        "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
        "tickIdx": "202600",
        "liquidityGross": "1038000403835928850",
        "liquidityNet": "-479724504561604558"
      },
      {
        "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202610",
        "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
        "tickIdx": "202610",
        "liquidityGross": "8291996536383547138",
        "liquidityNet": "-7176316981317573592"
      },
      {
        "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202620",
        "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
        "tickIdx": "202620",
        "liquidityGross": "7956402413878331796",
        "liquidityNet": "2279178812195187082"
      },
      {
        "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202630",
        "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
        "tickIdx": "202630",
        "liquidityGross": "386008744872409375",
        "liquidityNet": "310871568274768229"
      },
      {
        "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202640",
        "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
        "tickIdx": "202640",
        "liquidityGross": "922181074779082625634",
        "liquidityNet": "921376491976978808388"
      },
      {
        "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202650",
        "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
        "tickIdx": "202650",
        "liquidityGross": "2341800287908032536731",
        "liquidityNet": "487300883846074478903"
      },
      {
        "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202660",
        "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
        "tickIdx": "202660",
        "liquidityGross": "371194532086084630838",
        "liquidityNet": "-2458142079494579835626"
      },
      {
        "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202670",
        "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
        "tickIdx": "202670",
        "liquidityGross": "-1045583905759190816622",
        "liquidityNet": "1045587386689913628088"
      },
      {
        "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202680",
        "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
        "tickIdx": "202680",
        "liquidityGross": "25806409821400891",
        "liquidityNet": "-25806409821400891"
      },
      {
        "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202690",
        "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
        "tickIdx": "202690",
        "liquidityGross": "4390795436492533285",
        "liquidityNet": "-4235178605437148909"
      },
      {
        "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#202700",
        "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
        "tickIdx": "202700",
        "liquidityGross": "583460706473303477",
        "liquidityNet": "-574395513536555965"
      }

ethscan

https://etherscan.io/address/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#readContract#F15
and input tick in ticks input box and then query

input 202650, get -378705826327612370
input 202660, get -2888234503751328

Is data of thegraphapi wrong?

5
  • Can you post the gql query you used to get the tick data? I don't see a liquidity field in the v3-subgraph Tick entity, so curious how you're getting this.
    – MShakeG
    Commented Sep 26, 2023 at 8:00
  • Also, you might want to make a single gql query that returns all relevant data as if you're making separate queries there's a possibility of the data changing over time.
    – MShakeG
    Commented Sep 26, 2023 at 8:02
  • @MShakeG I have post gql of tick query. The query response from thegraph do not include liquidity, the liquidity is from pool query(my first query). Then I calculate neighbor tick liquidity by tick's liquidityNet
    – Hurry
    Commented Sep 27, 2023 at 5:39
  • Does my answer answer your question?
    – MShakeG
    Commented Sep 27, 2023 at 6:38
  • @MShakeG it is helpful, but I still have other question. I post at comment of answer
    – Hurry
    Commented Sep 27, 2023 at 15:04

1 Answer 1

2

1. No Such Thing as Negative Active Liquidity:

You are correct, active liquidity can't be negative.

2. Fields in Tick Structure

Uniswap V3 ticks have two liquidity-related fields, liquidityGross and liquidityNet. There's no direct liquidity field in the tick structure. The liquidity field in your tick data array appears to be your own incorrect computation of the expected active liquidity if the current price were to cross such that that the given tick is the nearest initialised tick that is below or equal to the currentTick.

3. Calculating Active Liquidity

The calculation for active liquidity as you cross ticks depends on the liquidityNet values of each tick crossed and the direction in which you are crossing the ticks.

Generalized Formula in TypeScript:

// Define a minimal Tick type
type Tick = {
  tickIdx: number;
  liquidityNet: bigint;
};

/**
* Compute active liquidity at a specific stop tick, given an initial tick and liquidity.
*
* @param ticks - An array of Tick objects, each containing tickIdx and liquidityNet.
* @param currentTickIdx - The tick index where you are starting.
* @param initialLiquidity - The active liquidity at the currentTickIdx.
* @param stopTickIdx - The tick index where the calculation will stop. Whether movingUpward or not stop just above stopTickIdx
*
* @returns The computed active liquidity at the stopTickIdx.
*/
const computeActiveLiquidityAtStopTick = (
  ticks: Tick[],
  currentTickIdx: number,
  initialLiquidity: bigint,
  stopTickIdx: number
): bigint => {
  // Sort the tick array by tickIdx for easier traversal
  const sortedTicks = [...ticks].sort((a, b) => a.tickIdx - b.tickIdx);

  // Initialize state with given initial liquidity
  let state = { liquidity: initialLiquidity };

  // Determine the direction: true if moving upward/rightward/forward, false if moving downward/leftward/backward
  // upward movement occurs if zeroForOne is false and downward movement ocurrs if zeroForOne is true

  const movingUpward = stopTickIdx > currentTickIdx;

  for (const { tickIdx, liquidityNet } of sortedTicks) {
    // Skip ticks that are not in the path from currentTickIdx to stopTickIdx
    if (movingUpward && (tickIdx <= currentTickIdx || tickIdx >= stopTickIdx)) continue;
    if (!movingUpward && (tickIdx >= currentTickIdx || tickIdx < stopTickIdx)) continue;

    // Update liquidity based on direction
    // If moving upward, we add liquidityNet. If moving downward, we subtract.
    state.liquidity += (movingUpward ? 1n : -1n) * liquidityNet;
  }

  return state.liquidity;
};

// subgraphTicks is the tick array mentioned in your question
const ticks: Tick[] = subgraphTicks.map((subgraphTick) => {
  return {
    tickIdx: Number(subgraphTick.tickIdx),
    liquidityNet: BigInt(subgraphTick.liquidityNet)
  }
})

const initialLiquidity = BigInt('23916677833605040001');
const currentTickIdx = 202694;
const stopTickIdx = 202660; // will cross up to but not including the stop tick
const resultLower = computeActiveLiquidityAtStopTick(ticks, currentTickIdx, initialLiquidity, stopTickIdx);
// this is effectively the active liquidity between ticks 202660 and 202670
console.log(`Computed active liquidity at tick ${stopTickIdx}: ${resultLower}`);
// 1441630673455615566335n

In this function, ticks is an array of ticks that must be crossed from the currentTickIdx to the stopTickIdx, currentTick is the tick where you start, initialLiquidity is the active liquidity at currentTick, and stopTick is the tick you want to calculate the active liquidity for. Note that the array of ticks should include liquidityNet for each tick, and the ticks should be between the currentTick and stopTick exclusively.

4. Direction Matters

The liquidityNet of a tick is an aggregate measure of all the liquidity referencing the tick across different liquidity positions where the tick could simultaneously be the lower tick of one position and the upper tick of another position. It's a signed 128-bit integer(i.e. int128) reflecting the net effect of all changes in liquidity at that particular tick. For example, if a position with liquidityMinted=liquidityDelta is minted on ticks [lower, upper] then the liquidityNet of lower will increase by liquidityDelta and the liquidityNet of upper will decrease by liquidityDelta. If the tick is a newly initialized tick then this increase/decrease on the lower/upper tick respectively will be from the tick's liquidityNet default value i.e. 0.

This should help give you an intuition regarding the following:

5. How liquidityNet Impacts the Change to Active Liquidity

Upward Movement (Lower to Higher Tick): If you're crossing a tick upwards, you add the tick's liquidityNet to the current active liquidity to get the new active liquidity after crossing. A positive liquidityNet would mean an increase in active liquidity, and a negative liquidityNet would result in a decrease.

Downward Movement (Higher to Lower Tick): If you're crossing a tick downwards, you subtract the tick's liquidityNet from the current active liquidity to get the new active liquidity after crossing. Again, a positive liquidityNet would mean a decrease in active liquidity, and a negative liquidityNet would mean an increase.

11
  • To the part 3, I have checked your computation tick[202660].liquidity= 23916677833605040001 - tick[202690].liquidityNet-tick[202670].liquidityNet-tick[202660].liquidityNet = 1441630673455615566335 But when we check tick[202670]' liquidity, would get -1016448575700997240445
    – Hurry
    Commented Sep 27, 2023 at 11:06
  • I have post some data in 20230927 above, some ticks' liquidityNet is difference between thegraphapi and ethscan
    – Hurry
    Commented Sep 27, 2023 at 11:22
  • @Hurry there seems to be a bug in the uniswap v3-subgraph as it returns a negative value for liquidityGross for tick 202670, however liquidityGross is a uint128(i.e. >= 0) and if you access its value directly from the contract by calling ticks(202670) you'll observe that it is positive. etherscan.io/address/…
    – MShakeG
    Commented Sep 27, 2023 at 17:31
  • @MShakeG thank you very much for this code. I've tried computations for USDC/WETH pool app.uniswap.org/explore/pools/ethereum/… replacing const ticks = [{ tickIdx: 193620, liquidityNet: 206931443626921680n }, { tickIdx: 193680, liquidityNet: 4437871585290785n }, { tickIdx: 193740, liquidityNet: -3613995902018273n }, { tickIdx: 193800, liquidityNet: 15922075084830948n }, { tickIdx: 193860, liquidityNet: 7300076493567658738n }] and got result 1559452697016368193. Could you please help interpret this result, how much is this in USDC and WETH? Commented May 26 at 18:37
  • I've seen the guide uniswapv3book.com/milestone_1/calculating-liquidity.html, but still not fully understand how to convert liquidity to two tokens amount, without this knowledge I can't use this code fully Commented May 26 at 18:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.