Uniswap v3 Providing Liquidity (Перевод гайда)

от автора

Это вторая статья из серии перевода гайдов Uniswap v3. Тут первая

В этом гайде мы рассмотрим пример контракта, который позволяет взаимодействовать с Periphery Uniswap V3 путем создания позиции и сбора комиссий.

Под Periphery Uniswap V3 подразумевается ряд контрактов написанных для простого и безопасного взаимодействия с core Uniswap V3.Они полезны но не обязательны,вы можете взаимодействовать с core Uniswap V3 напрямую или написать свою вариацию переферии

Сore Uniswap V3 — это ряд смарт-контрактов, необходимых для существования Uniswap. Обновление до новой версии ядра потребует переноса логики ликвидности.

Другие полезные термины можно найти тут.

Объявим версию Solidity, используемую для компиляции контракта, и abicoder v2, чтобы разрешить кодирование и декодирование произвольных вложенных массивов и структур в calldata ( функция, которую мы используем при работе с пулом).

// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; pragma abicoder v2;

Подгружаем необходимые пакетики пакетным менеджером***(на этом моменте стоит прочитать примечание)

import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";  import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol";

Создаем контракт с именем LiquidityExamples и наследуем от IERC721Receiver. Это позволит нашему контракту взаимодействовать с токенами IERC721

Для примера,адреса контрактов токенов (тут DAI и WETH9) и проценты платы за пул мы захардкодили. Очевидно,что контракт можно модифицировать так, чтобы изменять и пулы, и токены для каждой транзакции.

contract LiquidityExamples is IERC721Receiver {      address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;     address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;      uint24 public constant poolFee = 3000; 

Объявляем переменную nonfungiblePositionManager типа InonfungiblePositionManager(интерфейс относится к Periphery Uniswap V3 ) со следующими модификаторами immutable public.

(nonfungiblePositionManager по сути контракт обертка над Position,который из просто Position делает nft-шку)

 INonfungiblePositionManager public immutable nonfungiblePositionManager;

В этом моменте может возникнуть непонимание что к чему,какие позиции,какие менеджеры ????.Ничего страшного в этом нет.Лучше всего просто потыкаться по исходникам и посмотреть что вообще к чему,но небольшое пояснение я приведу.

«Позиция» олицетворяет промежуток в который мы кладем свои денюжки.Что-то вроде: «Вот тебе юнисвап родненький, мои 100$ и мои 100BYN,пользуйся на здоровье,но только при продаже одного доллара за 2-5 BYN,если же текущий курс не соответствует этому промежутку,положи мои гроши и не чапай «. А nonfungiblePositionManager делает из этой позиции нфтишку ,чтобы упростить с ней(позицией) взаимодействие и обеспечить проценты с вложенных во все это страшное дело средств.

Это было мое маленькое авторское отступление,вернемся к делу!

Каждый NFT имеет уникальнй АЙдишник uint256 внутри смарт-контракта ERC-721, объявленным как tokenId.

Чтобы разрешить депозит в наши волшебные токены ERC721,олицетворяющие ликвидность,мы создадим структуру Deposit.А так же, объявим мапу/словарь/сопоставление uint256 с нашей структуркой.Назовем переменную Deposits и доступ дадим всем всем всем.

struct Deposit {         address owner;         uint128 liquidity;         address token0;         address token1;  }  mapping(uint256 => Deposit) public deposits;

Конструктор

Здесь объявляем конструктор, он выполняется лишь однажды,когда контракт деплоится.В конструктор передаем адрес nonfungiblePositionManager. Адрес можно найти тут

    constructor(INonfungiblePositionManager _nonfungiblePositionManager) {         nonfungiblePositionManager = _nonfungiblePositionManager;     }

Хранение токенов ERC721 на контракте

Что бы разрешить контракту хранить токены ERC721, реализуйте функцию onERC721Received через наследование IERC721Receiver.sol .

Идентификатор from может быть опущен, поскольку он не используется.

Немого о onERC721Received:

Отправить нфтишку можно как на адрес пользователя,так и на адрес контракта,в случае если получатель токена контракт,то проходит проверка этого контракта на имплементирование им интерфейса ERC721Received,если это не так то транзакция отменяется.

В случае если все хорошо,то вызывается метод onERC721Received и там можно добавить кастомную логику. Например простейкать токен

    function onERC721Received(         address operator,         address,         uint256 tokenId,         bytes calldata     ) external override returns (bytes4) {         // get position information         _createDeposit(operator, tokenId);         return this.onERC721Received.selector;     }

Создание депозита

Чтобы добавить объект Deposit в мапу deposits,надо создать внутреннюю функцию _createDeposit,которая разбивает структуру positions функцией positions()из nonfungiblePositionManager.sol. и возвращает ее компоненты

Передаем необходимые нам переменные token0 token1 and liquidity в мапу 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

Чтобы создать новую позицию, мы используем nonFungiblePositionManager и вызываем mint.

Ради этого примера было жестко закодировано количество токенов, которое нужно отчеканить. В рабочей среде это будет настраиваемый пользователем аргумент функции.

/// @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

Тут мы даем отдобрение контракту nonfungiblePositionManager использовать токены нашего контракта, затем заполняем структуру MintParams и присваиваем ее локалььной переменной params,которая будет передана в nonfungiblePositionManager затем вызываем mint.

  • Используя TickMath.MIN_TICK and TickMath.MAX_TICK, мы устанавливаем ликвидность вдоль всего ценового ранжирования пула.В продакшене вы возможно захотите уточнить эти параменты.

  • Значения amount0Min и amount1Min равны в этом примере нулю — но в продакшене вам надо будет об этом позаботиться иначе у вас будут беды с проскальзыванием.

  • Обратите внимание, что эта функция не будет инициализировать пул, если он еще не существует.

 // 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 и рефинансирование вызывающего адресса

Теперь мы можем вызвать внутреннюю функцию,которую мы написали в Setting Up Your Contract. После этого мы можем взять любую ликвидность оставшуюся после выпуска и вернуть ее 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);         }     }

Сбор комиссии

Для каждого шага нашего примера наш контракт должен владеть NFTшками(которые выражают ликвидность). Так что NFTшки либо кондируем в код, либо предполагаем их наличие на контракте

Чтобы собрать комиссионные в качестве владельца позиции, передайте NFT с вызывающего адреса, назначьте соответствующие переменные из NFT локальным переменным в нашей функции и передайте эти переменные в thenonfungiblePositionManager для вызова сбора.

Эта функция собирает все сборы, отправляя их первоначальному владельцу NFT, сохраняя при этом позицию 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);     }  

Отправка сборов на вызывающий адрес

Эта внутренняя вспомогательная функция отправляет любые токены в виде сборов или токенов позиции владельцу NFT.

В _sendToOwner мы передаем сумму сборов, ранее заполненную в последней функции, в качестве аргументов для safeTransfer, которая переводит сборы владельцу.

Заключение

Я советую прочитать мою прошлую статью на тему юнисвапа https://habr.com/ru/post/684872/,либо как минимум примечание,так как там есть информация по деплою этого всего добра. Так же настоятельно рекомендую побродить,потыкаться в исходники для понимания происходящего


ссылка на оригинал статьи https://habr.com/ru/post/687694/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *