7

I'm trying to figure out the uncollected fees for my Uniswap v3 position programmatically. If I interpret the v3 whitepaper correctly it should be liquidity multiplied by the difference of feeGrowthGlobal0X128 and feeGrowthInside0LastX128.

What math shall I actually do to the feeGrowth* values that Uniswap contracts return to me? They are supposed to be Q128.128 values, so my expectation was I need to divide thm by 2**128 in order to get to actual number. Apparently it's wrong as the results I'm getting are far from what I see on the pool page.

For example, feeGrowthGlobal0X128 for the USDC/ETH v3 pool (0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8) is currently 706909750615834684891740623881648

What do I need to do with this number to come to "fees accumulated per unit of virtual liquidity"?

3
  • I think I have figured that out. If the price is inside your position range you need to take feeGrowthGlobal0X128 and subtract the following 3 values: 1) feeGrowthOutside0X128 returned by ticks() function for the lower tick of your position 2) feeGrowthOutside0X128 returned by ticks() function for the upper tick of your position 3) feeGrowthInside0LastX128 returned by positions() for your position You need to divide each fee growth number by 2**128, and multiply the result of the subtraction above by liquidity and then by 1e6 (no idea why TBH). You get the actual fees in token0 earned. Commented Jun 15, 2021 at 17:41
  • I am not able to reproduce... could you write it down as a single equation perhaps :)? It would make a good answer to your question. In my case it just doesn't seem to line up with the right numbers no matter how I try. Commented Jul 6, 2021 at 23:42
  • @vlad.london's method worked for me. The diving by 1e6 part depends on the decimals of the token in question. Many tokens are 18 decimals, so you would do 1e18 then I believe.
    – Travis M
    Commented Jul 15, 2021 at 0:00

5 Answers 5

2

There is a bunch of functions on the smart contract that explains how to use the liquidity field: In Uniswap V3 core repository, in Position.sol line 60:

// calculate accumulated fees
  uint128 tokensOwed0 =
     uint128(
        FullMath.mulDiv(
           feeGrowthInside0X128 - _self.feeGrowthInside0LastX128,
           _self.liquidity,
           FixedPoint128.Q128
        )
     );

This would translate to this in javascript, using bignumber.js

const tokensOwed0 = feeGrowthInside0LastX128
    .times(liquidity)
    .div(new bn(2).pow(128));
const tokensOwed1 = feeGrowthInside1LastX128
    .times(liquidity)
    .div(new bn(2).pow(128));

You may need to divide further by the decimals of the underlying ERC20

2

For those looking for a worked out example. Run this query on thegraph.com:

{
  positions(where: {id:"YOURPOOLIDHERE"}) 
  {
    liquidity
    token0 {symbol decimals}
    pool {feeGrowthGlobal0X128}
    feeGrowthInside0LastX128
    tickLower {feeGrowthOutside0X128} #feeGrowthOutside0X128_lower
    tickUpper {feeGrowthOutside0X128} #feeGrowthOutside0X128_upper
  }
}

Then, as @vlad.london described:

feetoken0 = ((feeGrowthGlobal0X128 - feeGrowthOutside0X128_lower - feeGrowthOutside0X128_upper - feeGrowthInside0LastX128)/(2**128))*liquidity/(1*10**decimals)
5
  • I checked with pool app.uniswap.org/#/pool/86957 and I don't get the correct fees using your formula. Am I missing something?
    – Sydney
    Commented Sep 12, 2021 at 14:17
  • Did you by any chance Add Liquidity after the initial pool creation?
    – user81317
    Commented Sep 15, 2021 at 4:10
  • I don't know because it's not my pool, but I guess the formula should work even if liquidity has been added.
    – Sydney
    Commented Sep 15, 2021 at 6:02
  • formula works out for me! thanks @el3ctric'mp
    – gosuto
    Commented Nov 9, 2021 at 11:52
  • 2
    This formula only works if the current Pool price is within range.
    – James
    Commented Nov 24, 2021 at 17:43
