Last active
February 7, 2025 02:31
-
-
Save adamstallard/f589a6b2f68dffacb96ebacf4e1a55e2 to your computer and use it in GitHub Desktop.
Locked Uniswap V4 Liquidity (can still withdraw fees)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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 | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.