Перейти к основному содержимому

MagnetIndexManager

Обзор

MagnetIndexManager — контракт-менеджер пользовательских Uniswap V3 позиций, реализованный как ERC721 NFT контракт. Он выступает прослойкой между пользователем и NonfungiblePositionManager, токенизирует владение позициями через собственные NFT (MIDX) и ограничивает работу только с заранее одобренными пулами.

Ключевые свойства:

  • upgradeable (UUPS);
  • ERC721 NFT (каждая позиция представлена уникальным токеном);
  • защита от реентранси (ReentrancyGuardUpgradeable);
  • админские операции через AccessControlUpgradeable;
  • whitelist пулов Uniswap V3;
  • per-user opt-in на управление позициями оператором + поддержка стандартных ERC721 approvals;
  • распределение rewards по партнерским кошелькам через backend-signed список при claimFees и closePosition.

Контекст и зависимости

  • Uniswap V3 NonfungiblePositionManager — основной внешний контракт, через который создаются, увеличиваются, уменьшаются, клеймятся и закрываются позиции.
  • ERC20 токены пула — пользователь должен предварительно сделать approve для MagnetIndexManager.
  • Бэкенд / off-chain validator — подписывает разрешенный список партнерских кошельков для claimFees и closePosition.

Контракт не выполняет свапы и не подбирает диапазон тиков автоматически. Он управляет уже заданными пользователем позициями.

Роли и права

  • DEFAULT_ADMIN_ROLE:

    • управление whitelist пулов;
    • настройка процентов партнерского распределения;
    • апгрейд реализации (UUPS);
    • выдача ролей POSITION_OPERATOR и SIGNATURE_VALIDATOR.
  • POSITION_OPERATOR:

    • может управлять позицией пользователя только если пользователь явно включил это через setUserOperatorEnabled(true).
  • SIGNATURE_VALIDATOR:

    • роль для адресов, чьей EIP-712 подписью авторизуется список партнеров в claimFees и closePosition.

Состояние

  • DOMAIN_SEPARATOR — EIP-712 domain separator для проверки подписей claimFees.
  • whitelistedPools[poolKey] — разрешен ли пул для создания новых позиций.
  • positionPools[tokenId] — конфигурация пула (npm, token0, token1, fee) для позиции.
  • positionLiquidity[tokenId] — последняя известная ликвидность позиции.
  • userOperatorEnabled[user] — дал ли пользователь право операторам Magnet управлять его позициями.
  • userNonces[user] — nonce для replay-protection в claimFees и closePosition.
  • partnerPercents — массив процентов в basis points для партнерского распределения.

Ограничения и безопасность

  • Создание только в whitelist пулах. Новую позицию можно создать только если admin заранее разрешил соответствующую комбинацию npm + token0 + token1 + fee.
  • NFT-владение. При создании позиции пользователь получает NFT (MIDX) с ID, равным ID позиции в Uniswap V3. Владение этой NFT дает право на управление позицией и получение средств.
  • Порядок токенов. Для конфигурации пула требуется token0 < token1, как и в Uniswap V3.
  • Operator access opt-in & ERC721 Approvals. Управлять позицией может:
    • владелец NFT;
    • оператор с ролью POSITION_OPERATOR, если владелец включил userOperatorEnabled;
    • адрес, имеющий стандартный approve или setApprovalForAll на данный NFT.
  • Claim/close only with signed partners list. Распределение партнерки выполняется только если список партнерских кошельков подписан адресом с ролью SIGNATURE_VALIDATOR.
  • Replay-защита. claimFees и closePosition используют userNonces[user]; одна и та же подпись не может быть использована повторно.
  • Соответствие массивов. Для claimFees и closePosition длина partners должна строго совпадать с длиной partnerPercents.
  • Нет встроенного referral registry. Контракт не знает, кто является партнером пользователя; он доверяет только подписанному списку кошельков.
  • Частичный withdraw не должен выводить claim fees в обход партнерки. При decreaseLiquidity контракт собирает только amounts, возвращенные из decreaseLiquidity, а накопленные fees остаются для последующего claimFees.
  • Закрытие позиции требует подписанный список партнеров. closePosition может materialize накопленные fees, поэтому он также проверяет backend-подпись партнерского списка и распределяет fee-часть по партнерке.

Ошибки

IndexAccessDenied

Реверт если вызывающий не является владельцем позиции и не имеет допустимого операторского доступа.

IndexDeadlineExpired

Реверт если операция вызвана после deadline.

IndexInvalidLiquidity

Реверт если liquidity равна нулю, превышает доступную или если mint/increase не создали ликвидность.

IndexInvalidPartners

