Managing Liquidity
Create, modify, and manage concentrated liquidity positions on Rapiddex v3 directly from your smart contracts.
This guide explains how to integrate Concentrated Liquidity Position Management on Rapiddex v3 into your smart
contracts. It covers interacting with the INonfungiblePositionManager contract to mint, adjust,
and collect accrued trading fees from liquidity positions.
Nonfungible Position Manager
Rapiddex v3 liquidity positions are tokenized as ERC-721 NFTs. The NonfungiblePositionManager
contract handles the minting of new positions, increasing/decreasing liquidity, and fee collection. Each
position is defined by its token pair, fee tier, tick range (lower and upper ticks), and current liquidity
amount.
Receiving Position NFTs
When minting a liquidity position to a smart contract recipient, the smart contract must be capable of
receiving and holding ERC-721 tokens. To support this, your contract must implement the
IERC721Receiver interface and return the selector for onERC721Received.
Minting a New Position
To supply liquidity and create a new position, transfer the required tokens from the user, approve the
position manager to spend them, and call the mint function with the specified range parameters.
Solidity Implementation Examples
The following Solidity contract shows how to set up token approvals, handle the ERC-721 receiver callback, mint new positions, decrease liquidity, and collect accrued trading fees:
solidity// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@rapiddex/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract LiquidityExamples is IERC721Receiver {
INonfungiblePositionManager public immutable positionManager;
// Track position token IDs minted by users through this contract
mapping(uint256 => address) public positionOwners;
constructor(INonfungiblePositionManager _positionManager) {
positionManager = _positionManager;
}
/// @notice Required to receive ERC721 position NFTs
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external override returns (bytes4) {
return this.onERC721Received.selector;
}
/// @notice Mint a new concentrated liquidity position
function mintNewPosition(
address token0,
address token1,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint256 amount0Desired,
uint256 amount1Desired,
uint256 amount0Min,
uint256 amount1Min
) external returns (
uint256 tokenId,
uint128 liquidity,
uint256 amount0,
uint256 amount1
) {
// Transfer tokens from the caller to this contract
IERC20(token0).transferFrom(msg.sender, address(this), amount0Desired);
IERC20(token1).transferFrom(msg.sender, address(this), amount1Desired);
// Approve the position manager to spend these tokens
IERC20(token0).approve(address(positionManager), amount0Desired);
IERC20(token1).approve(address(positionManager), amount1Desired);
INonfungiblePositionManager.MintParams memory params =
INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: fee,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: amount0Desired,
amount1Desired: amount1Desired,
amount0Min: amount0Min,
amount1Min: amount1Min,
recipient: address(this),
deadline: block.timestamp
});
// Call NonfungiblePositionManager to mint the position NFT
(tokenId, liquidity, amount0, amount1) = positionManager.mint(params);
// Track local owner
positionOwners[tokenId] = msg.sender;
// Refund any unused tokens to the sender
if (amount0 < amount0Desired) {
IERC20(token0).approve(address(positionManager), 0);
IERC20(token0).transfer(msg.sender, amount0Desired - amount0);
}
if (amount1 < amount1Desired) {
IERC20(token1).approve(address(positionManager), 0);
IERC20(token1).transfer(msg.sender, amount1Desired - amount1);
}
}
/// @notice Collect accrued fees from the position NFT
function collectAllFees(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) {
require(positionOwners[tokenId] == msg.sender, "Not position owner");
INonfungiblePositionManager.CollectParams memory params =
INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: msg.sender, // send directly to owner
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
});
(amount0, amount1) = positionManager.collect(params);
}
/// @notice Decrease the active liquidity in an existing position
function decreaseLiquidityInPosition(
uint256 tokenId,
uint128 liquidity,
uint256 amount0Min,
uint256 amount1Min
) external returns (uint256 amount0, uint256 amount1) {
require(positionOwners[tokenId] == msg.sender, "Not position owner");
INonfungiblePositionManager.DecreaseLiquidityParams memory params =
INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: liquidity,
amount0Min: amount0Min,
amount1Min: amount1Min,
deadline: block.timestamp
});
(amount0, amount1) = positionManager.decreaseLiquidity(params);
}
}
Increasing Liquidity
To add more liquidity to an existing position, call increaseLiquidity() on the position manager
with the token ID of the position NFT. The tokens are added within the same tick range as the original
position.
solidity/// @notice Add liquidity to an existing position
function increaseLiquidityInPosition(
uint256 tokenId,
uint256 amount0Desired,
uint256 amount1Desired,
uint256 amount0Min,
uint256 amount1Min
) external returns (uint128 liquidity, uint256 amount0, uint256 amount1) {
require(positionOwners[tokenId] == msg.sender, "Not position owner");
// Fetch token addresses for this position from the manager
(
, , address token0, address token1,
, , , , , , ,
) = positionManager.positions(tokenId);
// Transfer the desired token amounts from the caller
IERC20(token0).transferFrom(msg.sender, address(this), amount0Desired);
IERC20(token1).transferFrom(msg.sender, address(this), amount1Desired);
// Approve position manager to spend both tokens
IERC20(token0).approve(address(positionManager), amount0Desired);
IERC20(token1).approve(address(positionManager), amount1Desired);
INonfungiblePositionManager.IncreaseLiquidityParams memory params =
INonfungiblePositionManager.IncreaseLiquidityParams({
tokenId: tokenId,
amount0Desired: amount0Desired,
amount1Desired: amount1Desired,
amount0Min: amount0Min,
amount1Min: amount1Min,
deadline: block.timestamp
});
(liquidity, amount0, amount1) = positionManager.increaseLiquidity(params);
// Refund any unused tokens to the sender
if (amount0 < amount0Desired) {
IERC20(token0).approve(address(positionManager), 0);
IERC20(token0).transfer(msg.sender, amount0Desired - amount0);
}
if (amount1 < amount1Desired) {
IERC20(token1).approve(address(positionManager), 0);
IERC20(token1).transfer(msg.sender, amount1Desired - amount1);
}
}
Burning a Position
Once all liquidity has been removed and all fees have been collected from a position (both values must be
zero), you can permanently delete the position NFT by calling burn(tokenId). This reclaims a
small amount of gas and clears the storage slot.
solidityfunction burnPosition(uint256 tokenId) external {
require(positionOwners[tokenId] == msg.sender, "Not position owner");
// Liquidity and owed fees must both be 0 before burning
positionManager.burn(tokenId);
delete positionOwners[tokenId];
}
Parameters and Outputs Reference
1. mint
Mints a new Concentrated Liquidity position NFT representing liquidity provided in a custom tick range.
| Struct Field (MintParams) | Type | Description |
|---|---|---|
token0 |
address | The contract address of token0 (must have a lower address value). |
token1 |
address | The contract address of token1 (must have a higher address value). |
fee |
uint24 | The fee tier of the pool (500, 3000, or 10000). |
tickLower |
int24 | The lower boundary of the price range (must be divisible by tick spacing). |
tickUpper |
int24 | The upper boundary of the price range (must be divisible by tick spacing). |
amount0Desired |
uint256 | The maximum amount of token0 desired to be deposited. |
amount1Desired |
uint256 | The maximum amount of token1 desired to be deposited. |
amount0Min |
uint256 | Slippage limit: Minimum amount of token0 required to be deposited. |
amount1Min |
uint256 | Slippage limit: Minimum amount of token1 required to be deposited. |
recipient |
address | The address that will receive the minted NFT. |
deadline |
uint256 | Unix timestamp after which the transaction reverts. |
Outputs:
tokenId(uint256) - The ID of the minted ERC-721 position NFT.liquidity(uint128) - The amount of liquidity added to the pool.amount0(uint256) - The actual amount of token0 deposited.amount1(uint256) - The actual amount of token1 deposited.
2. increaseLiquidity
Adds additional token0 and/or token1 reserves to an existing position NFT.
| Struct Field (IncreaseLiquidityParams) | Type | Description |
|---|---|---|
tokenId |
uint256 | The ID of the position NFT to increase liquidity for. |
amount0Desired |
uint256 | The maximum amount of token0 desired to be added. |
amount1Desired |
uint256 | The maximum amount of token1 desired to be added. |
amount0Min |
uint256 | Slippage limit: Minimum amount of token0 required to be deposited. |
amount1Min |
uint256 | Slippage limit: Minimum amount of token1 required to be deposited. |
deadline |
uint256 | Unix timestamp after which the transaction reverts. |
Outputs:
liquidity(uint128) - The actual amount of liquidity added.amount0(uint256) - The actual amount of token0 deposited.amount1(uint256) - The actual amount of token1 deposited.
3. decreaseLiquidity
Removes a portion of liquidity from a position NFT (reserves are held in the contract as owed tokens until collected).
| Struct Field (DecreaseLiquidityParams) | Type | Description |
|---|---|---|
tokenId |
uint256 | The ID of the position NFT to decrease liquidity for. |
liquidity |
uint128 | The amount of liquidity units to remove. |
amount0Min |
uint256 | Slippage limit: Minimum amount of token0 expected to be withdrawn. |
amount1Min |
uint256 | Slippage limit: Minimum amount of token1 expected to be withdrawn. |
deadline |
uint256 | Unix timestamp after which the transaction reverts. |
Outputs:
amount0(uint256) - The amount of token0 removed.amount1(uint256) - The amount of token1 removed.
4. collect
Collects accrued trading fees and/or decreased liquidity tokens from the position NFT.
| Struct Field (CollectParams) | Type | Description |
|---|---|---|
tokenId |
uint256 | The ID of the position NFT to collect fees from. |
recipient |
address | The address that will receive the collected tokens. |
amount0Max |
uint128 | The maximum amount of token0 to collect (use type(uint128).max to collect all). |
amount1Max |
uint128 | The maximum amount of token1 to collect (use type(uint128).max to collect all). |
Outputs:
amount0(uint256) - The actual amount of token0 collected.amount1(uint256) - The actual amount of token1 collected.
