Positions in Uniswap v3
Adding liquidity to an AMM means depositing tokens into the AMM pool. Liquidity providers do this in the hope of earning fees from users who swap with that pool.
In Uniswap v2, when an LP adds liquidity, they receive shares of the pool in the form of Liquidity Provider tokens (LP tokens), representing the percentage of tokens in the pool to which they are entitled, including fees. These LP tokens are fungible ERC-20 tokens.
In Uniswap v3, this approach doesn’t work because the LP chooses the range where they want to deposit liquidity—the lower and upper ticks. Therefore, the protocol needs to track each deposit individually in a non-fungible manner. This brings us to the concept of positions.
When an LP adds liquidity in a range, we say they open or modify a position. It is opened when the position does not yet exist, and modified when it does.
A range consists of two ticks — a lower and an upper tick. Depositing liquidity into a range means increasing the real reserves within that range. This is achieved using the mint function in UniswapV3Pool.sol, whose interface is shown below.
// UniswapV3Pool.sol
function mint(
address recipient, // the owner of the position
int24 tickLower,
int24 tickUpper,
uint128 amount, // amount in liquidity
bytes calldata data // will be explained in a future chapter, it is not necessary for our discussion
) external override lock returns (uint256 amount0, uint256 amount1) {
The name mint for this function is reminiscent of Uniswap v2, where adding liquidity minted ERC-20 tokens for the liquidity provider. Although this no longer occurs in v3, the name has been preserved – this time to refer to minting a position.
The goal of this chapter is to examine how and where the protocol stores information about these positions.
The positions mapping
Positions are stored in a mapping called positions, located in the UniswapV3Pool contract, as shown in the image below.

The key that identifies a position is formed by the Keccak hash of the position owner’s address, the lower tick, and the upper tick.
Thus, if this is the first time liquidity is deposited between ticks -10 and 10 for owner 0xA, a position will be created with the key keccak(0xA, -10, 10).
Below is a Solidity code used to generate the key for a position.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Position {
function getKey(
address owner,
int24 tickLower,
int24 tickUpper)
public pure returns (bytes32 key) {
key = keccak256(abi.encodePacked(owner, tickLower, tickUpper));
}
}
Once created, if more liquidity is deposited (or withdrawn) between ticks -10 and 10 for owner 0xA, the position will be modified, since it already exists.
The mapping value is of type Position.Info, which is a struct named Info located in the Position library within the Position.sol contract.
This struct, shown below, stores the position’s liquidity (orange box), as well as four other fields related to fees and tokens owned by the position (green box). We will postpone the discussion of these other fields until we cover fees and liquidity withdrawal from a position.

For now, we can think of it this way: if liquidity is added within a range by a given owner, a position is created. After that, this position can be modified by adding or removing liquidity.
Positions are non-fungible
In Uniswap v2, when an LP provides liquidity, ERC-20 LP tokens are minted for the LP. These LP tokens are fungible and represent a share of the pool’s assets. This means that, if two LPs each own 1000 LP tokens and use their LP tokens to redeem their shares in the pool, the asset tokens they receive will be the same.
In Uniswap v3, positions are non-fungible. This means that if two LPs have different positions and use these positions to redeem their shares in the pool, they will probably not receive the same amount of assets, as they likely don’t represent the same range of ticks or the real reserves the LPs contributed between those ticks.
It is possible to handle these non-fungible positions as ERC-721 non-fungible tokens through a peripheral contract, but the core contract itself does not do this—it is not an ERC-721 contract and does not allow transferring positions to third parties. The core contract only allows opening and modifying positions.
Fees in Uniswap v3
Fees in Uniswap v3 also work very differently from those in Uniswap v2. In Uniswap v2, whenever a swap occurs, the fees are added to the pool, increasing the amount of tokens each share is entitled to.
In Uniswap v3, a position is only entitled to fees from swaps that occurred—partially or entirely—within its tick range. Therefore, if a LP opens a position that is never involved in a swap, that position earns no fees. This incentivizes LPs to open positions in regions where swaps are likely to happen, thereby increasing liquidity where it is most needed.
Since fees are no longer shared proportionally among all LPs but are attributed individually to each position, they must be tracked separately.
How the protocol calculates how many of the fees a position is entitled to is not straightforward and involves a combination of several variables, including the feeGrowthInside0LastX128 and feeGrowthInside1LastX128 variables present in the Position’s Info struct.
As a high-level explanation, the protocol keeps track of the fees accumulated in the pool since its creation, as well as how many fees were collected below and above each tick that serves as a position’s boundary.
This allows the protocol to calculate how much of the accumulated fees were collected between the lower and upper ticks of a position and use the position’s feeGrowthInside0LastX128 and feeGrowthInside1LastX128 variables to determine how much of these fees belong to that particular position.
The details of how the protocol achieves this will be covered in future chapters.
A pool is made up of several positions
We’ve been saying that a pool consists of segments, so we need to connect the idea of segments and positions. They are not the same, since positions can overlap. Let’s explore this through an example.
Consider only two positions:
- Between ticks -10 and 5 with liquidity of 200, shown in the red box below.
- Between ticks 0 and 10 with liquidity of 100, illustrated in the blue box below.

- Between ticks -10 and 0, there is only one position with liquidity of 200, so the liquidity for this segment will be 200.
- Between ticks 0 and 5, two positions overlap: one with liquidity of 200 and the other with liquidity of 100, so the liquidity for this segment will be 300.
- Between ticks 5 and 15, there is only one position with liquidity of 100, so the liquidity for this segment will be 100.
The segments are shown in the figure above on the right.
Below is an interactive tool where the reader can create multiple positions, and the tool automatically calculates segments based on these positions. It is also possible to change the current price and view the liquidity of the segment in which the current price is located.
However, the protocol does NOT compute all the segments based on the positions, as we just did. This would be highly inefficient, since the protocol does not need such a global picture.
In a later chapter, we will discuss how the protocol efficiently handles positions to compute segments. For now, we temporarily treat that computation as a black box.
In the next section, we will take a closer look at the mint function and what the LP must deposit to open a position.
The mint function
To add liquidity to a pool, it is necessary to deposit tokens. As we have seen, this is done through the mint function, whose interface is shown again below.
function mint(
address recipient, // the owner of the position
int24 tickLower,
int24 tickUpper,
uint128 amount, // amount in liquidity
bytes calldata data // will be explained in a future chapter, it is not necessary for our discussion
) external override lock returns (uint256 amount0, uint256 amount1) {
This function expects five parameters:
- The position owner’s address (
recipient), the lower tick (tickLower), and the upper tick (tickUpper) of the position. - The
dataparameter, of typebytes, will be explained later and is not important for the current discussion. - The
amountparameter, which is the amount of liquidity the LP wants to add to the pool between the lower and upper tick.
Note that amount is of type uint128, which means it cannot be negative. The mint function is used only to add liquidity, not to remove it—removals are handled by the burn function, which will be discussed later.
The amount of tokens that need to be deposited to add this amount of liquidity between the lower and upper ticks must then be calculated by the mint function.
This is what we will see next.
Tokens required to open a position
The amount of tokens corresponding to liquidity $L$ between a lower and an upper tick is the real reserves of the segment corresponding to that position, i.e., a segment with liquidity $L$ between the lower and upper tick.
We’ve already learned that a segment’s real reserves depend not only on the tick boundaries and liquidity $L$ but also on the current price. The rule is as follows:
- When the current price is at or above the upper tick, the segment has real reserves only in tokens Y.
- When the current price is at or below the lower tick, the segment has real reserves only in tokens X.
- When the current price is between the lower and upper ticks, the segment has real reserves in both tokens X and Y.
These three scenarios are illustrated in the figure below, where the red ray represents the current price $p$, $p_l$ represents the lower tick and $p_u$ represents the upper tick.

If the LP wants to add liquidity $L$ between the lower tick $p_l$ and the upper tick $p_u$, the protocol calculates $x_r$ (real reserves in tokens X) and $y_r$ (real reserves in tokens Y) according to the three scenarios described above.
- For scenario 1, the real reserves in tokens Y are
- For scenario 2, the real reserves in tokens X are
- For scenario 3, the real reserves in tokens X and Y are
These token amounts required to add liquidity $L$ to the position are calculated and returned by the mint function, as shown in the red box below.

However, for the end user, liquidity can be a highly abstract concept. It is much more common for the end user to think in terms of tokens rather than liquidity—for example, a user might want to provide liquidity by depositing 100 tokens X without having any idea of the amount of liquidity that 100 tokens X represents.
Adding liquidity by choosing token amounts can be facilitated through an intermediary contract that acts as a bridge between the end user and the core contract and calculates the conversion between token amounts and liquidity.
Opening a position through a position manager
The mint function in the core contract is meant to be called by another contract and NOT by EOAs.
When the mint function is called, it calls back the address that triggered it through the uniswapV3MintCallback function, as we can see in the code snippet of the mint function below, highlighted in a red box.

The address that calls the mint function must implement the uniswapV3MintCallback function, as this is when the caller must transfer the token amounts required to modify the position. The mint function checks the pool’s balance immediately before and after calling uniswapV3MintCallback, and accounts for the difference.
That is why the mint function cannot be called by an EOA: EOAs cannot respond to the callback to transfer the required token amounts. Thus, an EOA call to the mint function will revert if the LP is required to transfer any tokens, which is always the case (it is not allowed to modify a position by adding zero liquidity).
The transaction flow is illustrated below, assuming the contract that calls the mint function is named Position Manager.

The core contract is NOT user-friendly. In general, the LP wants to choose the amount of tokens X and/or tokens Y they wish to deposit to open a position, rather than specifying the liquidity. The role of the intermediary contract is to provide a user-friendly interface, where the user chooses a token amount and the Position Manager converts this into the corresponding liquidity for that amount.
A representation of this process is shown below. The end user (an EOA) calls the mint function in the intermediary contract (Position Manager), passing the amount of tokens they want to transform into liquidity as one of the parameters. The position manager then converts this amount of tokens into liquidity and opens a position for the end user by calling the mint function in the core contract.

We will not go into the details of how a Position Manager works in this book. Uniswap provides a contract that works as a Position Manager, named NonfungiblePositionManager, in its periphery library, located in a different repository than the core library.
Summary
- Adding liquidity in Uniswap v3 means opening or modifying a position. Positions are defined by their owner’s address and their lower and upper ticks.
- Positions are stored in a mapping called
positions, whose key is the Keccak hash of the owner address and the lower and upper ticks, and whose value is a struct located in thePositionlibrary. - To open or add liquidity to a position, the
mintfunction is used. This function is not user-friendly and receives, as an argument, the amount of liquidity the user wants to deposit between the lower and upper ticks. - The
mintfunction must be called by other contracts, not by EOAs. These intermediary contracts act between EOAs and the core contract, and one of their functions may be to convert a token amount defined by the LP into a liquidity amount expected by the core contract’smintfunction. - To deposit liquidity $L$ between a lower and an upper tick, it is necessary to deposit the amount of tokens corresponding to the real reserves of a segment with liquidity $L$ between these ticks.