//INonfungiblePositionManager.solstructDecreaseLiquidityParams {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 owedfunctiondecreaseLiquidity(DecreaseLiquidityParamscalldata params)externalpayablereturns (uint256 amount0,uint256 amount1);
//NonfungiblePositionManager.sol/// @inheritdoc INonfungiblePositionManagerfunctiondecreaseLiquidity(DecreaseLiquidityParamscalldata params)externalpayableoverrideisAuthorizedForToken(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;emitDecreaseLiquidity(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.
import*as ethers from'ethers'; //V6import { 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 providerconstprovider=newethers.JsonRpcProvider(hederaJsonRelayUrl,'', { batchMaxCount:1,//workaround for V6});//Load the ABI data for UniswapV3PoolconstpoolInterfaces=newethers.Interface(poolAbi);//Load the ABI data for NonfungiblePositionManagerconstnftManagerInterfaces=newethers.Interface(nftManagerAbi);//Construct the pool contractconstpoolContract=newethers.Contract(poolEvmAddress,poolInterfaces.fragments, provider);//Construct the NFT Manager contractconstnftManagerContract=newethers.Contract(nftManagerEvmAddress,nftManagerInterfaces.fragments, provider);//Get current position data for the given NFT token serial numberconstlp=awaitnftManagerContract.positions(tokenSN);consttoken0Address=lp.token0;consttoken1Address=lp.token1;constfeeTier=Number(lp.fee);consttickLower=Number(lp.tickLower);consttickUpper=Number(lp.tickUpper);constliquidity=lp.liquidity.toString();//Get current slot0 and liquidity data from the poolconst [slot0,poolLiquidity] =awaitPromise.all([poolContract.slot0(),poolContract.liquidity()]);//Construct the tokens//For Hedera chain id, see https://chainlist.org/?testnets=true&search=Hederaconsttoken0=newToken(hederaChainId, token0Address, token0Decimals);consttoken1=newToken(hederaChainId, token1Address, token1Decimals);//Construct the pool using the latest dataconstpool=newPool( token0, token1, feeTier,slot0.sqrtPriceX96.toString(),poolLiquidity.toString(),Number(slot0.tick));//Construct a position from liquidity and rangeconstposition=newPosition({ pool: pool, tickUpper: tickUpper, tickLower: tickLower, liquidity: liquidity});//Calculate the maximum amounts factoring in the price slippage % and rangeconstpriceSlippagePercent=newPercent(1,100); //1% price slippageconstburnAmounts=position.burnAmountsWithSlippage(priceSlippagePercent);constamount0Min=burnAmounts.amount0.toString();constamount1Min=burnAmounts.amount1.toString();//DecreaseLiquidityParams structconstparams= { 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 amount1MaxconstMAX_UINT128=newBigNumber(2).pow(128).minus(1).toFixed(0);//CollectParams structconstcollectParams= { 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.constdecreaseEncoded=nftManagerInterfaces.encodeFunctionData('decreaseLiquidity', [params]); constcollectEncoded=nftManagerInterfaces.encodeFunctionData('collect', [collectParams]); //The unwrapWHBAR is only needed when removing liquidity that includes HBARconstunwrapWHBAREncoded=nftManagerInterfaces.encodeFunctionData('unwrapWHBAR', [0, recipientAddress]);//Build encoded data for the multicallconstencodedData=nftManagerInterfaces.encodeFunctionData('multicall', [[decreaseEncoded, collectEncoded, unwrapWHBAREncoded]]); constencodedDataAsUint8Array=hexToUint8Array(encodedData.substring(2));//Execute the contract callconstresponse=awaitnewContractExecuteTransaction().setContractId(nftManagerContractId).setGas(gasGwei).setFunctionParameters(encodedDataAsUint8Array).execute(client);//Fetch the resultconstrecord=awaitresponse.getRecord(client); constresult=record.contractFunctionResult!;constresults=nftManagerInterfaces.decodeFunctionResult('multicall',result.bytes)[0];constcollectResult=nftManagerInterfaces.decodeFunctionResult('collect', results[1]);//Retrieve the amounts removed for informative purposesconstremovedAmount0=BigNumber(collectResult.amount0);constremovedAmount1=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.
//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.constdecreaseEncoded=nftManagerInterfaces.encodeFunctionData('decreaseLiquidity', [params]); constcollectEncoded=nftManagerInterfaces.encodeFunctionData('collect', [collectParams]); constburnEncoded=nftManagerInterfaces.encodeFunctionData('burn', [tokenSN]);//The unwrapWHBAR is only needed when removing liquidity that includes HBARconstunwrapWHBAREncoded=nftManagerInterfaces.encodeFunctionData('unwrapWHBAR', [0, recipientAddress]);//Build encoded data for the multicallconstencodedData=nftManagerInterfaces.encodeFunctionData('multicall', [[decreaseEncoded, collectEncoded, unwrapWHBAREncoded, burnEncoded]]); constencodedDataAsUint8Array=hexToUint8Array(encodedData.substring(2));//Give NFT spender allowance to NFT Manager contractconstnftId=newNftId(lpTokenId, tokenSN);constapproveResult=awaitnewAccountAllowanceApproveTransaction().approveTokenNftAllowance(nftId, ownerId, nftManagerContractId).execute(client);constallowanceReceipt=awaitapproveResult.getReceipt(client);console.log(`NFT allowance status: ${allowanceReceipt.status}`);//Execute the contract callconstresponse=awaitnewContractExecuteTransaction().setContractId(nftManagerContractId).setGas(gasGwei).setFunctionParameters(encodedDataAsUint8Array).execute(client);
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