Managing Liquidity

Mint, increase, decrease, and collect fees on Uniswap v4 liquidity positions using the PositionManager.

This guide covers the full lifecycle of a Uniswap v4 liquidity position: minting a new position, increasing and decreasing liquidity, and collecting accrued fees. All operations go through the PositionManager contract using an encoded command system.

Prerequisites

Developing with Uniswap v4 requires Foundry.

Use v4-template for a pre-configured project, or install manually:

forge install uniswap/v4-core
forge install uniswap/v4-periphery

All examples below assume you have already imported and instantiated the PositionManager:

import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "v4-periphery/src/libraries/Actions.sol";

IPositionManager posm = IPositionManager(<address>);

Minting a Position

Minting creates a new ERC-721 liquidity position in a specific pool and tick range.

Encode actions

bytes memory actions = abi.encodePacked(
    uint8(Actions.MINT_POSITION),
    uint8(Actions.SETTLE_PAIR)
);

For ETH liquidity positions, add a SWEEP action to recover excess ETH:

bytes memory actions = abi.encodePacked(
    uint8(Actions.MINT_POSITION),
    uint8(Actions.SETTLE_PAIR),
    uint8(Actions.SWEEP)
);

Encode parameters

bytes[] memory params = new bytes[](2); // 3 for ETH positions

params[0] = abi.encode(
    poolKey, tickLower, tickUpper, liquidity,
    amount0Max, amount1Max, recipient, hookData
);

params[1] = abi.encode(currency0, currency1);

// For ETH positions:
// params[2] = abi.encode(CurrencyLibrary.ADDRESS_ZERO, recipient);
ParameterTypeDescription
poolKeyPoolKeythe pool to add liquidity to
tickLowerint24lower tick boundary of the position
tickUpperint24upper tick boundary of the position
liquidityuint256amount of liquidity units to mint
amount0Maxuint128maximum currency0 the caller is willing to pay
amount1Maxuint128maximum currency1 the caller is willing to pay
recipientaddressaddress that receives the position NFT
hookDatabytesarbitrary data forwarded to hook functions

Submit the call

uint256 deadline = block.timestamp + 60;
uint256 valueToPass = currency0.isAddressZero() ? amount0Max : 0;

posm.modifyLiquidities{value: valueToPass}(
    abi.encode(actions, params),
    deadline
);

Increasing Liquidity

Add more tokens to an existing position. In v4, fee revenue is automatically credited when increasing liquidity.

Encode actions

bytes memory actions = abi.encodePacked(
    uint8(Actions.INCREASE_LIQUIDITY),
    uint8(Actions.SETTLE_PAIR)
);

If accrued fees fully cover the increase (rare), use CLOSE_CURRENCY instead of SETTLE_PAIR to handle any remainder:

bytes memory actions = abi.encodePacked(
    uint8(Actions.INCREASE_LIQUIDITY),
    uint8(Actions.CLOSE_CURRENCY),
    uint8(Actions.CLOSE_CURRENCY)
);

Encode parameters

bytes[] memory params = new bytes[](2);

params[0] = abi.encode(tokenId, liquidity, amount0Max, amount1Max, hookData);
params[1] = abi.encode(currency0, currency1);
ParameterTypeDescription
tokenIduint256position identifier (ERC-721 token ID)
liquidityuint256amount of liquidity to add
amount0Maxuint128maximum currency0 the caller is willing to pay
amount1Maxuint128maximum currency1 the caller is willing to pay
hookDatabytesarbitrary data forwarded to hook functions

Submit the call

uint256 deadline = block.timestamp + 60;

posm.modifyLiquidities(
    abi.encode(actions, params),
    deadline
);

Decreasing Liquidity

Remove tokens from an existing position. Fee revenue is automatically debited when decreasing liquidity.

Encode actions

bytes memory actions = abi.encodePacked(
    uint8(Actions.DECREASE_LIQUIDITY),
    uint8(Actions.TAKE_PAIR)
);

Encode parameters

bytes[] memory params = new bytes[](2);

params[0] = abi.encode(tokenId, liquidity, amount0Min, amount1Min, hookData);
params[1] = abi.encode(currency0, currency1, recipient);
ParameterTypeDescription
tokenIduint256position identifier
liquidityuint256amount of liquidity to remove
amount0Minuint128minimum currency0 the caller expects to receive
amount1Minuint128minimum currency1 the caller expects to receive
hookDatabytesarbitrary data forwarded to hook functions

Submit the call

uint256 deadline = block.timestamp + 60;

posm.modifyLiquidities(
    abi.encode(actions, params),
    deadline
);

Collecting Fees

There is no dedicated COLLECT command. To collect fees, decrease liquidity by zero:

Encode actions

bytes memory actions = abi.encodePacked(
    uint8(Actions.DECREASE_LIQUIDITY),
    uint8(Actions.TAKE_PAIR)
);

Encode parameters

bytes[] memory params = new bytes[](2);

// liquidity = 0, amount0Min = 0, amount1Min = 0
params[0] = abi.encode(tokenId, 0, 0, 0, hookData);
params[1] = abi.encode(currency0, currency1, recipient);

Setting amount0Min and amount1Min to 0 is safe for fee collection because fee amounts cannot be manipulated via front-running.

Submit the call

uint256 deadline = block.timestamp + 60;

posm.modifyLiquidities(
    abi.encode(actions, params),
    deadline
);

To determine how many fee tokens were collected, read the token balances of the recipient before and after calling modifyLiquidities().

Next Steps