Provide Liquidity (V3)
V3 allows LPs to concentrate liquidity within a specific price range for higher capital efficiency. Each position is an NFT with unique parameters.
Create a Position
1. Choose Pool Parameters
- Token pair: The two tokens
- Fee tier: 0.01%, 0.05%, 0.3%, or 1% (see Fee Tiers)
- Price range:
[tickLower, tickUpper] (see Ticks and Ranges)
2. Get Current Pool State
const V3_FACTORY = '0x0dfb1Bb755d872EA1fa4d95E4ad0c2E6317Ce9B9'; // Mainnet
const V3_POSITION_MANAGER = '0x343b244bEDF133D57C61b241557bF29AA32ea4F9';
const FACTORY_ABI = ['function getPool(address,address,uint24) view returns (address)'];
const POOL_ABI = [
'function slot0() view returns (uint160, int24, uint16, uint16, uint16, uint8, bool)',
'function tickSpacing() view returns (int24)'
];
const factory = new ethers.Contract(V3_FACTORY, FACTORY_ABI, provider);
const poolAddress = await factory.getPool(token0, token1, 3000);
const pool = new ethers.Contract(poolAddress, POOL_ABI, provider);
const [sqrtPriceX96, currentTick] = await pool.slot0();
const tickSpacing = await pool.tickSpacing();
3. Calculate Tick Range
Align your desired price range to the pool’s tick spacing:
function nearestUsableTick(tick, tickSpacing) {
return Math.round(tick / tickSpacing) * tickSpacing;
}
// Example: ±10% around current price
const tickLower = nearestUsableTick(currentTick - 2000, tickSpacing);
const tickUpper = nearestUsableTick(currentTick + 2000, tickSpacing);
4. Approve Tokens
await token0Contract.approve(V3_POSITION_MANAGER, amount0Desired);
await token1Contract.approve(V3_POSITION_MANAGER, amount1Desired);
5. Mint Position
const PM_ABI = [
'function mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256)) payable returns (uint256,uint128,uint256,uint256)'
];
const pm = new ethers.Contract(V3_POSITION_MANAGER, PM_ABI, signer);
const deadline = Math.floor(Date.now() / 1000) + 1200;
const tx = await pm.mint({
token0,
token1,
fee: 3000,
tickLower,
tickUpper,
amount0Desired,
amount1Desired,
amount0Min: amount0Desired * 99n / 100n,
amount1Min: amount1Desired * 99n / 100n,
recipient: userAddress,
deadline
});
const receipt = await tx.wait();
// Parse the tokenId from the receipt events
token0 must have a lower address than token1. Sort them before calling.
With Native KAS
If one token is KAS, send it as value instead of approving WKAS:
const tx = await pm.mint(params, { value: kasAmount });
Increase Liquidity
Add more tokens to an existing position (same tick range):
const tx = await pm.increaseLiquidity({
tokenId,
amount0Desired,
amount1Desired,
amount0Min: 0,
amount1Min: 0,
deadline
});
Collect Fees
Claim accumulated trading fees:
const tx = await pm.collect({
tokenId,
recipient: userAddress,
amount0Max: ethers.MaxUint128,
amount1Max: ethers.MaxUint128
});
Remove Liquidity
1. Decrease Liquidity
const tx = await pm.decreaseLiquidity({
tokenId,
liquidity: liquidityToRemove, // Use full liquidity for complete removal
amount0Min: 0,
amount1Min: 0,
deadline
});
2. Collect the Tokens
After decreasing, tokens are held by the Position Manager. Call collect() to withdraw:
const tx = await pm.collect({
tokenId,
recipient: userAddress,
amount0Max: ethers.MaxUint128,
amount1Max: ethers.MaxUint128
});
3. Burn the NFT (Optional)
After removing all liquidity and collecting all tokens/fees:
const tx = await pm.burn(tokenId);
Single-Sided Positions
You can create positions entirely above or below the current price:
| Range | Deposit | Behavior |
|---|
Above current price (tickLower > currentTick) | Token0 only | Acts as a limit sell order |
Below current price (tickUpper < currentTick) | Token1 only | Acts as a limit buy order |
// Limit sell: provide only token0, range above current price
const tx = await pm.mint({
token0,
token1,
fee: 3000,
tickLower: currentTick + 60, // Above current
tickUpper: currentTick + 600,
amount0Desired: sellAmount,
amount1Desired: 0, // No token1 needed
amount0Min: 0,
amount1Min: 0,
recipient: userAddress,
deadline
});
Key Points
- Each position is an NFT — track
tokenId for future operations
- Fees must be explicitly collected (they don’t auto-compound)
- Out-of-range positions earn no fees until price re-enters the range
- Narrower ranges = higher capital efficiency but more active management needed
- Use
multicall() on the Position Manager to batch decrease + collect in one transaction