Skip to content

Instantly share code, notes, and snippets.

@adamstallard
Last active February 7, 2025 02:31
Show Gist options
  • Select an option

  • Save adamstallard/f589a6b2f68dffacb96ebacf4e1a55e2 to your computer and use it in GitHub Desktop.

Select an option

Save adamstallard/f589a6b2f68dffacb96ebacf4e1a55e2 to your computer and use it in GitHub Desktop.
Locked Uniswap V4 Liquidity (can still withdraw fees)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Import OpenZeppelin's Ownable and ERC-20 libraries
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// Import the Uniswap V4 Position Manager Interface
import "./IPositionManager.sol";
/// @title Locked Liquidity Contract for Uniswap V4
/// @author C. Adam Stallard
/// @notice A contract that locks Uniswap V4 liquidity positions and allows fee withdrawals for a pre-defined token.
contract LockedLiquidity is Ownable, IERC721Receiver {
/** @dev Address of Uniswap V4's Position Manager */
address public immutable positionManager;
/** @dev The ID of the locked NFT liquidity position */
uint256 public lockedPositionId;
/** @dev Address of the token for which fees will be withdrawable (e.g., token0 or token1) */
address public immutable feeToken;
/**
* @param _positionManager The address of Uniswap V4’s Position Manager contract
* @param _feeToken The address of the token for which fees will be withdrawable
*/
constructor(address _positionManager, address _feeToken) {
positionManager = _positionManager;
feeToken = _feeToken;
}
/**
* @notice Handles receiving an ERC721 NFT (Uniswap V4 liquidity position)
* @dev Locks the position and sets the locked position ID.
* @param tokenId The NFT token ID for the locked position
* @return The selector for IERC721Receiver
*/
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external override returns (bytes4) {
require(msg.sender == positionManager, "Invalid sender: must be PositionManager");
require(lockedPositionId == 0, "A position is already locked");
lockedPositionId = tokenId;
return IERC721Receiver.onERC721Received.selector;
}
/**
* @notice Collects fees for the predefined fee token from the locked position and forwards them to the recipient.
* @param recipient The address where the collected fees will be sent
*/
function collectFees(address recipient) external onlyOwner {
bytes memory actions = abi.encodePacked(
uint256(Actions.DECREASE_LIQUIDITY), // Collect fees
uint256(Actions.TAKE) // Transfer the fee token to the contract
);
// DECREASE_LIQUIDITY (credits token fees without altering liquidity)
bytes[] memory params = new bytes[](2);
params[0] = abi.encode(
lockedPositionId, // ID of the locked position
0, // Zero liquidity (fee collection only)
0, // Min token0 amount (not applicable here)
0, // Min token1 amount (not applicable here)
"" // Hook data (empty)
);
// TAKE (move fees into the contract's balance)
params[1] = abi.encode(feeToken, address(this)); // Contract should receive the fees
// Execute modifyLiquidities to collect fees for the position
IPositionManager(positionManager).modifyLiquidities(
abi.encode(actions, params), // Encoded actions and parameters
block.timestamp + 60 // Deadline for the operation
);
uint256 feeTokenBalance = IERC20(feeToken).balanceOf(address(this));
IERC20(feeToken).transfer(recipient, feeTokenBalance);
}
}
/// @notice Uniswap V4 actions for liquidity modification
library Actions {
uint256 constant public DECREASE_LIQUIDITY = 0x01; // Action to decrease liquidity (or collect fees with 0 liquidity)
uint256 constant public TAKE = 0x12; // Action to collect fees for a single token
}
@adamstallard
Copy link
Author

I purposely made this to only allow collecting fees from one of the two tokens. You could modify it to use TAKE_PAIR if you want to allow collecting fees from both tokens.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment