Skip to main content

Documentation

Introduction

Structured reference
TOC available

Calculate LP fees

Introduction

With hooks introduced in v4, Dynamic Fees are possible and allow v4 pools to use flexible, responsive fee structures instead of fixed fee tiers.

In this v4 integration stack, swap fees accrue directly to liquidity positions and can be further handled by hooks at afterModifyLiquidity. As a result, feesAccrued should not be emitted in the ModifyLiquidity event. This saves gas and avoids confusion because the final fee amount can change as a result of hook execution.

// Modify liquidity in v4
function modifyLiquidity(PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params, bytes calldata hookData)
// ...
returns (BalanceDelta callerDelta, BalanceDelta feesAccrued)
{
PoolId id = key.toId();
{
// ... (other code)
BalanceDelta principalDelta;
(principalDelta, feesAccrued) = pool.modifyLiquidity(
// ... (the ModifyLiquidityParams)
);
callerDelta = principalDelta + feesAccrued;
}

// event is emitted before the afterModifyLiquidity call to ensure events are always emitted in order
emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper, params.liquidityDelta, params.salt);

BalanceDelta hookDelta;
(callerDelta, hookDelta) = key.hooks.afterModifyLiquidity(key, params, callerDelta, feesAccrued, hookData);
// ... (other code)
}

In this guide we will go through how you can calculate fees earned for a LP position in v4 specifically the uncollected fees and total lifetime fees.

Contract Setup

Import the necessary dependencies and prepare the function signature for getUncollectedFees and getLifetimeFees.

pragma solidity ^0.8.24;

import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol";
import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol";
import {Position} from "@uniswap/v4-core/src/libraries/Position.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";

import {IPositionManager} from "src/interfaces/IPositionManager.sol";
import {PositionConfig} from "src/types/PositionConfig.sol";

contract CalculateFeeExample {
using SafeCast for uint256;
using StateLibrary for IPoolManager;

IPositionManager posm = IPositionManager(<POSM_ADDRESS>);
IPoolManager manager = IPoolManager(<POOL_MANAGER_ADDRESS>);

function getUncollectedFees(PositionConfig memory config, uint256 tokenId)
external
view
returns (BalanceDelta uncollectedFees) {}

function getLifetimeFees(PositionConfig memory config, uint256 tokenId)
external
view
returns (BalanceDelta lifetimeFees) {}
}

Calculate uncollected fees for a LP position

In getUncollectedFees(PositionConfig config, uint256 tokenId):

  1. Get the last recorded fee growth in currency0 and currency1 per unit of liquidity from tickLower to tickUpper
PoolId poolId = config.poolKey.toId();

// getPositionInfo(poolId, owner, tL, tU, salt)
// owner is the position manager, salt is the tokenId
(uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) =
manager.getPositionInfo(poolId, address(posm), config.tickLower, config.tickUpper, bytes32(tokenId));
  1. Get the all-time fee growth in currency0 and currency1 per unit of liquidity from tickLower to tickUpper
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = 
manager.getFeeGrowthInside(poolId, config.tickLower, config.tickUpper);
  1. Compare the all-time feeGrowthInside from step 2 against the last recorded feeGrowthInside from step 1, and convert it to token amount
uint128 tokenAmount =
(FullMath.mulDiv(feeGrowthInsideX128 - feeGrowthInsideLastX128, liquidity, FixedPoint128.Q128)).toUint128();

Calculate lifetime fees earned for a LP range

Create a new function getLifetimeFee(PositionConfig config, uint256 tokenId):

  1. Get the all-time fee growth in currency0 and currency1 per unit of liquidity from tickLower to tickUpper
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = 
manager.getFeeGrowthInside(poolId, config.tickLower, config.tickUpper);
  1. Convert it to token amount
uint128 tokenAmount =
(FullMath.mulDiv(feeGrowthInsideX128, liquidity, FixedPoint128.Q128)).toUint128();

Full Contract

pragma solidity ^0.8.24;

import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol";
import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol";
import {Position} from "@uniswap/v4-core/src/libraries/Position.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";

import {IPositionManager} from "src/interfaces/IPositionManager.sol";
import {PositionConfig} from "src/types/PositionConfig.sol";

contract CalculateFeeExample {
using SafeCast for uint256;
using StateLibrary for IPoolManager;

IPositionManager posm = IPositionManager(<POSM_ADDRESS>);
IPoolManager manager = IPoolManager(<POOL_MANAGER_ADDRESS>);

function getUncollectedFees(PositionConfig memory config, uint256 tokenId)
external
view
returns (BalanceDelta uncollectedFees) {

PoolId poolId = config.poolKey.toId();

(uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) =
manager.getPositionInfo(poolId, address(posm), config.tickLower, config.tickUpper, bytes32(tokenId));
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
manager.getFeeGrowthInside(poolId, config.tickLower, config.tickUpper);

uncollectedFees = _convertFeesToBalanceDelta(
feeGrowthInside0X128 - feeGrowthInside0LastX128, feeGrowthInside1X128 - feeGrowthInside1LastX128, liquidity);
}

function getLifetimeFees(PositionConfig memory config, uint256 tokenId)
external
view
returns (BalanceDelta lifetimeFees) {

PoolId poolId = config.poolKey.toId();

(uint128 liquidity,,) = manager.getPositionInfo(poolId, address(posm), config.tickLower, config.tickUpper, bytes32(tokenId));
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
manager.getFeeGrowthInside(poolId, config.tickLower, config.tickUpper);

lifetimeFees = _convertFeesToBalanceDelta(feeGrowthInside0X128, feeGrowthInside1X128, liquidity);
}

function _convertFeesToBalanceDelta(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128, uint256 liquidity)
internal
view
returns (BalanceDelta feesInBalanceDelta) {

uint128 token0Amount = (FullMath.mulDiv(feeGrowthInside0X128, liquidity, FixedPoint128.Q128)).toUint128();
uint128 token1Amount = (FullMath.mulDiv(feeGrowthInside1X128, liquidity, FixedPoint128.Q128)).toUint128();
feesInBalanceDelta = toBalanceDelta(uint256(token0Amount).toInt128(), uint256(token1Amount).toInt128());
}
}