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