Adding Liquidity to a Uniswap v3 Pool
Once a pool is initialized, the next step is to add liquidity. In Uniswap v3, this process is more complex than in v2 because you need to specify a price range.
Understanding Liquidity Provision in v3
In Uniswap v3, when you provide liquidity:
- You specify a price range (lower and upper ticks)
- You provide both tokens in the correct ratio for the current price
- Your liquidity is only active when the price is within your specified range
- You receive an NFT representing your position
The NonfungiblePositionManager
Uniswap v3 uses a contract called NonfungiblePositionManager
to handle liquidity positions. This contract:
- Creates and manages liquidity positions as NFTs
- Handles minting, collecting fees, and burning positions
- Simplifies the complex interactions with the core pool contracts
Code Example: Adding Liquidity
Hereās how to add liquidity to a Uniswap v3 pool:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract LiquidityProvider {
INonfungiblePositionManager public positionManager;
constructor(address _positionManager) {
positionManager = INonfungiblePositionManager(_positionManager);
}
function addLiquidity(
address tokenA,
address tokenB,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint256 amountA,
uint256 amountB,
uint256 amountAMin,
uint256 amountBMin
) external returns (uint256 tokenId) {
// Approve tokens
IERC20(tokenA).approve(address(positionManager), amountA);
IERC20(tokenB).approve(address(positionManager), amountB);
// Prepare parameters
INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({
token0: tokenA < tokenB ? tokenA : tokenB,
token1: tokenA < tokenB ? tokenB : tokenA,
fee: fee,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: tokenA < tokenB ? amountA : amountB,
amount1Desired: tokenA < tokenB ? amountB : amountA,
amount0Min: tokenA < tokenB ? amountAMin : amountBMin,
amount1Min: tokenA < tokenB ? amountBMin : amountAMin,
recipient: address(this),
deadline: block.timestamp + 15 minutes
});
// Mint position
(tokenId, , , ) = positionManager.mint(params);
return tokenId;
}
}
Choosing Tick Ranges
Selecting the right tick range is crucial. Here are some strategies:
1. Narrow Range (Active Liquidity Management)
Provide liquidity in a narrow range around the current price for maximum capital efficiency, but monitor closely to adjust as price moves.
2. Wide Range (Passive Liquidity)
Provide liquidity across a wide price range for less active management, but with lower capital efficiency.
3. One-Sided Liquidity
Provide liquidity only above or below the current price if you have a directional view on the market.
Calculating Ticks from Prices
To convert a price to a tick index:
function getPriceFromTick(int24 tick) public pure returns (uint256) {
return uint256(1.0001 ** uint256(tick));
}
function getTickFromPrice(uint256 price) public pure returns (int24) {
return int24(log(price) / log(1.0001));
}
However, in practice, youāll want to use Uniswapās libraries for accurate calculations:
import "@uniswap/v3-core/contracts/libraries/TickMath.sol";
// Get the tick for a specific price
int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
// Get the price (as sqrtPriceX96) for a specific tick
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tick);
Example: Adding Liquidity to ETH/USDC Pool
Letās say we want to add liquidity to an ETH/USDC pool with a price range of $1,800 to $2,200:
// Addresses for Ethereum mainnet
address WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
uint24 fee = 3000; // 0.3%
// Calculate ticks for $1,800 and $2,200
// Note: These are approximate, you should use TickMath for exact values
int24 tickLower = -74959; // Approximately $1,800
int24 tickUpper = -73791; // Approximately $2,200
// Amount of tokens to provide
uint256 ethAmount = 1 ether;
uint256 usdcAmount = 2000 * 10**6; // 2000 USDC
// Minimum amounts (accounting for slippage)
uint256 ethMinAmount = ethAmount * 99 / 100; // 1% slippage
uint256 usdcMinAmount = usdcAmount * 99 / 100; // 1% slippage
// Add liquidity
uint256 tokenId = addLiquidity(
WETH,
USDC,
fee,
tickLower,
tickUpper,
ethAmount,
usdcAmount,
ethMinAmount,
usdcMinAmount
);
Different fee tiers have different tick spacing:
- 0.05% fee: 10 tick spacing
- 0.30% fee: 60 tick spacing
- 1.00% fee: 200 tick spacing
This means you can only use ticks that are multiples of the spacing.
What Happens After Adding Liquidity?
After adding liquidity:
- You receive an NFT representing your position
- Your liquidity is active if the current price is within your range
- You start earning fees when trades occur through your liquidity
- You can collect these fees or adjust your position later
In the next section, weāll learn how to perform swaps in Uniswap v3.