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];
}
ERC721 Approval Warning If your contract transfers position NFTs, make sure to appropriately manage approvals or check permissions to prevent unauthorized transfers of liquidity ranges.

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.