Реверт если:

  • сумма partnerPercents больше 100%;
  • длина массива партнеров не совпадает с длиной массива процентов;
  • один из партнерских адресов равен address(0).

IndexInvalidPool

Реверт если pool config содержит нулевые адреса.

IndexInvalidSignature

Реверт если подпись claimFees/closePosition невалидна, nonce не совпадает или подпись уже была использована.

IndexInvalidTokenOrder

Реверт если token0 >= token1.

IndexPositionNotFound

Реверт если позиция с таким tokenId не существует (не сминтили NFT).

IndexPoolNotWhitelisted

Реверт если пользователь пытается создать позицию в неразрешенном пуле.

Структуры

IndexPoolParams

Описание пула Uniswap V3:

  • npm — адрес NonfungiblePositionManager;
  • token0, token1 — токены пула;
  • fee — fee tier пула.

IndexCreatePositionParams

Параметры для создания позиции:

  • конфиг пула;
  • tickLower, tickUpper;
  • желаемые amounts и min amounts;
  • deadline.

IndexIncreaseLiquidityParams

Параметры добавления ликвидности:

  • tokenId;
  • amount0Desired, amount1Desired;
  • amount0Min, amount1Min;
  • deadline.

IndexDecreaseLiquidityParams

Параметры вывода ликвидности:

  • tokenId;
  • liquidity;
  • amount0Min, amount1Min;
  • deadline.

IndexClaimFeesParams

Параметры клейма комиссий:

  • tokenId;
  • partners — список партнерских кошельков;
  • nonce;
  • deadline;
  • signature — EIP-712 подпись от SIGNATURE_VALIDATOR.

IndexClosePositionParams

Параметры полного закрытия позиции:

  • tokenId;
  • amount0Min, amount1Min;
  • partners — список партнерских кошельков для fee-части;
  • nonce;
  • deadline;
  • signature — EIP-712 подпись от SIGNATURE_VALIDATOR.

UserPositionInfo

Информация о позиции пользователя:

  • tokenId — ID позиции в Uniswap V3;
  • owner — владелец позиции;
  • pool — конфигурация пула (IndexPoolParams);
  • liquidity — текущая ликвидность позиции.

События

PoolWhitelistUpdated

event PoolWhitelistUpdated(bytes32 indexed poolKey, bool whitelisted);

Эмитится при изменении whitelist статуса пула.

PartnerPercentsUpdated

event PartnerPercentsUpdated(uint16[] percents);

Эмитится при обновлении процентов партнерского распределения.

UserOperatorEnabled

event UserOperatorEnabled(address indexed user, bool enabled);

Эмитится когда пользователь включает или выключает управление от кошельков Magnet.

ProductMinted

event ProductMinted(address indexed user, uint256 indexed tokenId, bytes objectId, bool transferProhibited, bool isRootToken, uint256 level);

Эмитится при минте NFT позиции. Используется для индексации позиций. Поля objectId, transferProhibited, isRootToken и level в данном контракте являются вспомогательными и передаются как значения по умолчанию ("", false, false, 0).

PositionCreated

event PositionCreated(uint256 indexed tokenId, address indexed owner, bytes32 indexed poolKey, uint128 liquidity, uint256 amount0, uint256 amount1);

Эмитится при успешном mint новой позиции.

PositionLiquidityIncreased

event PositionLiquidityIncreased(uint256 indexed tokenId, address indexed owner, uint128 liquidity, uint256 amount0, uint256 amount1);

Эмитится при успешном добавлении ликвидности.

PositionLiquidityDecreased

event PositionLiquidityDecreased(uint256 indexed tokenId, address indexed owner, uint128 liquidity, uint256 amount0, uint256 amount1);

Эмитится при частичном выводе ликвидности.

PositionClosed

event PositionClosed(uint256 indexed tokenId, address indexed owner, uint256 amount0, uint256 amount1);

Эмитится при полном закрытии позиции и удалении ее из менеджера.

Поля amount0 и amount1 — итоговые суммы, отправленные владельцу позиции после распределения fee-части по партнерам.

FeesClaimed

event FeesClaimed(uint256 indexed tokenId, address indexed owner, uint256 grossAmount0, uint256 grossAmount1, uint256 netAmount0, uint256 netAmount1);

Эмитится при клейме комиссий после партнерского распределения.

Поля:

  • grossAmount0, grossAmount1 — reward/fee-часть, с которой считается партнерское распределение;
  • netAmount0, netAmount1 — итоговые суммы, отправленные владельцу позиции. Если при claimFees был сожжен 1 liquidity для materialize fees, principal от этого burn добавляется к netAmount.

Функции: инициализация и апгрейд

initialize

function initialize(address signatureValidator) external initializer

Назначение: инициализация upgradeable-контракта.

