Decreasing Liquidity

Decrease liquidity or completely exit an existing liquidity position.


Decrease Liquidity

Function name: decreaseLiquidity

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

Struct Parameter Name
Description

uint256 tokenSN

The serial number of the token for which liquidity is being increased

uint128 liquidity

Liquidity amount to remove

uint256 amount0Min

The minimum amount for the first token in its smallest unit

uint256 amount1Min

The minimum amount for the second token in its smallest unit

uint256 deadline

Deadline in Unix seconds

Solidity Interface & Function Body
//INonfungiblePositionManager.sol

struct DecreaseLiquidityParams {
  uint256 tokenSN;
  uint128 liquidity;
  uint256 amount0Min;
  uint256 amount1Min;
  uint256 deadline;
}

/// @notice Decreases the amount of liquidity in a position and accounts it to the position
/// @param params tokenSN The serial number of the token for which liquidity is being decreased,
/// amount The amount by which liquidity will be decreased,
/// amount0Min The minimum amount of token0 that should be accounted for the burned liquidity,
/// amount1Min The minimum amount of token1 that should be accounted for the burned liquidity,
/// deadline The time by which the transaction must be included to effect the change
/// @return amount0 The amount of token0 accounted to the position's tokens owed
/// @return amount1 The amount of token1 accounted to the position's tokens owed
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
  external
  payable
  returns (uint256 amount0, uint256 amount1);
//NonfungiblePositionManager.sol

/// @inheritdoc INonfungiblePositionManager
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
  external
  payable
  override
  isAuthorizedForToken(params.tokenSN)
  checkDeadline(params.deadline)
  returns (uint256 amount0, uint256 amount1)
{
  require(params.liquidity > 0);
  Position storage position = _positions[params.tokenSN];

  uint128 positionLiquidity = position.liquidity;
  require(positionLiquidity >= params.liquidity);

  PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId];
  IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
  (amount0, amount1) = pool.burn(position.tickLower, position.tickUpper, params.liquidity);

  require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');

  bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper);
  // this is now updated to the current transaction
  (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);

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

  position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
  position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
  // subtraction is safe because we checked positionLiquidity is gte params.liquidity
  position.liquidity = positionLiquidity - params.liquidity;

  emit DecreaseLiquidity(params.tokenSN, params.liquidity, amount0, amount1);
}

Code Overview

The following code demonstrates how to remove all liquidity from an existing position, collect the swap fees, and return the deposit amounts.

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

Call the collect function after removing the liquidity to withdraw the amounts to the recipient address. It will also collect any swap fees earned in the position.

To burn the NFT after completely exiting the position, include burn in the multi-call. See Burning the NFT.

Hedera's EVM chain ID can be retrieved from https://chainlist.org.

The following code is intended for guidance purposes and does not include checks and safeguards.

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 UniswapV3Pool
const poolInterfaces = new ethers.Interface(poolAbi);

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

//Construct the pool contract
const poolContract = new ethers.Contract(poolEvmAddress, 
  poolInterfaces.fragments, provider);

//Construct the NFT Manager contract
const nftManagerContract = new ethers.Contract(nftManagerEvmAddress, 
  nftManagerInterfaces.fragments, provider);

//Get current position data for the given NFT token serial number
const lp = await nftManagerContract.positions(tokenSN);
const token0Address = lp.token0;
const token1Address = lp.token1;
const feeTier = Number(lp.fee);
const tickLower = Number(lp.tickLower);
const tickUpper = Number(lp.tickUpper);
const liquidity = lp.liquidity.toString();

//Get current slot0 and liquidity data from the pool
const [slot0, poolLiquidity] = await Promise.all([
  poolContract.slot0(),
  poolContract.liquidity()
]);

//Construct the tokens
//For Hedera chain id, see https://chainlist.org/?testnets=true&search=Hedera
const token0 = new Token(hederaChainId, token0Address, token0Decimals);
const token1 = new Token(hederaChainId, token1Address, token1Decimals);

//Construct the pool using the latest data
const pool = new Pool(
  token0, token1, 
  feeTier, slot0.sqrtPriceX96.toString(),
  poolLiquidity.toString(), Number(slot0.tick)
);

//Construct a position from liquidity and range
const position = new Position({
  pool: pool,
  tickUpper: tickUpper,
  tickLower: tickLower,
  liquidity: liquidity
});

//Calculate the maximum amounts factoring in the price slippage % and range
const priceSlippagePercent = new Percent(1, 100); //1% price slippage
const burnAmounts = position.burnAmountsWithSlippage(priceSlippagePercent);
const amount0Min = burnAmounts.amount0.toString();
const amount1Min = burnAmounts.amount1.toString();

//DecreaseLiquidityParams struct
const params = {
  tokenSN: tokenSN,
  liquidity: liquidity, //liquidity amount to remove
  amount0Min: amount0Min, //in smallest unit
  amount1Min: amount1Min, //in smallest unit
  deadline: deadline, //Unix seconds
};

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

//CollectParams struct
const collectParams = {
  tokenSN: tokenSN,
  recipient: recipientAddress, //0x..   
  amount0Max: MAX_UINT128, //collect max fees and amount
  amount1Max: MAX_UINT128, //collect max fees and amount
};

//Construct encoded data for each function
//The unwrapWHBAR is needed when collecting the HBAR swap fees
//Optionally include 'collect' here to collect fees.
//Optionally include 'burn' to burn the NFT if all liquidity is removed.
const decreaseEncoded = nftManagerInterfaces.encodeFunctionData('decreaseLiquidity', [params]);  
const collectEncoded = nftManagerInterfaces.encodeFunctionData('collect', [collectParams]);  

//The unwrapWHBAR is only needed when removing liquidity that includes HBAR
const unwrapWHBAREncoded = nftManagerInterfaces.encodeFunctionData('unwrapWHBAR', [0, recipientAddress]);

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

//Execute the 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[1]);

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

Burning the NFT

After completely exiting a position, you may burn the NFT position if it is no longer needed. The following code demonstrates how to set a spender allowance for the NFT and include the burn function in the multicall.

A spender allowance for the NFT Manager contract must be approved by the token holder to enable the contract to retrieve the NFT to burn it.

Resources:

//Updated code to include burning of the NFT token

//Construct encoded data for each function
//The unwrapWHBAR is needed when collecting the HBAR swap fees
//Optionally include 'collect' here to collect fees.
//Optionally include 'burn' to burn the NFT if all liquidity is removed.
const decreaseEncoded = nftManagerInterfaces.encodeFunctionData('decreaseLiquidity', [params]);  
const collectEncoded = nftManagerInterfaces.encodeFunctionData('collect', [collectParams]);  
const burnEncoded = nftManagerInterfaces.encodeFunctionData('burn', [tokenSN]);

//The unwrapWHBAR is only needed when removing liquidity that includes HBAR
const unwrapWHBAREncoded = nftManagerInterfaces.encodeFunctionData('unwrapWHBAR', [0, recipientAddress]);

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

//Give NFT spender allowance to NFT Manager contract
const nftId = new NftId(lpTokenId, tokenSN);
const approveResult = await new AccountAllowanceApproveTransaction()
  .approveTokenNftAllowance(nftId, ownerId, nftManagerContractId)
  .execute(client);

const allowanceReceipt = await approveResult.getReceipt(client);
console.log(`NFT allowance status: ${allowanceReceipt.status}`);

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

Last updated