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 depositsyou need to create an inner function _createDepositwhich 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 and TickMath.MAX_TICKwe set liquidity along the entire price range of the pool. In production, you may want to refine these parameters.

  • Values amount0Min and amount1Min 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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *