Get all ticks of Uniswap V3 pools
How to get all ticks, aka liquidity info, of Uniswap V3 pools directly from on-chain data with Javascript and Solidity.
To draw liquidity distribution or calculate quotes of a Uniswap V3 pool, we need to know all ticks’ info. Currently, we use subgraphs or custom indexers to scan the mint/burn events, which can be out of sync and costly. There are no methods to get all the info from pool contracts with ease. So, this blog will show a solution with 1 query to blockchain.
Tick bitmap
Bitmap is a compact way to index data. Uniswap V3 uses this technique to know which tick has liquidity. When a bitmap’s bit is 1, the corresponding tick has liquidity. Otherwise, there are no liquidity in the tick.
We can use bitmap to scan all ticks faster. When a word is zero, we skip entire 256 ticks. Otherwise, we travel each bit. We only get tick info if its corresponding bit is 1. This reduces storage access and saves gas.
Get all ticks at once
Uniswap V3 provides 2 view functions: tickBitmap() to get a word and ticks() to get a tick info. So, we need tons of queries to RPCs even with the bitmap traveling method, which isn’t good. To avoid this, we will deploy a contract handling all works for us.
The first half of the below getAllTicks function calculates the number of initialized ticks (ticks with liquidity). This is because we have to know the array length before creating a dynamic array. Then, we travel all bitmaps again to get ticks’ info.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
interface IPool {
function tickSpacing() external view returns (int24);
function tickBitmap(int16 wordPos) external view returns (uint256);
function ticks(int24 tick) external view returns (uint128 liquidityGross, int128 liquidityNet);
}
struct Tick {
int24 index;
uint128 liquidityGross;
int128 liquidityNet;
}
contract UniswapV3Lens {
int24 internal constant MIN_TICK = -887272;
int24 internal constant MAX_TICK = -MIN_TICK;
function getAllTicks(IPool pool) external view returns (Tick[] memory ticks) {
int24 tickSpacing = pool.tickSpacing();
int256 minWord = int16((MIN_TICK / tickSpacing) >> 8);
int256 maxWord = int16((MAX_TICK / tickSpacing) >> 8);
uint256 numTicks = 0;
for (int256 word = minWord; word <= maxWord; word++) {
uint256 bitmap = pool.tickBitmap(int16(word));
if (bitmap == 0) continue;
for (uint256 bit; bit < 256; bit++) if (bitmap & (1 << bit) > 0) numTicks++;
}
ticks = new Tick[](numTicks);
uint256 idx = 0;
for (int256 word = minWord; word <= maxWord; word++) {
uint256 bitmap = pool.tickBitmap(int16(word));
if (bitmap == 0) continue;
for (uint256 bit; bit < 256; bit++) {
if (bitmap & (1 << bit) == 0) continue;
ticks[idx].index = int24((word << 8) + int256(bit)) * tickSpacing;
(ticks[idx].liquidityGross, ticks[idx].liquidityNet) = pool.ticks(ticks[idx].index);
idx++;
}
}
}
}I deployed the above UniswapV3Lens contract on Arbitrum network. You can check it at ArbiScan. Let’s query some pools with Javascript and ethers.js library:
const ethers = require("ethers");
async function main() {
const abi = ["function getAllTicks(address pool) external view returns (tuple(int24 index, uint128 liquidityGross, int128 liquidityNet)[] memory ticks)"];
const provider = new ethers.JsonRpcProvider("https://arb1.arbitrum.io/rpc");
const lens = new ethers.Contract("0xf632a03754090B44B605C0bA417Fffe369E26397", abi, provider);
await getTicks(lens, "WETH-USDT-0.05%", "0x641C00A822e8b671738d32a431a4Fb6074E5c79d");
await getTicks(lens, "USDC-USDT-0.01%", "0xbE3aD6a5669Dc0B8b12FeBC03608860C31E2eef6");
}
async function getTicks(lens, name, pool) {
const ticks = await lens.getAllTicks(pool);
console.log(`${name}: ${ticks.length} ticks, ticks[0]: ${ticks[0]}`);
}
main();WETH-USDT-0.05%: 1792 ticks, ticks[0]: -887270,36881048071624030145431278,36881048071624030145431278
USDC-USDT-0.01%: 723 ticks, ticks[0]: -887272,3053998183,3053998183Get ticks partially
Querying all ticks at once will work perfectly if the cost of retrieving all ticks is less than the gas limit. In the future, people will add more liquidity with new ticks, so we need a robust method to query. And, the solution is getting smaller sets of ticks multiple times until having full info.
We will add a new view function into the contract: getTicks(). This function allows us to get a predefined number of ticks from a tick index. It is nearly the same with getAllTicks(), except that we don’t need to calculate the array length.
function getTicks(IPool pool, int24 tickStart, uint256 numTicks) external view returns (Tick[] memory ticks) {
int24 tickSpacing = pool.tickSpacing();
int256 maxWord = int16((MAX_TICK / tickSpacing) >> 8);
tickStart /= tickSpacing;
int256 wordStart = int16(tickStart >> 8);
uint256 bitStart = uint8(uint24(tickStart % 256));
ticks = new Tick[](numTicks);
uint256 idx = 0;
for (int256 word = wordStart; word <= maxWord; word++) {
uint256 bitmap = pool.tickBitmap(int16(word));
if (bitmap == 0) continue;
for (uint256 bit = word == wordStart ? bitStart : 0; bit < 256; bit++) {
if (bitmap & (1 << bit) == 0) continue;
ticks[idx].index = int24((word << 8) + int256(bit)) * tickSpacing;
(ticks[idx].liquidityGross, ticks[idx].liquidityNet) = pool.ticks(ticks[idx].index);
if (++idx >= numTicks) return ticks;
}
}
}In our off-chain script, we query 1000 ticks each time. We only stop when the returned ticks is less than 1000. Here is the example:
const ethers = require("ethers");
async function main() {
const abi = ["function getTicks(address pool, int24 tickStart, uint256 maxTicks) external view returns (tuple(int24 index, uint128 liquidityGross, int128 liquidityNet)[] memory ticks)"];
const provider = new ethers.JsonRpcProvider("https://arb1.arbitrum.io/rpc");
const lens = new ethers.Contract("0xf632a03754090B44B605C0bA417Fffe369E26397", abi, provider);
await getTicks(lens, "WETH-USDT-0.05%", "0x641C00A822e8b671738d32a431a4Fb6074E5c79d");
await getTicks(lens, "USDC-USDT-0.01%", "0xbE3aD6a5669Dc0B8b12FeBC03608860C31E2eef6");
}
async function getTicks(lens, name, pool) {
const step = 1000;
let tickStart = -887272; // MIN_TICK
let results = [];
while (true) {
let ticks = await lens.getTicks(pool, tickStart, step);
ticks = ticks.filter((t) => t.liquidityGross > 0n);
console.log(`>>> ${name}: got ${ticks.length} ticks`);
results = results.concat(ticks);
tickStart = results[results.length - 1].index + 1n;
if (ticks.length < step) break;
}
console.log(`${name}: ${results.length} ticks, ticks[0]: ${results[0]}`);
}
main();>>> WETH-USDT-0.05%: got 1000 ticks
>>> WETH-USDT-0.05%: got 792 ticks
WETH-USDT-0.05%: 1792 ticks, ticks[0]: -887270,36881048071624030145431278,36881048071624030145431278
>>> USDC-USDT-0.01%: got 723 ticks
USDC-USDT-0.01%: 723 ticks, ticks[0]: -887272,3053998183,3053998183This method helps retrieving liquidity of a Uniswap V3 pool faster and more efficient. We can draw liquidity distribution and calculate quotes without custom indexers or subgraphs. Thank you.
