Files
pump-game-ops/docs/SECURITY.md
T
subochev 08ea780f7d docs(security): SEC-6 — уточнено, что фантиковая часть не localnet-only
Разделены два механизма ручки /faucet:
(а) SOL-налив (requestAirdrop) — технически только localnet, mainnet-RPC
    отдаёт 410 (проверено вживую);
(б) налив фантиков (mintTo) — НЕ привязан к сети, гейт только конфиг-флаг
    SMART_ALLOW_AIRDROP, проверки кластера в коде нет;
    SOL-барьер обходится: при балансе >=2 SOL airdrop не зовётся (ops.ts:204)
    и выполнение доходит прямо до mintTo.
Отмечено, что на боевом мемкойне pump.fun (revoked mint authority) часть (б)
исчезает сама.
2026-09-13 12:46:11 +03:00

52 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SECURITY.md — чек-лист безопасности игрового контура
**Статус:** открыт, ведётся с 2026-09-13.
**Область:** `smart-updown-token/relayer` (шлюз), контракт `smart-updown` (program `9ALs…`),
шина NATS, `test-front`. Стенд: `testfront.binom.pw` (k3s ns `game`).
Аудит проведён **на живом стенде**, не по чтению кода: воспроизведение эксплойта, проверка
ручек из внешнего интернета, проверка аутентификации шины. Фактические выводы приведены в строках.
Пункты нумеруются сквозным `SEC-N` (не меняется при добавлении/закрытии пунктов) — ссылаться на них
номером. Отмечать закрытое — `[ ]` → `[x]`, в закрытую строку добавлять дату и коммит/релиз.
**Сводка:** открыто 15 — критичных 6 (`SEC-1…SEC-6`), экономика 3 (`SEC-7…SEC-9`),
инфраструктура 3 (`SEC-10…SEC-12`), отложено 3 (`SEC-13…SEC-15`). Закрыто: 0.
---
## Критично — эксплуатируется
- [ ] **SEC-1. Нет верхней границы окна закрытия ставки** — `close_bet` проверяет только `now >= expire_time`, но не `now <= expire_time + WINDOW`. Ставка остаётся `open` бесконечно, и момент расчёта выбирает тот, кто позвал `/close`. Место: `program/programs/smart-updown/src/instructions/close_bet.rs:60`. Фикс: `require!(now <= bet.expire_time + CLOSE_WINDOW_SECONDS, Error::CloseWindowExpired)`.
- [ ] **SEC-2. Нет авто-закрытия ставок на сервере** — в шлюзе отсутствует таймер закрытия; единственный `setInterval` в коде — WS-heartbeat. Экспирация ничего не значит, раунд завершает клиент. Место: `relayer/src/gateway.ts:621`. Фикс: серверный планировщик, закрывающий ставку в момент `expire_time`.
- [ ] **SEC-3. `POST /close` доступен из интернета без авторизации** — любой может закрыть чужую ставку в выбранный им момент. Проверено: `curl -X POST https://testfront.binom.pw/api/close -d '{"id":"1"}'` → обработчик выполняется. Фикс: авторизация + серверное закрытие.
- [ ] **SEC-4. Безрисковый арбитраж на паре ставок UP+DOWN** — при `entry ≈ X` обе ставки закрываются в свой выгодный момент, обе получают `payout_done` (пара как схема — **вывод** из дыры окна; прямого прогона пары не делали). Прямо воспроизведено: ставка №5 (`DOWN`, `entry=9998000000`) висела `open` >2 мин после экспирации 15 с и закрыта при `exitPrice=9997000000` → `{"status":1,"statusName":"payout_done"}` — момент расчёта выбран вручную. Фикс: `SEC-1` + `SEC-2`.
- [ ] **SEC-5. `POST /bet` без авторизации** — любой может создавать ставки на произвольные адреса и накручивать счётчик раундов `global.counter`. Проверено: `curl -X POST .../api/bet` → 200. Фикс: токен/подпись запроса.
- [ ] **SEC-6. `GET /faucet/{address}` открыт наружу и выдаёт реальные средства** — из двух механизмов внутри ручки (`handleFaucet`, `gateway.ts:395`): **(а) SOL-налив** (`fundBettor` → `requestAirdrop`) технически только localnet — mainnet-RPC отвечает `410 Gone "RPC call or parameters have been disabled"` (проверено вживую); **(б) налив фантиков** (`ensureBettorTokenAccount` → `mintTo`) — **НЕ привязан к сети**: работает везде, где у шлюза есть mint authority нашего минта (наш `8dpXNUgW…` — authority `E7Hg…` у релейера, проверено). Гейт — только конфиг-флаг `SMART_ALLOW_AIRDROP`, **проверки кластера в коде нет** (текст «faucet only on localnet» — надпись, не замок). Дополнительно: SOL-барьер обходится — airdrop зовётся лишь при балансе <2 SOL (`ops.ts:204`), значит адрес с ≥2 SOL пролетает airdrop и доходит прямо до `mintTo`. Проверено на стенде: `curl …/api/faucet/6Q1ko…` → `{"sol":5,"solLamports":5000000000,"tokenUnits":"10000000000"}`. Фикс: `SMART_ALLOW_AIRDROP=0` вне localnet (per-environment values), ручку не публиковать наружу. **На боевом мемкойне pump.fun дыра по (б) испаряется сама** — у таких токенов mint authority отзывают; но пока на стенде свой минт с нашей authority — она живая, и полагаться на это нельзя.
## Важно — экономика и устойчивость
- [ ] **SEC-7. Множитель 1.3× требует от дома винрейта ~77 %** — `SMART_MULTIPLIER_BPS=13000`; порог безубытка = 1/1.3 = 76.9 %. При честной монете дом убыточен, а с дырой окна закрытия игрок забирает близко к 100 %. Фикс: пересчитать экономику под реальный edge.
- [ ] **SEC-8. Нефинализированные ставки запирают средства в ваулте** — `vault` не уменьшается, пока ставку не закроют. Наблюдение: `vaultBalanceHuman` 28100 → 30100 при незакрытых ставках. Фикс: серверное закрытие + клинап просроченных.
- [ ] **SEC-9. Равные `exit_price == entry_price` не финализируют ставку** — `close_bet` делает `return Ok(())`, статус остаётся `open`; при «плоской» цене ставка зависает без исхода. Место: `close_bet.rs:66`. Фикс: определить политику (продление / возврат / ничья) вместо молчаливого выхода.
## Средний приоритет — инфраструктура
- [ ] **SEC-10. NATS без аутентификации** — `auth_required` не выставлен, анонимная публикация в шину проходит (проверено на тестовом subject; боевой не трогали). Пока цена условна — терпимо; как только станет денежной, у шины нет прав на запись и любой сможет напечатать свой курс. Фикс: auth + subject-ACL, шлюзу — read-only на `market.price.*`.
- [ ] **SEC-11. Ключ релейера — единственная точка доверия в контракте** — контракт не проверяет соответствие цены рынку и принимает любое значение от подписанта (`create_bet.rs:75`, `close_bet.rs:62`). Модель «релейер = оракул» стандартна, но компрометация одного ключа `E7Hg…` = произвольный исход всех ставок. Фикс: on-chain оракул (Pyth/Switchboard) для боя.
- [ ] **SEC-12. `entry_price` не проверяется контрактом** — передаётся в `create_bet` целиком на усмотрение вызывающего; контракт не может убедиться, что это цена момента `bet_time`. Место: `create_bet.rs:46,75`. Фикс: внешний верифицируемый фид.
## Отложено — зафиксировать как риск
- [ ] **SEC-13. `SMART_ALLOW_AIRDROP=1` задеплоен в боевом поде** — флаг «только localnet» фактически включён на публичном стенде. Проверка: `kubectl get pod -n game … -o jsonpath='{.spec.containers[0].env}'`. Фикс: per-environment values, по умолчанию `0`.
- [ ] **SEC-14. Политика деградации при отсутствии цены не определена** — сейчас таймаут `PRICE_WAIT_TIMEOUT_MS=15000`, ставки просто падают. Место: `relayer/src/priceSource.ts:9`. Нужно решение: останавливать приём ставок / бэкфилл / заморозка раундов.
- [ ] **SEC-15. Аудит `binance-market` (upstream цены) не проводился** — по договорённости источник считается доверенным. Репо `subochev/binance-market`, ns `invest`. Проверить авторизацию публикации в NATS и целостность источника.
---
## Ссылки
- Реализация цены и диаграммы: `docs/PRICE-NATS.md`, `docs/diagrams/price-flow.puml`
- Решения: `docs/DECISIONS.md` — ADR-006 (предмет ставки), ADR-007 (источник), ADR-008 (реализация)
- Контракт: `program/programs/smart-updown/src/instructions/{create_bet,close_bet}.rs`
- Шлюз: `relayer/src/{gateway,natsPrice,priceSource}.ts`