1

maybe this well help this is the math to get the fees for a position with math notated

async function getFees(feeGrowthGlobal0, feeGrowthGlobal1, feeGrowth0Low, feeGrowth0Hi, feeGrowthInside0, feeGrowth1Low, feeGrowth1Hi, feeGrowthInside1, liquidity, decimals0, decimals1, tickLower, tickUpper, tickCurrent){
    // Check out the relevant formulas below which are from Uniswap Whitepaper Section 6.3 and 6.4
    // ๐‘“๐‘Ÿ =๐‘“๐‘”โˆ’๐‘“๐‘(๐‘–๐‘™)โˆ’๐‘“๐‘Ž(๐‘–๐‘ข)
    // ๐‘“๐‘ข =๐‘™ยท(๐‘“๐‘Ÿ(๐‘ก1)โˆ’๐‘“๐‘Ÿ(๐‘ก0))
    // Global fee growth per liquidity '๐‘“๐‘”' for both token 0 and token 1
    let feeGrowthGlobal_0 = toBigNumber(feeGrowthGlobal0);
    let feeGrowthGlobal_1 = toBigNumber(feeGrowthGlobal1);

    // Fee growth outside '๐‘“๐‘œ' of our lower tick for both token 0 and token 1
    let tickLowerFeeGrowthOutside_0 = toBigNumber(feeGrowth0Low);
    let tickLowerFeeGrowthOutside_1 = toBigNumber(feeGrowth1Low);

    // Fee growth outside '๐‘“๐‘œ' of our upper tick for both token 0 and token 1
    let tickUpperFeeGrowthOutside_0 = toBigNumber(feeGrowth0Hi);
    let tickUpperFeeGrowthOutside_1 = toBigNumber(feeGrowth1Hi);

    // These are '๐‘“๐‘(๐‘–๐‘™)' and '๐‘“๐‘Ž(๐‘–๐‘ข)' from the formula
    // for both token 0 and token 1
    let tickLowerFeeGrowthBelow_0 = ZERO;
    let tickLowerFeeGrowthBelow_1 = ZERO;
    let tickUpperFeeGrowthAbove_0 = ZERO;
    let tickUpperFeeGrowthAbove_1 = ZERO;

    // These are the calculations for '๐‘“๐‘Ž(๐‘–)' from the formula
    // for both token 0 and token 1
    if (tickCurrent >= tickUpper){
        tickUpperFeeGrowthAbove_0 = subIn256(feeGrowthGlobal_0, tickUpperFeeGrowthOutside_0);
        tickUpperFeeGrowthAbove_1 = subIn256(feeGrowthGlobal_1, tickUpperFeeGrowthOutside_1);
    }else{
        tickUpperFeeGrowthAbove_0 = tickUpperFeeGrowthOutside_0
        tickUpperFeeGrowthAbove_1 = tickUpperFeeGrowthOutside_1
    }

    // These are the calculations for '๐‘“b(๐‘–)' from the formula
    // for both token 0 and token 1
    if (tickCurrent >= tickLower){
        tickLowerFeeGrowthBelow_0 = tickLowerFeeGrowthOutside_0
        tickLowerFeeGrowthBelow_1 = tickLowerFeeGrowthOutside_1
    }else{
        tickLowerFeeGrowthBelow_0 = subIn256(feeGrowthGlobal_0, tickLowerFeeGrowthOutside_0);
        tickLowerFeeGrowthBelow_1 = subIn256(feeGrowthGlobal_1, tickLowerFeeGrowthOutside_1);
    }

    
    // Calculations for '๐‘“๐‘Ÿ(๐‘ก1)' part of the '๐‘“๐‘ข =๐‘™ยท(๐‘“๐‘Ÿ(๐‘ก1)โˆ’๐‘“๐‘Ÿ(๐‘ก0))' formula
    // for both token 0 and token 1
    let fr_t1_0 = subIn256(subIn256(feeGrowthGlobal_0, tickLowerFeeGrowthBelow_0), tickUpperFeeGrowthAbove_0);
    let fr_t1_1 = subIn256(subIn256(feeGrowthGlobal_1, tickLowerFeeGrowthBelow_1), tickUpperFeeGrowthAbove_1);

    // '๐‘“๐‘Ÿ(๐‘ก0)' part of the '๐‘“๐‘ข =๐‘™ยท(๐‘“๐‘Ÿ(๐‘ก1)โˆ’๐‘“๐‘Ÿ(๐‘ก0))' formula
    // for both token 0 and token 1
    let feeGrowthInsideLast_0 = toBigNumber(feeGrowthInside0);
    let feeGrowthInsideLast_1 = toBigNumber(feeGrowthInside1);

    // The final calculations for the '๐‘“๐‘ข =๐‘™ยท(๐‘“๐‘Ÿ(๐‘ก1)โˆ’๐‘“๐‘Ÿ(๐‘ก0))' uncollected fees formula
    // for both token 0 and token 1 since we now know everything that is needed to compute it
    let uncollectedFees_0 = (liquidity * subIn256(fr_t1_0, feeGrowthInsideLast_0)) / Q128;
    let uncollectedFees_1 = (liquidity * subIn256(fr_t1_1, feeGrowthInsideLast_1)) / Q128;
    console.log("Amount fees token 0 wei: "+Math.floor(uncollectedFees_0));
    console.log("Amount fees token 1 wei: "+Math.floor(uncollectedFees_1));
    
    // Decimal adjustment to get final results
    let uncollectedFeesAdjusted_0 = (uncollectedFees_0 / toBigNumber(10**decimals0)).toFixed(decimals0);
    let uncollectedFeesAdjusted_1 = (uncollectedFees_1 / toBigNumber(10**decimals1)).toFixed(decimals1);
    console.log("Amount fees token 0 Human format: "+uncollectedFeesAdjusted_0);
    console.log("Amount fees token 1 Human format: "+uncollectedFeesAdjusted_1);
}

