ТЗ: Токен 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. Пользовательские сценарии
- Обмен UNIT → EUNIT. Пользователь вызывает
exchangeUnit(amount): его UNIT лочатся на балансе контракта EUNIT, взамен минтитсяamountEUNIT (1:1), квота обратного обмена растёт наamount. - Обратный обмен EUNIT → UNIT. Пользователь вызывает
exchangeBack(amount): EUNIT сжигаются, залоченные UNIT возвращаются. Работает только в пределах квотыunitExchanged[msg.sender]. - Начисление платформой.
MINTER_ROLEминтит EUNIT пользователю (награды и т.п.) — такие EUNIT квоту обратного обмена не дают. - Трата 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)
unitCoin.transferFrom(msg.sender, address(this), amount)— UNIT лочатся в контракте._mint(msg.sender, amount)— EUNIT 1:1 (внутренний минт,minted += amount).unitExchanged[msg.sender] += amount.- Событие
UnitExchanged(user, amount).
exchangeBack(uint amount)
require(unitExchanged[msg.sender] >= amount)— квота; баланс EUNIT проверяет сам_burn.unitExchanged[msg.sender] -= amount.- Сжигание
amountEUNIT у вызывающего в обходcheckBurnAccess— прямой статический вызовERC20Upgradeable._burn(msg.sender, amount)минует переопределённый_burn, поэтому обратный обмен не зависит от глобальногоburnEnabledи burn-вайтлиста; счётчикburnedинкрементируется вручную. unitCoin.transfer(msg.sender, amount)— возврат UNIT. ПроверкаcheckTransferAccessв UnitCoin проходит по веткеtransferWhitelist[msg.sender], т.к. EUnitCoin добавлен в вайтлист UnitCoin (см. 4.1); веткаfrom == address(this)не применяется — это исключение для самого UnitCoin.- Событие
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. Известные ограничения
- EUnitCoin должен быть в
transferWhitelistUnitCoin (обязательный шаг деплоя).- При
exchangeUnitвызовunitCoin.transferFromидёт сmsg.sender == EUnitCoin;checkTransferAccessв UnitCoin пропускает перевод пользователя (не из вайтлиста UNIT, при выключенномtransferEnabled) только еслиtransferWhitelist[msg.sender] == true. - Без этого шага обмен не работает. Обратный перевод UNIT из контракта проходит по той же ветке.
- При
- Квота привязана к адресу. Если вайтлист-адрес переместит 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:
- Обмен туда: UNIT списываются и лочатся на контракте, EUNIT минтится 1:1, квота растёт, событие
UnitExchanged. - Обмен обратно: EUNIT сжигается, UNIT возвращается, квота уменьшается, событие
UnitExchangedBack. - Квота: нельзя вернуть больше, чем вложил; после полного возврата повторный
exchangeBackревертится; потраченные EUNIT не мешают вернуть другие в пределах квоты (вложил 100 → потратил 100 → получил другие 100 минтом → обменял обратно 100). - Минт наградами:
mintподMINTER_ROLEне увеличивает квоту →exchangeBackэтих EUNIT ревертится. - Трансферы: запрещены по умолчанию; разрешены из вайтлиста; blacklist работает;
enableTransferоткрывает всем. - Обмен при выключенных флагах:
exchangeBackработает приburnEnabled == false;exchangeUnitработает при вайтлистнутом EUnitCoin в UnitCoin и не работает без этого. - Роли и апгрейд:
mintбез роли ревертится;_authorizeUpgradeтолькоDEFAULT_ADMIN_ROLE;amount == 0ревертится.