Что делает:

  • инициализирует ERC721 ("Magnet Index", "MIDX");
  • инициализирует AccessControl, ReentrancyGuard, UUPS;
  • выдает DEFAULT_ADMIN_ROLE деплоеру;
  • при ненулевом адресе выдает SIGNATURE_VALIDATOR указанному валидатору;
  • вычисляет DOMAIN_SEPARATOR.

_authorizeUpgrade

function _authorizeUpgrade(address newImplementation) internal view override onlyRole(DEFAULT_ADMIN_ROLE)

Назначение: ограничивает UUPS-апгрейд только администраторами.

Функции: чтение

getPartnerPercents

function getPartnerPercents() external view returns (uint16[] memory)

Возвращает текущий массив процентов партнерского распределения.

getPoolKey

function getPoolKey(IndexPoolParams memory pool) public pure returns (bytes32)

Возвращает детерминированный poolKey = keccak256(abi.encode(npm, token0, token1, fee)).

Используется:

  • для whitelist пула;
  • для off-chain идентификации конфигурации.

getUserPositions

function getUserPositions(address user) external view returns (UserPositionInfo[] memory)

Возвращает массив всех активных позиций указанного пользователя. Для каждой позиции возвращается полная информация: ID, текущий владелец (из ERC721), параметры пула и объем ликвидности.

Особенности:

  • использует стандарт ERC721Enumerable для получения списка токенов пользователя.

tokensOfOwner

function tokensOfOwner(address _owner) external view returns (uint256[] memory)

Возвращает массив всех tokenId, которыми владеет указанный адрес.

getPendingFees

function getPendingFees(uint256 tokenId) external view returns (uint256 amount0, uint256 amount1)

Возвращает накопленные (незаклеймленные) комиссии позиции. Данные берутся из полей tokensOwed0 и tokensOwed1 контракта Uniswap NPM. Комиссии обновляются при любом взаимодействии с позицией (добавление/вывод ликвидности).

Функции: администрирование

setWhitelistedPool

function setWhitelistedPool(IndexPoolParams calldata pool, bool whitelisted) external onlyRole(DEFAULT_ADMIN_ROLE)

Назначение: включить или выключить пул в whitelist.

Проверки:

  • npm, token0, token1 не должны быть нулевыми;
  • token0 < token1.

setPartnerPercents

function setPartnerPercents(uint16[] calldata percents) external onlyRole(DEFAULT_ADMIN_ROLE)

Назначение: настроить проценты распределения партнерских rewards.

Правила:

  • порядок процентов соответствует порядку кошельков из partners в claimFees;
  • сумма процентов не должна превышать 10000 basis points.

Функции: user settings

setUserOperatorEnabled

function setUserOperatorEnabled(bool enabled) external

Назначение: пользователь включает или выключает возможность управления его позициями со стороны кошельков Magnet с ролью POSITION_OPERATOR.

Функции: управление позициями

createPosition

function createPosition(IndexCreatePositionParams calldata params) external nonReentrant returns (uint256 tokenId, uint128 liquidity)

Назначение: создать новую позицию Uniswap V3.

Алгоритм:

  1. Проверяет deadline.
  2. Проверяет корректность пула и наличие его в whitelist.
  3. Забирает token0 и token1 у пользователя.
  4. Выдает approvals на NonfungiblePositionManager.
  5. Вызывает mint.
  6. Минтит NFT (_safeMint) на адрес пользователя и эмитит ProductMinted.
  7. Сохраняет pool config и liquidity.
  8. Возвращает пользователю неиспользованный остаток токенов.

Важно:

  • Владение позицией теперь определяется владением NFT на контракте MagnetIndexManager. Передача NFT другому адресу передает полные права на управление позицией и получение средств.

increaseLiquidity

function increaseLiquidity(IndexIncreaseLiquidityParams calldata params) external nonReentrant returns (uint128 liquidity)

Назначение: добавить ликвидность в существующую позицию.

Кто может вызвать:

  • владелец позиции;
  • оператор с ролью POSITION_OPERATOR, если владелец включил userOperatorEnabled.

Особенности:

  • токены списываются с owner позиции, а не с msg.sender;
  • неиспользованный остаток возвращается owner.

decreaseLiquidity

function decreaseLiquidity(IndexDecreaseLiquidityParams calldata params) external nonReentrant returns (uint256 amount0, uint256 amount1)

Назначение: частично вывести ликвидность из позиции.

Поведение:

  • уменьшает positionLiquidity;
  • собирает только amounts, соответствующие именно операции decreaseLiquidity;
  • переводит withdrawn amounts владельцу позиции.

Важно:

  • накопленные fees не распределяются через эту функцию и не уходят пользователю в обход партнерки.

closePosition