here is the full code to with calls to get data

import { JSBI } from "@uniswap/sdk";
import { ethers } from 'ethers'
import * as fs from 'fs';


    // ERC20 json abi file
let ERC20Abi = fs.readFileSync('<path>/ERC20.json');
const ERC20 = JSON.parse(ERC20Abi);

    // V3 pool abi json file
let pool = fs.readFileSync('<path>/V3PairAbi.json');
const IUniswapV3PoolABI = JSON.parse(pool);

    // V3 factory abi json
let facto = fs.readFileSync('<path>/V3factory.json');
const IUniswapV3FactoryABI = JSON.parse(facto);

    // V3 NFT manager abi
let NFT = fs.readFileSync('<path>/UniV3NFT.json');
const IUniswapV3NFTmanagerABI = JSON.parse(NFT);


const provider = new ethers.providers.JsonRpcProvider("https://eth-mainnet.alchemyapi.io/v2/<API_Key>")


    // V3 standard addresses (different for celo)
const factory = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
const NFTmanager = "0xC36442b4a4522E871399CD717aBDD847Ab11FE88";



async function getData(tokenID){
    var FactoryContract = new ethers.Contract(factory, IUniswapV3FactoryABI, provider);

    var NFTContract =  new ethers.Contract(NFTmanager, IUniswapV3NFTmanagerABI, provider);
    var position = await NFTContract.positions(tokenID);
    
    var token0contract =  new ethers.Contract(position.token0, ERC20, provider);
    var token1contract =  new ethers.Contract(position.token1, ERC20, provider);
    var Decimal0 = await token0contract.decimals();
    var Decimal1 = await token1contract.decimals();
    
    var token0sym = await token0contract.symbol();
    var token1sym = await token1contract.symbol();
    
    var V3pool = await FactoryContract.getPool(position.token0, position.token1, position.fee);
    var poolContract = new ethers.Contract(V3pool, IUniswapV3PoolABI, provider);

    var slot0 = await poolContract.slot0();
    var tickLow = await poolContract.ticks(position.tickLower.toString());
    var tickHi = await poolContract.ticks(position.tickUpper.toString());
    
    var feeGrowthGlobal0 = await poolContract.feeGrowthGlobal0X128();
    var feeGrowthGlobal1 = await poolContract.feeGrowthGlobal1X128();
    
    var pairName = token0sym +"/"+ token1sym;
    
    var PoolInfo = {
    "Pair": pairName, 
    "Decimal0": Decimal0, 
    "Decimal1": Decimal1, 
    "tickCurrent": slot0.tick, 
    "tickLow": position.tickLower, 
    "tickHigh": position.tickUpper, 
    "liquidity": position.liquidity.toString(), 
    "feeGrowth0Low": tickLow.feeGrowthOutside0X128.toString(),  
    "feeGrowth0Hi": tickHi.feeGrowthOutside0X128.toString(), 
    "feeGrowth1Low": tickLow.feeGrowthOutside1X128.toString(),  
    "feeGrowth1Hi": tickHi.feeGrowthOutside1X128.toString(),
    "feeGrowthInside0LastX128": position.feeGrowthInside0LastX128.toString(),
    "feeGrowthInside1LastX128": position.feeGrowthInside1LastX128.toString(),
    "feeGrowthGlobal0X128": feeGrowthGlobal0.toString(), 
    "feeGrowthGlobal1X128": feeGrowthGlobal1.toString()}
    
    return PoolInfo
}


