Minting a Position

Introduction

This guide will cover how to create (or mint) a liquidity position on the Uniswap V3 protocol. It is based on the minting a position code example (opens in a new tab), found in the Uniswap code examples repository (opens in a new tab). To run this example, check out the examples's README (opens in a new tab) and follow the setup instructions.

ℹ️

If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our background page!

In the Uniswap V3 protocol, liquidity positions are represented using non-fungible tokens. In this guide we will use the NonfungiblePositionManager class to help us mint a liquidity position for the USDC - DAI pair. The inputs to our guide are the two tokens that we are pooling for, the amount of each token we are pooling for and the Pool fee.

The guide will cover:

  1. Giving approval to transfer our tokens
  2. Creating an instance of a Pool
  3. Calculating our Position from our input tokens
  4. Configuring and executing our minting transaction

At the end of the guide, given the inputs above, we should be able to mint a liquidity position with the press of a button and view the position on the UI of the web application.

For this guide, the following Uniswap packages are used:

The core code of this guide can be found in mintPosition() (opens in a new tab)

Giving approval to transfer our tokens

The first step is to give approval to the protocol's NonfungiblePositionManager to transfer our tokens:

  const tokenInApproval = await getTokenTransferApproval(
    CurrentConfig.tokens.token0
  )
  const tokenOutApproval = await getTokenTransferApproval(
    CurrentConfig.tokens.token1
  )

The logic to achieve that is wrapped in the getTokenTransferApprovals function. In short, since both USDC and DAI are ERC20 tokens, we setup a reference to their smart contracts and call the approve function:

    const tokenContract = new ethers.Contract(
      token.address,
      ERC20_ABI,
      provider
    )
 
    const transaction = await tokenContract.populateTransaction.approve(
      NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS,
      TOKEN_AMOUNT_TO_APPROVE_FOR_TRANSFER
    )

Creating an instance of a Pool

Having approved the transfer of our tokens, we now need to get data about the pool for which we will provide liquidity, in order to instantiate a Pool class.

To start, we compute our Pool's address by using a helper function and passing in the unique identifiers of a Pool - the two tokens and the Pool fee. The fee input parameter represents the swap fee that is distributed to all in range liquidity at the time of the swap:

  const currentPoolAddress = computePoolAddress({
    factoryAddress: POOL_FACTORY_CONTRACT_ADDRESS,
    tokenA: CurrentConfig.tokens.token0,
    tokenB: CurrentConfig.tokens.token1,
    fee: CurrentConfig.tokens.poolFee,
  })

Then, we get the Pool's data by creating a reference to the Pool's smart contract and accessing its methods:

  const poolContract = new ethers.Contract(
    currentPoolAddress,
    IUniswapV3PoolABI.abi,
    provider
  )
 
  const [token0, token1, fee, tickSpacing, liquidity, slot0] =
    await Promise.all([
      poolContract.token0(),
      poolContract.token1(),
      poolContract.fee(),
      poolContract.tickSpacing(),
      poolContract.liquidity(),
      poolContract.slot0(),
    ])

Having collected the required data, we can now create an instance of the Pool class:

  const configuredPool = new Pool(
    token0Amount.currency,
    token1Amount.currency,
    poolInfo.fee,
    poolInfo.sqrtPriceX96.toString(),
    poolInfo.liquidity.toString(),
    poolInfo.tick
  )

Calculating our Position from our input tokens

Having created the instance of the Pool class, we can now use that to create an instance of a Position class, which represents the price range for a specific pool that LPs choose to provide in:

  return Position.fromAmounts({
    pool: configuredPool,
    tickLower:
      nearestUsableTick(poolInfo.tick, poolInfo.tickSpacing) -
      poolInfo.tickSpacing * 2,
    tickUpper:
      nearestUsableTick(poolInfo.tick, poolInfo.tickSpacing) +
      poolInfo.tickSpacing * 2,
    amount0: token0Amount.quotient,
    amount1: token1Amount.quotient,
    useFullPrecision: true,
  })

We use the fromAmounts static function of the Position class to create an instance of it, which uses the following parameters:

  • The tickLower and tickUpper parameters specify the price range at which to provide liquidity. This example calls nearestUsableTick to get the current useable tick and adjust the lower parameter to be below it by two tickSpacing and the upper to be above it by two tickSpacing. This guarantees that the provided liquidity is "in range", meaning it will be earning fees upon minting this position
  • amount0 and amount1 define the maximum amount of currency the liquidity position can use. In this example, we supply these from our configuration parameters.

Given those parameters, fromAmounts will attempt to calculate the maximum amount of liquidity we can supply.

Configuring and executing our minting transaction

The Position instance is then passed as input to the NonfungiblePositionManager's addCallParameters function. The function also requires an AddLiquidityOptions (opens in a new tab) object as its second parameter. This is either of type MintOptions (opens in a new tab) for minting a new position or IncreaseOptions (opens in a new tab) for adding liquidity to an existing position. For this example, we're using a MintOptions to create our position.

  const mintOptions: MintOptions = {
    recipient: address,
    deadline: Math.floor(Date.now() / 1000) + 60 * 20,
    slippageTolerance: new Percent(50, 10_000),
  }
 
  // get calldata for minting a position
  const { calldata, value } = NonfungiblePositionManager.addCallParameters(
    positionToMint,
    mintOptions
  )

The function returns the calldata as well as the value required to execute the transaction:

  const transaction = {
    data: calldata,
    to: NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS,
    value: value,
    from: address,
    maxFeePerGas: MAX_FEE_PER_GAS,
    maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS,
  }
 
  return sendTransaction(transaction)

The effect of the transaction is to mint a new Position NFT. We should see a new position with liquidity in our list of positions.

Next Steps

Once you have minted a position, our next guide (Adding and Removing Liquidity) will demonstrate how you can add and remove liquidity from that minted position!