Liquidity Positions
Introduction
This guide will introduce us to liquidity positions in Uniswap V3 and present the v3-sdk
classes and Contracts used to interact with the protocol.
The concepts and code snippets showcased here can be found across the Pooling Liquidity examples in the Uniswap code examples repository.
In this guide, we will take a look at the Position and NonfungiblePositionManager classes, as well as the NonfungiblePositionManager Contract.
At the end of the guide, we should be familiar with the most important classes used to interact with liquidity positions. We should also understand how to fetch positions from the NonfungiblePositionManager Contract.
For this guide, the following Uniswap packages are used:
The code mentioned in this guide can be found across the minting Position, collecting Fees, modifying positions and swap and add liquidity examples.
Prerequisites
To understand what Positions are, we need to understand some underlying concepts of the Uniswap protocol.
Consider checking out the Concepts section as well as the Uniswap Book.
Concentrated liquidity
Uniswap V3 Pools use concentrated liquidity to allow a denser concentration of liquidity at specific prices. Compared to the full range liquidity model Uniswap V2 uses, this allows traders to make larger trades with less price impact. Liquidity providers can choose a specific price range in which they want their liquidity to be used by trades.
To achieve this, Uniswap V3 Pools discriminate the price range with Ticks.
Ticks
Ticks are the boundaries between discrete price ranges.
A change of 1 Tick always represents a price change of 0.01% from the current price.
Uniswap V3 Pools can have different tickSpacings
, a constant that describes which ticks can be used by the Pool.
Only ticks at indices that are divisible by the tickSpacing can be initialized.
This value is dependant on the fee of the Pool, Pools with higher fees have higher tickSpacing.
For example, a Pool with HIGH fee (1%) has a tickSpacing of 200, meaning the price difference between initializable Ticks is:
or %
Liquidity Positions
When someone provides liquidity to a Pool, they create a Liquidity Position. This position is defined by the amount of liquidity provided and the start tick and the end tick, or price range, of the Position.
Because V3 Pools allow users to choose any price range in which they want to provide liquidity, it is possible to create positions that do not contain the current Price of the Pool. In this case, the liquidity provider will pay only one type of Token into the Pool, creating a single side liquidity position.
To learn more about how Ticks and Liquidity positions work, consider reading the whitepaper or the other resources mentioned above.
Now that we have a rough understanding of liquidity positions in Uniswap V3, let's look at the correspondent classes the SDK offers us.
Position class
The sdk provides a Position
class used to create local representations of an onchain position.
It is used to create the calldata for onchain calls to mint or modify an onchain position.
There are four ways to construct a position.
Directly with the constructor:
import { Pool, Position } from '@uniswap/v3-sdk'
import JSBI from 'jsbi'
const pool = new Pool(...)
const tickLower: number = -100
const tickUpper: number = 200
const liquidity: JSBI = JSBI.BigInt('1000000000000000000')
const position = new Position({
pool,
liquidity,
tickLower,
tickUpper
})
Using the fromAmounts()
function:
import { BigIntish } from '@uniswap/sdk-core'
const pool = new Pool(...)
const tickLower: number = -100
const tickUpper: number = 200
const amount0: BigIntish = '1000000000000000000'
const amount1: BigIntish = JSBI.BigInt('1000000000000000000')
const useFullPrecision: boolean = true
const position = Position.fromAmounts({
pool,
tickLower,
tickUpper,
amount0,
amount1,
useFullPrecision
})
Or using the fromAmount0()
or fromAmount1()
functions:
import { BigIntish } from '@uniswap/sdk-core'
...
const pool = new Pool(...)
const tickLower: number = -200
const tickUpper: number = 100
const amount0: BigIntish = '1000000000000000000'
const useFullPrecision: boolean = true
const singleSidePositionToken0 = Position.fromAmount0({
pool,
tickLower,
tickUpper,
amount0,
useFullPrecision
})
const amount1: BigIntish = 100000000
const singleSidePositionToken1 = Position.fromAmount1({
pool,
tickLower,
tickUpper,
amount1,
useFullPrecision
})
These last two functions calculate a position at the given tick range given the amount of token0
or token1
. The amount of the second token is calculated from the ratio of the tokens inside the tick range and the amount of token one.
A create transaction would then fail if the wallet doesn't hold enough token1
or the Contract is not given the necessary Transfer Approval.
All of these functions take an Object with named values as a call parameter. The amount and liquidity values are of type BigIntish
which accepts number
, string
and JSBI
.
The values of tickLower
and tickUpper
must match initializable ticks of the Pool.
NonfungiblePositionManager
The NonfungiblePositionManager
class is mainly used to create calldata for functions on the NonfungiblePositionManager Contract.
We will look at the sdk class and write functions on the Contract in this section.
Creating a Position
To create a position on a Pool, the mint
function is called on the Contract.
The sdk class provides the addCallParameters
function to create the calldata for the transaction:
import { MintOptions, NonfungiblePositionManager } from '@uniswap/v3-sdk'
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
)
This call creates a position if it doesn't exist, but can also be used to increase an existing position. Take a look at the Mint Position guide and Modify Position guide to learn more.
Decreasing and Increasing a Position
To decrease or increase the liquidity of a Position, the decreaseLiquidity
or increaseLiquidity
functions are called on the Contract.
To increase, addCallParameters
is used as mentioned above, to decrease we use removeCallParameters
:
const { calldata, value } = NonfungiblePositionManager.removeCallParameters(
currentPosition,
removeLiquidityOptions
)
Take a look at the Modify Positions guide to learn how to create the currentPosition
and removeLiquidityOptions
parameters.
Collecting Fees
To collect fees accrued, the collect
function is called on the Contract.
The sdk class provides the collectCallParameters
function to create the calldata for that:
const { calldata, value } =
NonfungiblePositionManager.collectCallParameters(collectOptions)
Next steps
Now that you are familiar with the most important classes and Contract to interact with Liquidity Positions, continue with the next guide on Minting Positions.