Uniswap v3 Providing Liquidity (Guide translation)
This is the second article in a series of translating Uniswap v3 guides. Here is the first
In this guide, we will look at an example contract that allows you to interact with Periphery Uniswap V3 by creating a position and collecting commissions.
Under Periphery Uniswap V3 implies a set of contracts written for simple and secure interaction with core Uniswap V3. They are useful but not required, you can interact with core Uniswap V3 directly or write your own variation of the periphery
Core Uniswap V3 is a set of smart contracts required for the existence of Uniswap. Upgrading to a newer kernel version will require moving the liquidity logic.
Other useful terms can be found here.
Let’s declare the version of Solidity used to compile the contract and abicoder v2 to allow encoding and decoding of arbitrary nested arrays and structures in calldata (a function we use when working with the pool).
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;
pragma abicoder v2;
We load the necessary packages with the package manager *** (at this point it is worth reading the note)
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol";
We create a contract named LiquidityExamples and inherit from IERC721Receiver. This will allow our contract to interact with IERC721 tokens
For example, the addresses of token contracts (here DAI and WETH9
) and pool fee percentages we hardcoded. Obviously, the contract can be modified to change both pools and tokens for each transaction.
contract LiquidityExamples is IERC721Receiver {
address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
uint24 public constant poolFee = 3000;
Declaring a variable nonfungiblePositionManager
type InonfungiblePositionManager
(interface refers to Periphery Uniswap V3 ) with the following immutable public modifiers.
(nonfungiblePositionManager is essentially a wrapper contract over Position, which makes an nft-shku from just Position)
INonfungiblePositionManager public immutable nonfungiblePositionManager;
At this moment, there may be a misunderstanding of what’s what, what positions, what managers 🤯🤯🤯🤯. There’s nothing wrong with that. It’s best to just poke around the source code and see what’s what, but I’ll give a little explanation.
“Position” personifies the gap in which we put our money. Something like: “Here’s a dear uniswap for you, my 100$ and my 100BYN, use it to your health, but only when selling one dollar for 2-5 BYN, if the current rate does not correspond to this gap, put my pennies and do not chapai “. BUT nonfungiblePositionManager
makes this position nftishka to simplify interaction with it (position) and provide interest on the funds invested in all this terrible business.
This was my little author’s digression, back to business!
Each NFT has a unique ID uint256 inside the ERC-721 smart contract declared as tokenId.
To allow a deposit in our magical ERC721 tokens representing liquidity, we will create a Deposit structure. Also, we will declare a map / dictionary / uint256 mapping with our structure. Let’s call the variable Deposits and give access to everyone to everyone.
struct Deposit {
address owner;
uint128 liquidity;
address token0;
address token1;
}
mapping(uint256 => Deposit) public deposits;
Constructor
Here we declare the constructor, it is executed only once, when the contract is deployed. We pass the address to the constructor nonfungiblePositionManager.
Address can be found here
constructor(INonfungiblePositionManager _nonfungiblePositionManager) {
nonfungiblePositionManager = _nonfungiblePositionManager;
}
Storing ERC721 tokens on a contract
To allow a contract to store ERC721 tokens, implement the function onERC721Received
through inheritance IERC721Receiver.sol
.
The from identifier can be omitted as it is not used.
Silent about onERC721Received:
You can send a nftishka both to the user’s address and to the address of the contract, if the recipient of the token is a contract, then this contract is checked for the implementation of the ERC721Received interface, if this is not the case, the transaction is canceled.
If all is well, then the method is called onERC721Received
and there you can add custom logic. For example, stake a token
function onERC721Received(
address operator,
address,
uint256 tokenId,
bytes calldata
) external override returns (bytes4) {
// get position information
_createDeposit(operator, tokenId);
return this.onERC721Received.selector;
}
Creating a deposit
To add an object Deposit
to mapa deposits
you need to create an inner function _createDeposit
which breaks the structure positions
function positions()
from nonfungiblePositionManager.sol.
and returns its components
We pass the variables we need token0
token1
and liquidity
to mapa deposits
.
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});
}
Mint a New Position
To create a new position, we use nonFungiblePositionManager and call mint.
For the sake of this example, the number of tokens to be minted has been hardcoded. In production, this will be a user-configurable function argument.
/// @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;
Calling Mint
Here we give approval to the contract nonfungiblePositionManager
use the tokens of our contract, then fill in the structure MintParams
and assign it to a local variable params,
which will be transferred to nonfungiblePositionManager
then we call mint
.
Using
TickMath.MIN_TICK
andTickMath.MAX_TICK
we set liquidity along the entire price range of the pool. In production, you may want to refine these parameters.Values
amount0Min
andamount1Min
are zero in this example – but in production you will have to take care of this otherwise you will have slippage troubles.Note that this function will not initialize the pool if it doesn’t already exist.
// 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);
Deposit Mapping update and calling address refinancing
Now we can call the inner function we wrote in Setting Up Your Contract. After that, we can take any liquidity left after the issue and return it msg.sender
.
// 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);
}
}
Commission collection
For each step of our example, our contract must hold NFTs (which represent liquidity). So we either condense the NFTs into the code, or we assume their presence on the contract
To collect commissions as a position owner, pass the NFT from the calling address, assign the appropriate variables from the NFT to local variables in our function, and pass those variables to thenonfungiblePositionManager to call the collection.
This feature collects all fees by sending them to the original NFT holder while maintaining the position of the NFT.
/// @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
// Call to safeTransfer will trigger `onERC721Received` which must return the selector else transfer will fail
nonfungiblePositionManager.safeTransferFrom(msg.sender, address(this), tokenId);
// 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);
}
Sending fees to the calling address
This internal helper function sends any tokens in the form of fees or position tokens to the owner of the NFT.
In _sendToOwner , we pass the amount of fees previously filled in the last function as arguments to safeTransfer, which transfers the fees to the owner.
Conclusion
I advise you to read my last article on the uniswap https://habr.com/ru/post/684872/, or at least a note, since there is information on deploying all this stuff. I also strongly recommend wandering, poking into the source to understand what is happening