const ZERO = JSBI.BigInt(0);
const Q128 = JSBI.exponentiate(JSBI.BigInt(2), JSBI.BigInt(128));
const Q256 = JSBI.exponentiate(JSBI.BigInt(2), JSBI.BigInt(256));


function toBigNumber(numstr){
  let bi = numstr;
  if (typeof sqrtRatio !== 'bigint') {
    bi = JSBI.BigInt(numstr);
  }
  return bi;
};


function subIn256(x, y){
  const difference = JSBI.subtract(x, y)

  if (JSBI.lessThan(difference, ZERO)) {
    return JSBI.add(Q256, difference)
  } else {
    return difference
  }
}


async function getFees(feeGrowthGlobal0, feeGrowthGlobal1, feeGrowth0Low, feeGrowth0Hi, feeGrowthInside0, feeGrowth1Low, feeGrowth1Hi, feeGrowthInside1, liquidity, decimals0, decimals1, tickLower, tickUpper, tickCurrent){
    
    let feeGrowthGlobal_0 = toBigNumber(feeGrowthGlobal0);
    let feeGrowthGlobal_1 = toBigNumber(feeGrowthGlobal1);

    
    let tickLowerFeeGrowthOutside_0 = toBigNumber(feeGrowth0Low);
    let tickLowerFeeGrowthOutside_1 = toBigNumber(feeGrowth1Low);

    
    let tickUpperFeeGrowthOutside_0 = toBigNumber(feeGrowth0Hi);
    let tickUpperFeeGrowthOutside_1 = toBigNumber(feeGrowth1Hi);

    
    let tickLowerFeeGrowthBelow_0 = ZERO;
    let tickLowerFeeGrowthBelow_1 = ZERO;
    let tickUpperFeeGrowthAbove_0 = ZERO;
    let tickUpperFeeGrowthAbove_1 = ZERO;

    
    if (tickCurrent >= tickUpper){
        tickUpperFeeGrowthAbove_0 = subIn256(feeGrowthGlobal_0, tickUpperFeeGrowthOutside_0);
        tickUpperFeeGrowthAbove_1 = subIn256(feeGrowthGlobal_1, tickUpperFeeGrowthOutside_1);
    }else{
        tickUpperFeeGrowthAbove_0 = tickUpperFeeGrowthOutside_0
        tickUpperFeeGrowthAbove_1 = tickUpperFeeGrowthOutside_1
    }

    
    if (tickCurrent >= tickLower){
        tickLowerFeeGrowthBelow_0 = tickLowerFeeGrowthOutside_0
        tickLowerFeeGrowthBelow_1 = tickLowerFeeGrowthOutside_1
    }else{
        tickLowerFeeGrowthBelow_0 = subIn256(feeGrowthGlobal_0, tickLowerFeeGrowthOutside_0);
        tickLowerFeeGrowthBelow_1 = subIn256(feeGrowthGlobal_1, tickLowerFeeGrowthOutside_1);
    }

        
    let fr_t1_0 = subIn256(subIn256(feeGrowthGlobal_0, tickLowerFeeGrowthBelow_0), tickUpperFeeGrowthAbove_0);
    let fr_t1_1 = subIn256(subIn256(feeGrowthGlobal_1, tickLowerFeeGrowthBelow_1), tickUpperFeeGrowthAbove_1);

    
    let feeGrowthInsideLast_0 = toBigNumber(feeGrowthInside0);
    let feeGrowthInsideLast_1 = toBigNumber(feeGrowthInside1);

    
    let uncollectedFees_0 = (liquidity * subIn256(fr_t1_0, feeGrowthInsideLast_0)) / Q128;
    let uncollectedFees_1 = (liquidity * subIn256(fr_t1_1, feeGrowthInsideLast_1)) / Q128;
    console.log("Amount fees token 0 in lowest decimal: "+Math.floor(uncollectedFees_0));
    console.log("Amount fees token 1 in lowest decimal: "+Math.floor(uncollectedFees_1));
    
    
    let uncollectedFeesAdjusted_0 = (uncollectedFees_0 / toBigNumber(10**decimals0)).toFixed(decimals0);
    let uncollectedFeesAdjusted_1 = (uncollectedFees_1 / toBigNumber(10**decimals1)).toFixed(decimals1);
    console.log("Amount fees token 0 Human format: "+uncollectedFeesAdjusted_0);
    console.log("Amount fees token 1 Human format: "+uncollectedFeesAdjusted_1);
}


