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

ТЗ: Токен EUnitCoin (EUNIT)


Метаданные

ПараметрЗначение
Дата создания2026-07-06
Дата последнего изменения2026-07-06
Статус апрува✅ Одобрено
Дата апрува2026-07-06

Новый токен EUNIT — нетрансферабельный ERC20 в apps/contracts-v2. Пользователи получают EUNIT обменом UNIT 1:1 либо начислением платформой (минт без ограничения саплая). Тратить EUNIT могут только адреса из вайтлиста. Обратный обмен EUNIT → UNIT ограничен персональной квотой: адрес может вернуть не больше UNIT, чем сам вложил через обмен. EUNIT из других источников обменять на UNIT нельзя.


2.1. Пользовательские сценарии

  1. Обмен UNIT → EUNIT. Пользователь вызывает exchangeUnit(amount): его UNIT лочатся на балансе контракта EUNIT, взамен минтится amount EUNIT (1:1), квота обратного обмена растёт на amount.
  2. Обратный обмен EUNIT → UNIT. Пользователь вызывает exchangeBack(amount): EUNIT сжигаются, залоченные UNIT возвращаются. Работает только в пределах квоты unitExchanged[msg.sender].
  3. Начисление платформой. MINTER_ROLE минтит EUNIT пользователю (награды и т.п.) — такие EUNIT квоту обратного обмена не дают.
  4. Трата EUNIT. Переводы разрешены только по правилам TransferManager (по умолчанию — только адресам/адресами из вайтлиста, например платформенным контрактам).

2.2. Бизнес-логика

  • Курс фиксированный 1:1, decimals 18 (как у UNIT).
  • Квота — числовой счётчик на адрес, не пометка конкретных токенов. Трата EUNIT квоту не меняет: пользователь, вложивший 100 UNIT, может потратить полученные 100 EUNIT и позже обменять обратно любые другие 100 EUNIT (но не больше 100 суммарно).
  • Механика обмена — лок, не burn/mint. UNIT депонируются на балансе контракта EUnitCoin и возвращаются оттуда; резерв всегда покрывает обратный обмен, MINTER_ROLE на UnitCoin не требуется.
  • Минт EUNIT — без ограничения саплая.
  • amount == 0 в обеих функциях обмена — revert.

2.3. UI/UX требования

Не применимо (только смарт-контракт; интеграция фронтенда/бэкенда — вне области этого ТЗ).


3.1. Архитектура

Новый контракт apps/contracts-v2/contracts/tokens/EUnitCoin.sol по образцу UnitCoin.sol:

Initializable, ERC20Upgradeable, ERC20BurnableUpgradeable,
AccessControlUpgradeable, UUPSUpgradeable, TransferManager
  • Имя EUnit Coin, символ EUNIT.
  • Роли: OWNER_ROLE, MINTER_ROLE, TRANSFER_MANAGER_ROLE; модификатор onlyRoleOrOwner как в UnitCoin.
  • Функции обмена — внутри самого контракта EUNIT (требование), без отдельного контракта-обменника.
  • Ограничение трансферов — переиспользуем TransferManager один в один: transferEnabled = false по умолчанию, методы управления (whitelist/blacklist/enable/disable) как в других токенах.

3.2. Описание технической реализации

Хранение

IERC20Upgradeable public unitCoin;              // адрес UnitCoin
mapping(address => uint) public unitExchanged; // квота обратного обмена на адрес
uint public minted;
uint public burned;

initialize(address unitCoin) — сохраняет адрес UnitCoin, выдаёт DEFAULT_ADMIN_ROLE деплоеру.

exchangeUnit(uint amount)

  1. unitCoin.transferFrom(msg.sender, address(this), amount) — UNIT лочатся в контракте.
  2. _mint(msg.sender, amount) — EUNIT 1:1 (внутренний минт, minted += amount).
  3. unitExchanged[msg.sender] += amount.
  4. Событие UnitExchanged(user, amount).

