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

MagnetPaymentsManager

Обзор

MagnetPaymentsManager — контракт приема платежей. Пользователь оплачивает инвойс в нативной валюте сети или ERC20, а контракт форвардит оплату на адрес treasury.

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

  • upgradeable (UUPS);
  • защита от реентранси (ReentrancyGuardUpgradeable);
  • админские операции через AccessControlUpgradeable.

Роли и права

  • DEFAULT_ADMIN_ROLE:
    • настройка treasury и signatureValidator;
    • управление флагами tokenAllowed;
    • вывод средств, которые оказались на контракте (wthdraw);
    • апгрейд реализации (UUPS).

Состояние

  • signatureValidator — адрес, чьей подписью подтверждаются инвойсы.
  • treasury — адрес получателя оплат.
  • tokenAllowed[token] — конфигурационный флаг разрешенности токена.
  • usedSignatures[signature] — защита от повторного использования подписи.

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

  • Нет ончейн‑реестра инвойсов. Контракт не хранит статус/сумму инвойса; «истиной» является инвойс сгенерированный на бекенде и подпись signatureValidator.
  • Replay‑защита по подписи. Повторная оплата с тем же signature невозможна (usedSignatures).
  • Привязка к плательщику. В подписи участвует userAddress, поэтому подпись действительна только для конкретного отправителя (при корректной выдаче подписи бекендом).
  • Экспирация. Если invoiceExpiration > 0, то платеж возможен только пока block.timestamp < invoiceExpiration.
  • Переплата в native. При token == address(0) проверяется только msg.value >= amount; излишек остается на контракте и может быть выведен админом через wthdraw.
  • Ограничение по токенам (allowlist). pay проверяет tokenAllowed[token] и ревертится с TokenNotAllowed, если токен не разрешен.
    • В текущей реализации проверка выполняется до валидации подписи и распространяется также на token == address(0).
    • При этом setTokenAllowed запрещает token == address(0), поэтому оплата в нативной валюте фактически отключена (будет реверт TokenNotAllowed). Если нативные платежи нужны — это требует апгрейда контракта (например, проверять allowlist только для ERC20 или разрешить address(0) в конфиге).

Ошибки

InvalidAddress

Реверт при передаче нулевого адреса в админские методы.

InvalidSignature

Реверт при некорректной подписи инвойса.

InvoiceExpired

Реверт если invoiceExpiration > 0 и текущее время >= invoiceExpiration.

SignatureAlreadyUsed

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

InvalidAmount

Реверт если сумма некорректна (например, amount == 0 для вывода, либо msg.value < amount при оплате нативной валютой).

TokenNotAllowed

Реверт если tokenAllowed[token] == false (on-chain allowlist не разрешает токен оплаты).

События

TokenAllowedChanged

event TokenAllowedChanged(address indexed token, bool allowed);

Эмитится при изменении разрешенности токена админом.

SignatureValidatorChanged

event SignatureValidatorChanged(address indexed signatureValidator);

Эмитится при смене signatureValidator.

TreasuryChanged

event TreasuryChanged(address indexed treasury);

Эмитится при смене treasury.

Withdrawal

event Withdrawal(address indexed token, address indexed to, uint amount);

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

PaymentCompleted

event PaymentCompleted(string invoiceId, address indexed sender, address indexed token, uint amount);

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

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

initialize

function initialize() public initializer

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

Что делает:

  • выдает DEFAULT_ADMIN_ROLE деплоеру;
  • устанавливает signatureValidator = msg.sender;
  • устанавливает treasury = msg.sender.

_authorizeUpgrade

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

Назначение: ограничение апгрейда реализации UUPS.

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

setTokenAllowed

function setTokenAllowed(address token, bool allowed) external onlyRole(DEFAULT_ADMIN_ROLE)

Назначение: включить/выключить токен в конфиге.

setSignatureValidator

function setSignatureValidator(address _signatureValidator) external onlyRole(DEFAULT_ADMIN_ROLE)

Назначение: сменить адрес подписанта инвойсов.

setTreasury

function setTreasury(address _treasury) external onlyRole(DEFAULT_ADMIN_ROLE)

Назначение: сменить адрес получателя платежей.

wthdraw