function closePosition(IndexClosePositionParams calldata params) external nonReentrant returns (uint256 amount0, uint256 amount1)

Назначение: полностью закрыть позицию.

Алгоритм:

  1. Проверяет доступ и deadline.
  2. Проверяет, что длина partners совпадает с partnerPercents.
  3. Проверяет EIP-712 подпись ClosePartners(address user,uint256 tokenId,bytes32 partnersHash,uint256 nonce,uint256 deadline).
  4. Если в позиции есть ликвидность — вызывает decreaseLiquidity на полный объем.
  5. Собирает все owed amounts.
  6. Отделяет principal от fee-части.
  7. Principal переводит владельцу, fee-часть распределяет между партнерами и владельцем.
  8. Сжигает NFT позиции в NonfungiblePositionManager.
  9. Сжигает NFT (_burn) контракта MagnetIndexManager.
  10. Удаляет локальное состояние позиции из менеджера.

Важно:

  • closePosition инкрементирует userNonces[owner], как и claimFees;
  • fee-часть при закрытии позиции не может быть выведена в обход партнерского распределения.

claimFees

function claimFees(IndexClaimFeesParams calldata params) external nonReentrant returns (uint256 netAmount0, uint256 netAmount1)

Назначение: клейм накопленных fees с распределением по партнерским кошелькам.

Ключевые проверки:

  • deadline не истек;
  • caller имеет право управлять позицией;
  • длина partners совпадает с длиной partnerPercents;
  • подпись валидна и подписана адресом с ролью SIGNATURE_VALIDATOR;
  • nonce совпадает с userNonces[positionOwner].

Алгоритм:

  1. Верифицирует EIP-712 подпись ClaimPartners(address user,uint256 tokenId,bytes32 partnersHash,uint256 nonce,uint256 deadline).
  2. Если у позиции есть ликвидность — вызывает decreaseLiquidity(..., liquidity: 1) для materialize fees в tokensOwed.
  3. Собирает все клеймируемые amounts через collect.
  4. Отделяет principal от fee-части, полученной из burn 1 liquidity.
  5. Для каждого токена распределяет только fee-часть по partners.
  6. Principal и остаток fee-части переводит владельцу позиции.
  7. Инкрементирует userNonces[owner].

Важно:

  • partners — это не on-chain вычисляемая структура, а backend-authorized список кошельков;
  • перестановка адресов в массиве делает подпись невалидной;
  • reuse той же подписи невозможен.

Внутренняя логика

_requireCanManage

Проверяет право на управление позицией.

Разрешенные сценарии:

  • msg.sender == ownerOf(tokenId);
  • msg.sender имеет POSITION_OPERATOR и userOperatorEnabled[owner] == true;
  • msg.sender имеет getApproved(tokenId) или isApprovedForAll(owner, msg.sender).

_verifyPartnersSignature

Проверяет EIP-712 подпись для claimFees или closePosition и инкрементирует nonce владельца.

Используемые typehash:

  • CLAIM_PARTNERS_TYPEHASH — для claimFees;
  • CLOSE_PARTNERS_TYPEHASH — для closePosition.

_distributeCollected

Отделяет principal от collected amounts и распределяет только reward/fee-часть по partnerPercents.

Используется:

  • в claimFees, где principal появляется из burn 1 liquidity;
  • в closePosition, где principal — полный withdraw позиции, а остаток collected amounts считается fee-частью.

_distribute

Распределяет amount токена между партнерами по partnerPercents, остаток переводит владельцу.

_collect / _collectAmount

Обертки над collect в NonfungiblePositionManager:

  • _collect — собирает максимум;
  • _collectAmount — собирает ограниченный объем, соответствующий withdraw amounts.

_validatePool

Проверяет:

  • ненулевые адреса;
  • корректный порядок токенов (token0 < token1).

Типовой сценарий использования

  1. Админ деплоит прокси и вызывает initialize.
  2. Админ выдает роли POSITION_OPERATOR и SIGNATURE_VALIDATOR.
  3. Админ whitelist'ит нужные Uniswap V3 пулы через setWhitelistedPool.
  4. Админ задает partnerPercents.
  5. Пользователь делает approve обоих токенов пула на MagnetIndexManager.
  6. Пользователь создает позицию через createPosition.
  7. Пользователь:
    • сам управляет позицией, либо
    • включает setUserOperatorEnabled(true), если хочет разрешить операторское управление.
  8. Для claimFees backend формирует список партнерских кошельков и подписывает его адресом с ролью SIGNATURE_VALIDATOR.
  9. Пользователь или оператор вызывает claimFees, rewards распределяются между партнерами и owner.
  10. Для полного закрытия позиции backend подписывает ClosePartners, после чего пользователь или оператор вызывает closePosition.