async function Start(positionID){
    var PoolInfo = await getData(positionID);

    
    var Fees = await getFees(PoolInfo.feeGrowthGlobal0X128, PoolInfo.feeGrowthGlobal1X128, PoolInfo.feeGrowth0Low, PoolInfo.feeGrowth0Hi, PoolInfo.feeGrowthInside0LastX128, PoolInfo.feeGrowth1Low, PoolInfo.feeGrowth1Hi, PoolInfo.feeGrowthInside1LastX128, PoolInfo.liquidity, PoolInfo.Decimal0, PoolInfo.Decimal1, PoolInfo.tickLow, PoolInfo.tickHigh, PoolInfo.tickCurrent);

}


Start(5);
0

The formula for the case when the current tick is out of the range of the position

  • When current tick is higher than tick upper
token0fee = ((feeGrowthOutside0X128_upper - feeGrowthOutside0X128_lower
- feeGrowthInside0LastX128 ) / (2**128))*position.liquidity)
  • When current tick is lower than tick upper
token0fee = ((feeGrowthOutside0X128_lower - feeGrowthOutside0X128_upper
- feeGrowthInside0LastX128 ) / (2**128))*position.liquidity)
0

@Crypto_Rachel your code works only when we have defined min and max ticks. When we have position with whole range min tick on NFT image is -887220 but tickLower from positions returns value like 115792089237316195423570985008687907853269984665640564039457584007913111975500.

min tick -887220

tickLower 115792089237316195423570985008687907853269984665640564039457584007913111975500

tickLower (binary) 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110111100100111011001001100

max tick 887220

tickUpper 887220

tickUpper (binary) 11011000100110110100

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