Claiming Fees

Claim the swap fees from an existing liquidity position.

See Get user positions for details how to retrieve all positions of a user including fees earned if any.


Function name: collect

Recommended gas: 300,000 gwei (~ $0.026 USD)

Solidity Interface & Function Body
//INonfungiblePositionManager.sol

struct CollectParams {
  uint256 tokenSN;
  address recipient;
  uint128 amount0Max;
  uint128 amount1Max;
}

/// @notice Collects up to a maximum amount of fees owed to a specific position to the recipient
/// @param params tokenSN The serial number of the NFT for which tokens are being collected,
/// recipient The account that should receive the tokens,
/// amount0Max The maximum amount of token0 to collect,
/// amount1Max The maximum amount of token1 to collect
/// @return amount0 The amount of fees collected in token0
/// @return amount1 The amount of fees collected in token1
function collect(CollectParams calldata params) external payable 
  returns (uint256 amount0, uint256 amount1);
//NonfungiblePositionManager.sol

/// @inheritdoc INonfungiblePositionManager
function collect(CollectParams calldata params)
  external
  payable
  override
  isAuthorizedForToken(params.tokenSN)
  returns (uint256 amount0, uint256 amount1)
{
  require(params.amount0Max > 0 || params.amount1Max > 0);
  // allow collecting to the nft position manager address with address 0
  address recipient = params.recipient == address(0) ? address(this) : params.recipient;

  Position storage position = _positions[params.tokenSN];

  PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId];

  IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));

  (uint128 tokensOwed0, uint128 tokensOwed1) = (position.tokensOwed0, position.tokensOwed1);

  // trigger an update of the position fees owed and fee growth snapshots if it has any liquidity
  if (position.liquidity > 0) {
    pool.burn(position.tickLower, position.tickUpper, 0);
    (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(
      PositionKey.compute(address(this), position.tickLower, position.tickUpper)
    );

    tokensOwed0 += uint128(
      FullMath.mulDiv(
        feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
        position.liquidity,
        FixedPoint128.Q128
      )
    );
    tokensOwed1 += uint128(
      FullMath.mulDiv(
        feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
        position.liquidity,
        FixedPoint128.Q128
      )
    );

    position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
    position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
  }

  // compute the arguments to give to the pool#collect method
  (uint128 amount0Collect, uint128 amount1Collect) = (
    params.amount0Max > tokensOwed0 ? tokensOwed0 : params.amount0Max,
    params.amount1Max > tokensOwed1 ? tokensOwed1 : params.amount1Max
  );

  // the actual amounts collected are returned
  (amount0, amount1) = pool.collect(
    recipient,
    position.tickLower,
    position.tickUpper,
    amount0Collect,
    amount1Collect
  );

  // sometimes there will be a few less wei than expected due to rounding down in core, but we just subtract the full amount expected
  // instead of the actual amount so we can burn the token
  (position.tokensOwed0, position.tokensOwed1) = (tokensOwed0 - amount0Collect, tokensOwed1 - amount1Collect);

  emit Collect(params.tokenSN, recipient, amount0Collect, amount1Collect);
}

Code Overview

The following code demonstrates how to claim all swap fees from a pool.

When claiming fees from a pool that involves HBAR, include unwrapWHBAR in your multicall to convert the Wrapped HBAR (WHBAR) output token back into the native HBAR cryptocurrency.

Resources:

import * as ethers from 'ethers'; //V6
import { Pool, Position, nearestUsableTick, priceToClosestTick } from '@uniswap/v3-sdk';
import { Fraction, Percent, Token, Price } from '@uniswap/sdk-core';
import { ContractExecuteTransaction, .. } from '@hashgraph/sdk';

//Set one of Hedera's JSON RPC Relay as the provider
const provider = new ethers.JsonRpcProvider(hederaJsonRelayUrl, '', {
  batchMaxCount: 1, //workaround for V6
});

//Load the ABI data for NonfungiblePositionManager
const nftManagerInterfaces = new ethers.Interface(nftManagerAbi);

//get max possible value for amount0Max and amount1Max
const MAX_UINT128 = new BigNumber(2).pow(128).minus(1).toFixed(0);

//CollectParams struct
const params = {
  tokenSN: tokenSN,
  recipient: recipientAddress,    
  amount0Max: MAX_UINT128, //collect max fees
  amount1Max: MAX_UINT128, //collect max fees
};

//Construct encoded data for each function
const collectEncoded = nftManagerInterfaces.encodeFunctionData('collect', [params]);  

//Not needed if HBAR isn't include in the pool
const unwrapWHBAREncoded = nftManagerInterfaces.encodeFunctionData('unwrapWHBAR', [0, recipientAddress]);

//Build encoded data for the multicall
const encodedData = nftManagerInterfaces.encodeFunctionData('multicall', 
  [[collectEncoded, unwrapWHBAREncoded]]);  
const encodedDataAsUint8Array = hexToUint8Array(encodedData.substring(2));

//Execute the paid contract call
const response = await new ContractExecuteTransaction()
  .setContractId(nftManagerContractId)
  .setGas(gasGwei)
  .setFunctionParameters(encodedDataAsUint8Array)
  .execute(client);

//Fetch the result
const record = await response.getRecord(client);    
const result = record.contractFunctionResult!;
const results = nftManagerInterfaces.decodeFunctionResult('multicall', result.bytes)[0];
const collectResult = nftManagerInterfaces.decodeFunctionResult('collect', results[0]);

//Retrieve the collected amounts for informative purposes
const amount0 = BigNumber(collectResult.amount0);
const amount1 = BigNumber(collectResult.amount1);

Last updated