Рабочий Hardhat-проект: контракты, тесты, скрипт деплоя и сквозной сценарий.
- Сеть: BNB Smart Chain
- Токен: BTOWN, BEP-20 / ERC-20, фиксированная эмиссия 300 000 000, минт только в конструкторе
- Solidity 0.8.28, optimizer runs 200, viaIR, evmVersion cancun
- OpenZeppelin Contracts 5.2, Chainlink VRF v2.5
- Независимого аудита человеческой командой нет. Деплой в mainnet требует явного
CONFIRM_MAINNET=56
BattleTownPresale.sol продаёт BTOWN за стейблкоин по десяти стадиям вайтпейпера: публичная серия
100M и корпоративная 20M, цены и объёмы — из BattleTownConstants.sol. Дат у стадий нет: стадия
переключается распродажей объёма или вручную владельцем (advanceStage); остаток покинутой стадии
не переносится и уходит в хранилище наград при финализации. Стейблкоин уходит в казну той же
транзакцией, что и покупка, — на пресейле он не задерживается.
Купленные токены идут одним из двух путей:
- грант в вестинг — корпоративная серия всегда (
CORPORATE), публичная — пока владелец держит включённым тумблерpublicVestingEnabled(PUBLIC); - лок на пресейле — публичная покупка при выключенном тумблере. Лок забирается одним
claim()после наступления TGE; дату TGE пресейл читает из вестинга, своей у него нет.
Режим фиксируется в момент покупки: переключение тумблера не трогает уже сделанные локи и гранты.
Покупателю режим не предлагается, бонуса за вестинг нет. Функции покупки принимают только
btownAmount.
KYC и офчейн-реестр остаются офчейн: пакеты грантов по-прежнему заводит грантер через
createGrants, а пресейл — ещё один грантер, тратящий собственный инвентарь.
- §7, вестинг публичного пресейла. Вайтпейпер обещает вестинг публичной серии; в контракте он включается и выключается глобальным тумблером владельца. При выключенном тумблере покупка — лок до TGE, целиком.
- §8, soft cap и возвраты. Не реализованы. Стейблкоин уходит в казну немедленно, функции возврата у пресейла нет. Дедлайн 30 июня 2027 остаётся жёстким стопом на покупки и на запуск продажи.
- Непроданный остаток после закрытия уходит в
BattleTownRewardsVault.sol(§9) — это делает permissionless-finalize.
деплой → vesting.setGranter(presale, true) → перевод 120M на пресейл → startSale (владелец)
→ покупки (тумблер и стадии по ходу) → closeSale → finalize (непроданное → хранилище)
→ vesting.finalizeDistribution → vesting.setTge → клеймы локов и грантов
finalizeDistribution закрывает грантовый путь (покупка с вестингом ревертит DistributionClosed
из вестинга), setTge закрывает все покупки (TgeAlreadySet из пресейла). Сноска:
finalizeDistribution ревертит NoGrantsToFinalize, если грантов не было ни одного, — при продаже
одними локами для фиксации TGE нужен хотя бы один грант, обычно первый team-грант.
| Контракт | Ответственность |
|---|---|
BattleTownConstants.sol |
Числовой канон: корзины, стадии пресейла, графики вестинга, NFT-таблицы, призы, комиссии. Библиотека internal-функций, не деплоится |
BattleTownToken.sol |
BTOWN: чистый ERC-20, 300M одному получателю в конструкторе. Ролей, пауз, минта и гейта переводов нет |
BattleTownPresale.sol |
Продажа BTOWN по стадиям вайтпейпера: стейблкоин сразу в казну, токены — грантом в вестинг или локом до TGE |
BattleTownTank.sol |
ERC-721 на 2 046 танков, ранг 1–10 иммутабелен. Единственный минтер — контракт продажи |
BattleTownTankSale.sol |
Продажа танков за стейблкоин по фиксированной таблице цен, выручка сразу в казну |
BattleTownVesting.sol |
Гранты PUBLIC / CORPORATE / TEAM, pull-фондирование, одноразовая фиксация TGE |
BattleTownTournament.sol |
Эскроу и выплаты официальных и community-турниров, Chainlink VRF |
BattleTownNftRewards.sol |
Модуль эпох выплат держателям NFT, наследуется турниром (отдельно не деплоится) |
BattleTownRewardsVault.sol |
Хранилище пула Mining / Rewards и непроданного пресейла, единственный выход — зарегистрированный турнир |
BattleTownTankLens.sol |
Read-only периферия: держатели танков с пагинацией |
BattleTownTournamentLens.sol |
Read-only периферия: эпохи и расчёт выплат батчем |
flowchart LR
Token[BattleTownToken 300M] -->|депозит пула| Vault[BattleTownRewardsVault]
Token -->|инвентарь 120M| Presale[BattleTownPresale]
Token -->|пакеты грантов| Vesting[BattleTownVesting]
Presale -->|гранты покупателей| Vesting
Presale -->|стейблкоин| Treasury[Treasury]
Presale -->|непроданное после finalize| Vault
Presale -->|claim лока после TGE| Buyers[Покупатели и команда]
Vault -->|fundTournament| Tournament[BattleTownTournament + NftRewards]
Sale[BattleTownTankSale] -->|mint| Tank[BattleTownTank]
Sale -->|стейблкоин| Treasury
Tank -->|снапшот рангов при создании турнира| Tournament
Tournament -->|призы| Players[Игроки]
Tournament -->|эпохи выплат| Holders[Держатели танков]
Tournament -->|комиссии и остатки| Treasury
Vesting -->|claim после TGE| Buyers
Токен минтит всю эмиссию одному получателю. Ончейн из корзин вайтпейпера закреплены две: пул Mining / Rewards, который скрипт деплоя кладёт в хранилище, и инвентарь пресейла 120M, который скрипт переводит на контракт продажи.
| Корзина | Количество | Где живёт |
|---|---|---|
| Public Presale | 100M | инвентарь BattleTownPresale.sol; непроданное после finalize — в хранилище наград |
| Corporate Presale | 20M | там же |
| Mining / Rewards | 80M | депозит в BattleTownRewardsVault.sol при деплое |
| Team | 20M | у получателя генезиса, потолок TEAM-грантов проверяет вестинг |
| DEX Liquidity | 30M | кошельки проекта |
| Staking / Ecosystem / Marketing / Bounty / Launchpool | 50M | кошельки проекта |
| Итого | 300M |
| Категория | На TGE | Cliff | Дальше | Полностью |
|---|---|---|---|---|
| Public | 10% | 1 месяц | шаги по 10% | месяц 10 |
| Corporate | 0% | 6 месяцев | шаги по 10% | месяц 16 |
| Team | 0% | 12 месяцев | шаги по 10% | месяц 22 |
Между годовщинами разблокировка линейна. Отсчёт идёт от max(TGE, дата создания гранта).
Суммарные TEAM-гранты ограничены 20M — контракт не даёт превысить потолок. TGE ставится одним
вызовом setTge после finalizeDistribution: дата не раньше текущего блока и не дальше 90 дней
вперёд, операция одноразовая и действует немедленно — ни заявки, ни окна ожидания, ни отмены нет.
Опечатка в дате в самом вестинге не лечится; турнир и пресейл умеют перепривязать источник TGE
через свой setVesting. Отзыв грантов возможен только пока TGE не наступил; после него не
исправляется ничего.
Цена уровня задана в долларах и оплачивается стейблкоином без оракула. Владение BattleTownTank.sol
принадлежит контракту продажи — это единственный путь минта. Продажа включается только после того,
как все пост-проверки деплоя прошли, и требует пустой коллекции: бесплатный пре-минт закрыт
интерлоком в контракте, а не ассертом в скрипте.
Официальный турнир эскроуит 2 075 680 BTOWN периода 0 (1 000 000 победителю, по 500 000 двум случайным игрокам, 75 680 держателям). Период выводится из TGE самим контрактом, каденция — 60 дней. Случайная битва выбирается Chainlink VRF: ростер уходит в calldata, в storage ложатся только хеш и число битв, розыгрыш раскрывается тем же массивом. Брошенный турнир отменяется через 180 дней.
Community-турнир: 5% комиссии, из них 1,5% приза держателям NFT и 3,5% в казну. Ключ строки солится адресом организатора, поэтому занять чужой идентификатор нельзя.
Выплаты держателям идут эпохами. Агрегаты Tank снимаются в момент создания турнира, поэтому
момент вызова выплаты ни на что не влияет. Держатель забирает сам: claimNftRewards(epochId, ids).
Доля в режиме WEIGHT считается от фактического веса снапшота: держатели, существовавшие на
момент создания турнира, делят между собой все 1,5% приза, как обещает §16 вайтпейпера.
Непроданные танки в делителе не участвуют. Через 180 дней невыбранный остаток уходит в казну.
Это осознанные решения, а не недоделки.
- Мультисига нет. Владелец каждого контракта — обычный EOA. Компрометация ключа не отменяется технически: задержка передачи прав не защищает от их применения, у вора тот же ключ.
- Аварийного стоп-крана нет. Ни у токена, ни у танков, ни у турнира, ни у вестинга, ни у пресейла нет паузы. Выплаты нельзя остановить. Пауза защищала бы от одного класса аварий и создавала бы другой — ключ, способный заморозить деньги покупателей.
- Прокси и апгрейда нет. Ошибка в контракте лечится только новым деплоем и перепривязкой.
- Страховки от потерянного ключа нет. Ни у владельца, ни у бенефициара гранта.
- Задержек на перепривязку нет. Казна, адрес Tank и вестинг в турнире, казна и передача владения Tank в продаже, адрес турнира в хранилище, дата TGE в вестинге, казна и вестинг в пресейле — всё меняется одной транзакцией владельца и действует немедленно. Компрометация ключа владельца применяется одной транзакцией во всех контрактах. Двухшаговая схема с ожиданием давала бы EOA-владельцу лишь иллюзию времени на реакцию: у вора тот же ключ, и отменить чужую заявку он может так же, как подать свою.
- Soft cap, возвратов и предохранителей покупки нет. Пресейл не умеет возвращать стейблкоин, а
buyPublic/buyCorporateпринимают толькоbtownAmount— без потолка цены и без ожидания режима (см. «Заявленное поведение»).
renounceOwnership() заблокирован в вестинге, продаже танков, турнире, хранилище и пресейле: одна
транзакция иначе навсегда убивала бы единственный выход средств.
Что это значит на практике: окно максимального риска — период между startSale пресейла (или
первым пакетом грантов) и наступлением TGE. В нём компрометация владельца означает потерю всей
дальнейшей выручки и всего офчейн-флоата. Держать это окно коротким — операционное требование,
кодом оно не закрывается.
- Незаклеймленная награда путешествует вместе с NFT. Платим текущему владельцу токена. Продавец может снять накопленные награды в любой момент до перехода токена, включая опережение транзакции исполнения сделки на маркетплейсе. Покупатель обязан считать неклеймленные эпохи нулевыми.
- Танк, лежащий на контракте, который не умеет вызвать claim (эскроу маркетплейса, стейкинг), теряет награду через 180 дней. Клейм по маркетплейсному аппруву не разрешён намеренно.
- Официальная выплата уровня 1 — 2 BTOWN, и газ на её клейм сопоставим с ней. Это цена отказа от обхода держателей циклом; фронт обязан показывать сумму по всем эпохам сразу.
- Комиссии community-турниров с малым призом держателям не распределяются и уходят в казну целиком: ниже порога эпоху дороже клеймить, чем она стоит.
- Депег или блэклист стейблкоина: цены танков и стадий пресейла прибиты к доллару без оракула,
блэклист казны останавливает обе продажи до смены адреса казны — владелец делает её одной
транзакцией
setTreasury, ожидания нет. - Танков 10 уровня всего два, и один батч забирает оба. Аллоулиста и анти-бот механики нет.
- Принудительно присланный BNB невозвратим:
receive()и вывода нативного токена нет намеренно. - Отзыв гранта, оплаченного через пресейл.
revokeGrantsвестинга не различает гранты из офчейн-пакетов и гранты, созданные пресейлом: до наступления TGE владелец может отозвать любой из них на любой адрес, а стейблкоин покупателя уже в казне и ончейн не возвращается. Локи, напротив, неотзывны — у пресейла нет функции отзыва. Регламент владельца: при отзыве возврат делается вручную по спискам, вне блокчейна. - Смена условий покупки в мемпуле. Между отправкой транзакции покупателя и её майнингом
владелец может продвинуть стадию или переключить тумблер; покупка исполнится по новым условиям
без реверта. Предохранители
maxUsd6/expectVestedотклонены владельцем как маловероятный сценарий.
deployWhitepaper.js разворачивает десять контрактов в порядке
PAYMENT_TOKEN → Token → Tank → TankSale → Vesting → Tournament → RewardsVault → Presale → две линзы.
Циклических зависимостей нет, поэтому предвычисления адресов тоже нет. Пресейл регистрируется
грантером вестинга и получает 120M с держателя генезиса; startSale скрипт не вызывает — запуск
продажи остаётся за владельцем.
Предполётные проверки: белый список сетей, нативный баланс, попарная различность
owner / granter / operator / treasury / tokenRecipient, code.length там, где нужен EOA и где
нужен контракт, живой платёжный токен с печатью decimals() и пересчётом цены, отсутствие
незамайненных транзакций у деплойера, валидация вектора резерва танков.
Артефакт deployments/battletown.<network>.json дописывается после каждой транзакции, поэтому
падение на шестом деплое не оставляет осиротевших контрактов. Продажа танков включается последней
транзакцией, после всех пост-проверок.
npx hardhat run deployWhitepaper.js --network hardhate2e-local.js прогоняет весь путь на локальной сети: пакет грантов с pull-фондированием →
пресейл (лок, тумблер, грант PUBLIC, грант CORPORATE, ручное продвижение стадии, закрытие,
финализация непроданного в хранилище) → finalizeDistribution → setTge → клейм 10% гранта и
клейм лока → продажа танков → официальный турнир при нулевом балансе BTOWN у оператора → эпоха
выплат держателям → клеймы → свип остатка → community-турнир с комиссией → финальные инварианты.
npx hardhat run e2e-local.js --network hardhatТесты покрывают все десять контрактов, скрипт деплоя и сквозной сценарий. Никаких заявлений о числе тестов и о покрытии здесь не будет, пока их нельзя подтвердить командой.
Актуальные числа даёт npm run verify — компиляция, весь набор тестов, сквозной сценарий,
контроль размера контрактов (scripts/checkSizes.js, порог 23 000 байт) и отчёт о покрытии
(HTML складывается в coverage/, в репозиторий не коммитится).
Ни одно из этого не заменяет независимый аудит.
battle-town-smart-contracts/
├── contracts/ продакшн-контракты и mocks/ (только для тестов)
├── interface/ внешние интерфейсы, симлинк contracts/interfaces
├── scripts/ checkSizes.js — контроль размера байткода
├── tests/ unit/, integration/, helpers/
├── deployWhitepaper.js скрипт деплоя
├── e2e-local.js сквозной сценарий
└── hardhat.config.js профиль компиляции и профиль покрытия