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-peripheryAll 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);| Parameter | Type | Description |
|---|---|---|
poolKey | PoolKey | the pool to add liquidity to |
tickLower | int24 | lower tick boundary of the position |
tickUpper | int24 | upper tick boundary of the position |
liquidity | uint256 | amount of liquidity units to mint |
amount0Max | uint128 | maximum currency0 the caller is willing to pay |
amount1Max | uint128 | maximum currency1 the caller is willing to pay |
recipient | address | address that receives the position NFT |
hookData | bytes | arbitrary 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);| Parameter | Type | Description |
|---|---|---|
tokenId | uint256 | position identifier (ERC-721 token ID) |
liquidity | uint256 | amount of liquidity to add |
amount0Max | uint128 | maximum currency0 the caller is willing to pay |
amount1Max | uint128 | maximum currency1 the caller is willing to pay |
hookData | bytes | arbitrary 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);| Parameter | Type | Description |
|---|---|---|
tokenId | uint256 | position identifier |
liquidity | uint256 | amount of liquidity to remove |
amount0Min | uint128 | minimum currency0 the caller expects to receive |
amount1Min | uint128 | minimum currency1 the caller expects to receive |
hookData | bytes | arbitrary 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
- Create a pool: initialize a new v4Â pool
- Burn liquidity: close a position entirely
- Batch liquidity: manage multiple positions in a single transaction
- PositionManager reference: full APIÂ reference