Это вторая статья из серии перевода гайдов 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/, либо как минимум примечание,так как там есть информация по деплою этого всего добра. Так же настоятельно рекомендую побродить, потыкаться в исходники для понимания происходящего.

Комментарии (0)