The Full Contract
Below we have the complete functioning code example: a contract that can custody Uniswap V3 position NFT's and manipulate the positions and liquidity therein by collecting fees, increasing or decreasing liquidity, and minting new positions. View on github here.
// SPDX-License-Identifier: GPL-2.0-or-laterpragma solidity =0.7.6;pragma abicoder v2;
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';import '@uniswap/v3-core/contracts/libraries/TickMath.sol';import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol';import '../libraries/TransferHelper.sol';import '../interfaces/INonfungiblePositionManager.sol';import '../base/LiquidityManagement.sol';
contract LiquidityExamples is IERC721Receiver { address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
uint24 public constant poolFee = 3000;
INonfungiblePositionManager public immutable nonfungiblePositionManager;
/// @notice Represents the deposit of an NFT struct Deposit { address owner; uint128 liquidity; address token0; address token1; }
/// @dev deposits[tokenId] => Deposit mapping(uint256 => Deposit) public deposits;
constructor( INonfungiblePositionManager _nonfungiblePositionManager ) { nonfungiblePositionManager = _nonfungiblePositionManager; }
// Implementing `onERC721Received` so this contract can receive custody of erc721 tokens function onERC721Received( address operator, address, uint256 tokenId, bytes calldata ) external override returns (bytes4) { // get position information
_createDeposit(operator, tokenId);
return this.onERC721Received.selector; }
function _createDeposit(address owner, uint256 tokenId) internal { (, , address token0, address token1, , , , uint128 liquidity, , , , ) = nonfungiblePositionManager.positions(tokenId);
// set the owner and data for position // operator is msg.sender deposits[tokenId] = Deposit({owner: owner, liquidity: liquidity, token0: token0, token1: token1}); }
/// @notice Calls the mint function defined in periphery, mints the same amount of each token. /// For this example we are providing 1000 DAI and 1000 USDC in liquidity /// @return tokenId The id of the newly minted ERC721 /// @return liquidity The amount of liquidity for the position /// @return amount0 The amount of token0 /// @return amount1 The amount of token1 function mintNewPosition() external returns ( uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1 ) { // For this example, we will provide equal amounts of liquidity in both assets. // Providing liquidity in both assets means liquidity will be earning fees and is considered in-range. uint256 amount0ToMint = 1000; uint256 amount1ToMint = 1000;
// transfer tokens to contract TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amount0ToMint); TransferHelper.safeTransferFrom(USDC, msg.sender, address(this), amount1ToMint);
// Approve the position manager TransferHelper.safeApprove(DAI, address(nonfungiblePositionManager), amount0ToMint); TransferHelper.safeApprove(USDC, address(nonfungiblePositionManager), amount1ToMint);
INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ token0: DAI, token1: USDC, fee: poolFee, tickLower: TickMath.MIN_TICK, tickUpper: TickMath.MAX_TICK, amount0Desired: amount0ToMint, amount1Desired: amount1ToMint, amount0Min: 0, amount1Min: 0, recipient: address(this), deadline: block.timestamp });
// Note that the pool defined by DAI/USDC and fee tier 0.3% must already be created and initialized in order to mint (tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager.mint(params);
// Create a deposit _createDeposit(msg.sender, tokenId);
// Remove allowance and refund in both assets. if (amount0 < amount0ToMint) { TransferHelper.safeApprove(DAI, address(nonfungiblePositionManager), 0); uint256 refund0 = amount0ToMint - amount0; TransferHelper.safeTransfer(DAI, msg.sender, refund0); }
if (amount1 < amount1ToMint) { TransferHelper.safeApprove(USDC, address(nonfungiblePositionManager), 0); uint256 refund1 = amount1ToMint - amount1; TransferHelper.safeTransfer(USDC, msg.sender, refund1); } }
/// @notice Collects the fees associated with provided liquidity /// @dev The contract must hold the erc721 token before it can collect fees /// @param tokenId The id of the erc721 token /// @return amount0 The amount of fees collected in token0 /// @return amount1 The amount of fees collected in token1 function collectAllFees(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) { // Caller must own the ERC721 position, meaning it must be a deposit
// set amount0Max and amount1Max to uint256.max to collect all fees // alternatively can set recipient to msg.sender and avoid another transaction in `sendToOwner` INonfungiblePositionManager.CollectParams memory params = INonfungiblePositionManager.CollectParams({ tokenId: tokenId, recipient: address(this), amount0Max: type(uint128).max, amount1Max: type(uint128).max });
(amount0, amount1) = nonfungiblePositionManager.collect(params);
// send collected feed back to owner _sendToOwner(tokenId, amount0, amount1); }
/// @notice A function that decreases the current liquidity by half. An example to show how to call the `decreaseLiquidity` function defined in periphery. /// @param tokenId The id of the erc721 token /// @return amount0 The amount received back in token0 /// @return amount1 The amount returned back in token1 function decreaseLiquidityInHalf(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) { // caller must be the owner of the NFT require(msg.sender == deposits[tokenId].owner, 'Not the owner'); // get liquidity data for tokenId uint128 liquidity = deposits[tokenId].liquidity; uint128 halfLiquidity = liquidity / 2;
// amount0Min and amount1Min are price slippage checks // if the amount received after burning is not greater than these minimums, transaction will fail INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager.DecreaseLiquidityParams({ tokenId: tokenId, liquidity: halfLiquidity, amount0Min: 0, amount1Min: 0, deadline: block.timestamp });
(amount0, amount1) = nonfungiblePositionManager.decreaseLiquidity(params);
//send liquidity back to owner _sendToOwner(tokenId, amount0, amount1); }
/// @notice Increases liquidity in the current range /// @dev Pool must be initialized already to add liquidity /// @param tokenId The id of the erc721 token /// @param amount0 The amount to add of token0 /// @param amount1 The amount to add of token1 function increaseLiquidityCurrentRange( uint256 tokenId, uint256 amountAdd0, uint256 amountAdd1 ) external returns ( uint128 liquidity, uint256 amount0, uint256 amount1 ) { TransferHelper.safeTransferFrom(deposits[tokenId].token0, msg.sender, address(this), amountAdd0); TransferHelper.safeTransferFrom(deposits[tokenId].token1, msg.sender, address(this), amountAdd1);
TransferHelper.safeApprove(deposits[tokenId].token0, address(nonfungiblePositionManager), amountAdd0); TransferHelper.safeApprove(deposits[tokenId].token1, address(nonfungiblePositionManager), amountAdd1);
INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager.IncreaseLiquidityParams({ tokenId: tokenId, amount0Desired: amountAdd0, amount1Desired: amountAdd1, amount0Min: 0, amount1Min: 0, deadline: block.timestamp });
(liquidity, amount0, amount1) = nonfungiblePositionManager.increaseLiquidity(params);
}
/// @notice Transfers funds to owner of NFT /// @param tokenId The id of the erc721 /// @param amount0 The amount of token0 /// @param amount1 The amount of token1 function _sendToOwner( uint256 tokenId, uint256 amount0, uint256 amount1 ) internal { // get owner of contract address owner = deposits[tokenId].owner;
address token0 = deposits[tokenId].token0; address token1 = deposits[tokenId].token1; // send collected fees to owner TransferHelper.safeTransfer(token0, owner, amount0); TransferHelper.safeTransfer(token1, owner, amount1); }
/// @notice Transfers the NFT to the owner /// @param tokenId The id of the erc721 function retrieveNFT(uint256 tokenId) external { // must be the owner of the NFT require(msg.sender == deposits[tokenId].owner, 'Not the owner'); // transfer ownership to original owner nonfungiblePositionManager.safeTransferFrom(address(this), msg.sender, tokenId); //remove information related to tokenId delete deposits[tokenId]; }}