Это вторая статья из серии перевода гайдов 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_TICKandTickMath.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/
Добавить комментарий