function wthdraw(address token, address to, uint amount) external onlyRole(DEFAULT_ADMIN_ROLE)

Назначение: вывести средства, которые находятся на балансе контракта.

Когда нужно:

  • пользователь отправил нативную валюту с msg.value > amount (лишнее останется на контракте);
  • кто-то ошибочно перевел ERC20 напрямую на контракт;
  • другие нештатные сценарии накопления баланса.

Функции: оплата

pay

function pay(string calldata invoiceId, uint invoiceExpiration, address token, uint amount, bytes memory signature) external payable nonReentrant

Назначение: оплатить инвойс.

Параметры инвойса, участвующие в подписи:

  • userAddress (оплачивающий адрес);
  • invoiceId;
  • invoiceExpiration (0 = без экспирации);
  • token (address(0) = нативная валюта);
  • amount.

Алгоритм:

  1. Проверяет, что токен разрешен: require(tokenAllowed[token] || token == address(0), TokenNotAllowed()).
  2. Валидирует подпись инвойса (см. validateInvoiceSignature).
  3. Если token == address(0):
    • проверяет msg.value >= amount;
    • переводит amount на treasury.
  4. Иначе переводит amount ERC20 с пользователя на treasury через safeTransferFrom.
  5. Эмитит PaymentCompleted.

Примечания:

  • лишний msg.value (сверх amount) не возвращается автоматически;
  • для ERC20 пользователь должен предварительно сделать approve на контракт.

Функции: внутренняя логика

validateInvoiceSignature

function validateInvoiceSignature(address userAddress, string calldata invoiceId, uint invoiceExpiration, address token, uint amount, bytes memory signature) private

Назначение: проверить подпись инвойса и защититься от повторного использования.

Логика:

  • если invoiceExpiration > 0, проверяет срок действия;
  • проверяет, что signature еще не использовалась (usedSignatures);
  • строит хэш: keccak256(abi.encodePacked(userAddress, invoiceId, invoiceExpiration, token, amount));
  • применяет toEthSignedMessageHash() и recover(signature);
  • проверяет, что recoveredSigner == signatureValidator;
  • помечает подпись как использованную.

Связь с бекендом: модуль web3-payments

Связанный документ: apps/docs/docs/engineering/Сервисы/backend/web3-payments.md.

В web3-payments модуле инвойс создается в БД, а для оплаты пользователю выдается подпись и параметры транзакции. Далее фронтенд вызывает pay в смарт‑контракте платежей.

Как параметры из web3-payments маппятся на pay

  • invoiceId (из БД) → invoiceId.
  • invoiceExpiration (из ответа бекенда; 0 = без истечения) → invoiceExpiration.
  • tokenAddress (из инвойса; ZeroAddress для нативной валюты) → token.
  • amount (строка на бекенде) → amount (целое число в минимальных единицах токена, уже с decimals).
  • signature (подпись бекенда/валидатора) → signature.

Для оплаты нативной валютой фронтенд дополнительно передает value = amount (или больше, но это приведет к остаткам на контракте).

Сценарий выполнения (end-to-end)

  1. Создание инвойса на бекенде (любой продуктовый триггер: апгрейд NFT, покупка и т.д.) → пользователь получает invoiceId.
  2. Получение параметров оплаты через эндпоинт:
    • GET /web3-payments/invoice/signature-to-pay/:invoiceId.
  3. Оплата на фронтенде:
    • вызов pay(...) в MagnetPaymentsManager с параметрами из шага 2;
    • если token == address(0) — добавить value.
  4. Фиксация оплаты:
    • контракт эмитит PaymentCompleted(invoiceId, sender, token, amount);
    • крон check-payments.job.ts подтягивает события и вызывает handlePaymentCompleted.
  5. Применение бизнес‑действия на бекенде (в зависимости от InvoiceActions/invoice.data).

Рекомендации для интеграции

  • Формируйте amount на бекенде сразу в минимальных единицах (wei/decimals) и не пересчитывайте на фронте.
  • Всегда задавайте invoiceExpiration, чтобы ограничить окно оплаты.
  • Следите, чтобы signatureValidator на контракте совпадал с ключом, которым подписывает бекенд.
  • Настройте on-chain allowlist: перед тем как выпускать подписи на оплату в токене T, включите setTokenAllowed(T, true).