exchangeBack(uint amount)

  1. require(unitExchanged[msg.sender] >= amount) — квота; баланс EUNIT проверяет сам _burn.
  2. unitExchanged[msg.sender] -= amount.
  3. Сжигание amount EUNIT у вызывающего в обход checkBurnAccess — прямой статический вызов ERC20Upgradeable._burn(msg.sender, amount) минует переопределённый _burn, поэтому обратный обмен не зависит от глобального burnEnabled и burn-вайтлиста; счётчик burned инкрементируется вручную.
  4. unitCoin.transfer(msg.sender, amount) — возврат UNIT. Проверка checkTransferAccess в UnitCoin проходит по ветке transferWhitelist[msg.sender], т.к. EUnitCoin добавлен в вайтлист UnitCoin (см. 4.1); ветка from == address(this) не применяется — это исключение для самого UnitCoin.
  5. Событие UnitExchangedBack(user, amount).

Трансферы и burn

_transfer / _burn переопределяются как в UnitCoin: checkTransferAccess(from) / checkBurnAccess(account). Запуск с transferEnabled = false, burnEnabled = false.

Деплой

  • Скрипт по образцу существующих в apps/contracts-v2/deploy/, UUPS-прокси.
  • pnpm copy-typechain после компиляции.

4. Проблемы и компромиссы

4.1. Известные ограничения

  1. EUnitCoin должен быть в transferWhitelist UnitCoin (обязательный шаг деплоя).
    • При exchangeUnit вызов unitCoin.transferFrom идёт с msg.sender == EUnitCoin; checkTransferAccess в UnitCoin пропускает перевод пользователя (не из вайтлиста UNIT, при выключенном transferEnabled) только если transferWhitelist[msg.sender] == true.
    • Без этого шага обмен не работает. Обратный перевод UNIT из контракта проходит по той же ветке.
  2. Квота привязана к адресу. Если вайтлист-адрес переместит EUNIT от пользователя A к пользователю B, у B не появится квоты на обратный обмен — это ожидаемое поведение.

4.2. Технический долг

  • Нет.

4.3. Риски

  • Забыли вайтлистнуть EUnitCoin в UnitCoin при деплое → обмен ревертится. Митигация: шаг зашит в деплой-скрипт + тест.

5. Вопросы на дополнительное обсуждение

  • Нет — ключевые вопросы (механика лока, семантика вайтлиста, счётчик квоты, курс 1:1) закрыты при проектировании.

6. План реализации

6.1. Этапы разработки

  • Этап 1: Контракт EUnitCoin.sol (TDD, по образцу UnitCoin)
  • Этап 2: Тесты test/EUnitCoin.test.ts
  • Этап 3: Деплой-скрипт + вайтлист EUnitCoin в UnitCoin
  • Этап 4: pnpm copy-typechain, обновление документации

6.2. Критические зависимости

  • Задеплоенный UnitCoin (уже существует); права TRANSFER_MANAGER_ROLE/админа на UnitCoin для вайтлиста.

8. Документация

  • Обновление Engineering docs (описание контракта EUnitCoin)

9. Ссылки

  • apps/contracts-v2/contracts/tokens/UnitCoin.sol — образец паттерна
  • apps/contracts-v2/contracts/tokens/common/TransferManager.sol — переиспользуемая логика вайтлистов

Тесты (критерии приёмки)

apps/contracts-v2/test/EUnitCoin.test.ts:

  1. Обмен туда: UNIT списываются и лочатся на контракте, EUNIT минтится 1:1, квота растёт, событие UnitExchanged.
  2. Обмен обратно: EUNIT сжигается, UNIT возвращается, квота уменьшается, событие UnitExchangedBack.
  3. Квота: нельзя вернуть больше, чем вложил; после полного возврата повторный exchangeBack ревертится; потраченные EUNIT не мешают вернуть другие в пределах квоты (вложил 100 → потратил 100 → получил другие 100 минтом → обменял обратно 100).
  4. Минт наградами: mint под MINTER_ROLE не увеличивает квоту → exchangeBack этих EUNIT ревертится.
  5. Трансферы: запрещены по умолчанию; разрешены из вайтлиста; blacklist работает; enableTransfer открывает всем.
  6. Обмен при выключенных флагах: exchangeBack работает при burnEnabled == false; exchangeUnit работает при вайтлистнутом EUnitCoin в UnitCoin и не работает без этого.
  7. Роли и апгрейд: mint без роли ревертится; _authorizeUpgrade только DEFAULT_ADMIN_ROLE; amount == 0 ревертится.