Get Uniswap V3 position amounts & unclaimed fees from NFT ID

Get Uniswap V3 position amounts & unclaimed fees from NFT ID

Learn how to input a Uniswap V3 NFT ID and a block number to get a liquidity position snapshot

Hello guys! This tutorial will explain how to get a snapshot of a liquidity position (amounts and unclaimed fee) from a Uniswap V3 NFT ID. That could come in handy for an airdrop, to reward liquidity providers. Let's get started!

Dependencies we'll use:

  • web3@1.2.9

  • ethers@5.6.1

  • @uniswap/sdk@3.0.3

Github repo for the full code: https://github.com/BenAzlay/UniswapV3LiquiditySnapshotMaker

Get liquidity position amounts

First of all, let's instance Uniswap's NonfungiblePositionManager contract to get on-chain position data (the ABI can be found on Etherscan). We'll use Ethers for this, and not Web3.js, because Ethers enables us to make static call, which will be useful later to fetch the unclaimed fees ;)

const provider = new ethers.providers.StaticJsonRpcProvider("<your-mainnet-rpc>", 1);
const positionManagerContract = new ethers.Contract(
      "0xC36442b4a4522E871399CD717aBDD847Ab11FE88",
      NFTPositionManagerABI,
      provider,
);

Now we can make calls to the positions method to get data on the liquidity position corresponding to the nftId (of your choice). Here and in the rest of this tutorial, defaultBlock will be the block number at which we are fetching the on-chain data. You can set it at "latest" to get the latest block's data.

const {
      token0: token0Address,
      token1: token1Address,
      fee,
      tickLower,
      tickUpper,
      liquidity,
} = await positionManagerContract.functions.positions(nftId, { blockTag: defaultBlock });

This call does not get us all the data we'd want, though. For example, we don't have the address of the owner of that NFT. Here's how to get it:

const [nftOwner] = await positionManagerContract.functions.ownerOf(nftId, { blockTag: defaultBlock });

Now we need to fetch data from the pool itself. To do this, we first have to fetch the pool address from the UniswapV3Factory contract, then instanciate the pool's contract using that address. Once we have the pool's contract, we can fetch sqrtPriceX96, which we'll use to calculate the amounts. We also need to get the token decimals so we can convert the amounts in wei to the actual human-readable amounts.

// Get pool address from token addresses and fee
const uniswapV3FactoryContract = new web3.eth.Contract(UniswapV3FactoryAbi, '0x1F98431c8aD98523631AE4a59f267346ea31F984');
const poolAddress = await uniswapV3FactoryContract.methods.getPool(token0Address, token1Address, fee).call({}, defaultBlock);

// Get sqrtPriceX96 from pool contract
const poolContract = new web3.eth.Contract(UniswapPoolContractAbi, poolAddress);
const { sqrtPriceX96 } = await poolContract.methods.slot0().call({}, defaultBlock);

// Get token decimals
const token0Contract = new web3.eth.Contract(Erc20Abi, token0Address);
const decimals0 = await token0Contract.methods.decimals().call({}, defaultBlock);

const token1Contract = new web3.eth.Contract(Erc20Abi, token1Address);
const decimals1 = await token1Contract.methods.decimals().call({}, defaultBlock);

And now, we get into the weeds of it: Uniswap maths! Those maths are very complicated but you can read about them here. For the following code, huge thanks to Crypto_Rachel on Ethereum Stack Exchange for her brilliant answer.

import { JSBI } from "@uniswap/sdk";
import { BigNumber } from "@ethersproject/bignumber";

const Q96 = JSBI.exponentiate(JSBI.BigInt(2), JSBI.BigInt(96));
const MAX_UINT128 = BigNumber.from(2).pow(128).sub(1);

function getTickAtSqrtRatio(sqrtPriceX96){
  let tick = Math.floor(Math.log((sqrtPriceX96/Q96)**2)/Math.log(1.0001));
  return tick;
}

// Calculate amounts
let sqrtRatioA = Math.sqrt(1.0001**tickLower).toFixed(18);
let sqrtRatioB = Math.sqrt(1.0001**tickUpper).toFixed(18);
let currentTick = getTickAtSqrtRatio(sqrtPriceX96);
let currentRatio = Math.sqrt(1.0001**currentTick).toFixed(18);
let amount0wei = 0;
let amount1wei = 0;
if(currentTick <= tickLower){
      amount0wei = Math.floor(liquidity*((sqrtRatioB-sqrtRatioA)/(sqrtRatioA*sqrtRatioB)));
}
if(currentTick > tickUpper){
        amount1wei = Math.floor(liquidity*(sqrtRatioB-sqrtRatioA));
}
if(currentTick >= tickLower && currentTick < tickUpper){ 
        amount0wei = Math.floor(liquidity*((sqrtRatioB-currentRatio)/(currentRatio*sqrtRatioB)));
        amount1wei = Math.floor(liquidity*(currentRatio-sqrtRatioA));
}
const amount0 = amount0wei / (10**decimals0);
const amount1 = amount1wei / (10**decimals1);

Get unclaimed fees

For the unclaimed fees, this is where static calls are useful, because we need to call the method collect from the NonFungiblePositionManager contract, BUT that method is a write method, not a read method. We don't want to execute it, but we want to get its return values, so we need to perform a static call.

// Get unclaimed fees
const {
      amount0: unclaimedFee0Wei,
      amount1: unclaimedFee1Wei,
} = await positionManagerContract.callStatic.collect({
      tokenId: nftId,
      recipient: nftOwner,
      amount0Max: MAX_UINT128,
      amount1Max: MAX_UINT128,
}, {from: nftOwner, blockTag: defaultBlock});
const unclaimedFee0 = unclaimedFee0Wei / (10**decimals0);
const unclaimedFee1 = unclaimedFee1Wei / (10**decimals1);

And voilà! If this tutorial helped you, you can send me a little tip at benjaminazoulay.eth