Skip to content

Repository files navigation

Battle Town — смарт-контракты BTOWN

Рабочий 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
Loading

Распределение 300M

Токен минтит всю эмиссию одному получателю. Ончейн из корзин вайтпейпера закреплены две: пул 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 hardhat

Сквозной сценарий

e2e-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   профиль компиляции и профиль покрытия

About

contracts for audits

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages