docs(sec): SEC-9 закрыт — возврат залога при ничьей

Сводка: открыто 8 (было 9), закрыто 8. SEC-9 перенесён в закрытые с полной
историей: причина (close_bet.rs:66), замер доли ничьих (33.6%), решение
владельца (правка контракта), состав правок, собственная мутационная приёмка,
деплой и живой эффект (залипшие #0/#1 закрылись сами, ваулт −1000).
Хвост SEC-8 о «вечном залипании» помечен как устранённый.

Инструменты приёмки:
- tie-refund-accept.cjs — ставки циклом, проверка статуса на цепи
  (живой прогон: 6 возвратов из 25 ставок)
- tie-distribution.cjs, policy-economics.cjs — выбор политики ничьей
- vault-check.cjs — разбор Bet-аккаунтов сырыми байтами
This commit is contained in:
2026-09-14 02:19:28 +03:00
parent 6ee30c39fb
commit b8a1f8dc39
5 changed files with 241 additions and 5 deletions
+19 -5
View File
@@ -10,11 +10,13 @@
Пункты нумеруются сквозным `SEC-N` (не меняется при добавлении/закрытии пунктов) — ссылаться на них
номером. Отмечать закрытое — `[ ]` → `[x]`, в закрытую строку добавлять дату и коммит/релиз.
**Сводка:** открыто 9 — критичных 1 (`SEC-5`, частично закрыт), экономика 2 (`SEC-7`,`SEC-9`),
инфраструктура 3 (`SEC-10…SEC-12`), отложено 3 (`SEC-13…SEC-15`). Закрыто: 7 —
**Сводка:** открыто 8 — критичных 1 (`SEC-5`, частично закрыт), экономика 1 (`SEC-7`),
инфраструктура 3 (`SEC-10…SEC-12`), отложено 3 (`SEC-13…SEC-15`). Закрыто: 8 —
`SEC-6` (2026-09-13, `updown-relayer:2` / `test-front:3`),
`SEC-1`, `SEC-2`, `SEC-4` (2026-09-13, `updown-relayer:3`, коммит `0b136c6`),
`SEC-3`, `SEC-8` (2026-09-13, `updown-relayer:6` — авто-закрытие + одна открытая ставка на адрес),
**`SEC-9`** (2026-09-14, `updown-relayer:7` / `test-front:5` — возврат залога при ничьей в контракте,
коммит `42dfa58`),
плюс `SEC-2` и `SEC-4` усилены в `:6`.
**Итерация 2026-09-13 (`updown-relayer:6`) — что сделано:** авто-закрытие ставок серверным
@@ -51,9 +53,21 @@
- [ ] **SEC-7. Множитель 1.3× требует от дома винрейта ~77 %** — `SMART_MULTIPLIER_BPS=13000`; порог безубытка = 1/1.3 = 76.9 %. При честной монете дом убыточен, а с дырой окна закрытия игрок забирает близко к 100 %. Фикс: пересчитать экономику под реальный edge.
- [x] **SEC-8. Нефинализированные ставки запирают средства в ваулте** — **ЗАКРЫТ 2026-09-13** (релиз `updown-relayer:6`). Причины было две, обе устранены. **(1) Закрывать было некому:** авто-закрытия не существовало, а клиент мог просто не позвать `/close` — теперь шлюз закрывает сам (см. SEC-2), и ставка не может остаться висеть. **(2) Можно было набрать много ставок разом:** ничто не мешало занять сто ставок, заперев сто залогов; теперь на адрес допускается **одна открытая** ставка (вторая → `409`), поэтому размер запертого одним человеком залога ограничен одной ставкой.
*Проверено на живом стенде:* вторая ставка при открытой первой → `409`; висевшая ставка `id=14` закрыта автоматически через 16 с, средства разблокированы.
*Остаток:* если ставка попала в «равные цены» (`exit == entry`, см. SEC-9), планировщик повторяет попытку и логирует `still open … will retry` — молчаливый повтор, средства при этом всё равно остаются запертыми до первого движения цены. Это отдельная проблема SEC-9.
- [ ] **SEC-9. Равные `exit_price == entry_price` не финализируют ставку** — `close_bet` делает `return Ok(())`, статус остаётся `open`; при «плоской» цене ставка зависает без исхода. Место: `close_bet.rs:66`. Фикс: определить политику (продление / возврат / ничья) вместо молчаливого выхода.
*Остаток (закрыт):* если ставка попадала в «равные цены» (`exit == entry`), залог оставался запертым, а из-за правила «одна ставка на адрес» игрок терял возможность играть (`409`). **Устранено 2026-09-14 вместе с `SEC-9`** — ничья возвращает залог и финализирует ставку, поэтому залипаний больше нет.
- [x] **SEC-9. Равные `exit_price == entry_price` не финализировали ставку — ЗАКРЫТО 2026-09-14 (возврат залога в контракте).**
**Было:** `close_bet.rs:66` делал `return Ok(())`, статус оставался `open`; `settle.ts:303-304` берёт цену выхода по **фиксированному** моменту экспирации, поэтому ретрай каждые 2 с вечно получал ту же пару — залипание ВЕЧНОЕ. Замер: равные цены — **33.6 % окон** (11 682 из 34 769 за 580 минут). Наложенный эффект `SEC-8`: игрок с залипшей ставкой получал `409` навсегда.
**Решение владельца:** править контракт (залог лежит в ваулте под PDA — тронуть может только контракт, значит «настоящий возврат» возможен лишь правкой Rust). Программа обновляемая, authority `GXd83Q…` — у нас.
**Сделано:**
* `constants.rs`: `STATUS_REFUNDED = 3` (0/1/2 не тронуты — зашиты в шлюз и в живых аккаунтах цепи).
* `close_bet.rs`: при `exit == entry` — CPI-перевод `amount` из ваулта беттору под сидами ваулт-PDA, запись `exit_price` и `status = 3`, событие `BetClosed { user_won: false, payout: amount, status: 3 }`.
* `settle.ts`: `STATUS_REFUNDED`, `statusName → "refunded"`, статус 3 в условии финализации (иначе возврат попадал в `unexpected status` и WS-событие игроку не уходило).
* `check.ts`: проверка формы события `refunded`.
* Тест `close_bet_equal_price_stays_open` (закреплял старое поведение) → `close_bet_equal_price_refunds` + отклонение повторного закрытия.
* UI (`test-front/index.html`): три исхода вместо двух — ничья больше не показывается как «ПРОИГРАЛ (house_won)»; `API.md` дополнен.
**Проверка (своими руками, не отчётом агента):** `cargo test-sbf` 10/10 зелёных; `npm run typecheck` EXIT=0; две мутации (снять запись статуса / снять CPI-перевод) валят ровно новый тест, после отката снова 10/10.
**Деплой:** программа `9ALs…` апгрейднута (ProgramData расширен 247 848 → 267 848, хэш дампа из цепи совпал с локальным `.so` побайтово); шлюз `updown-relayer:7` (helm rev 15); фронт `test-front:5` (rev 5).
**Живой эффект:** две залипшие ставки (`#0`, `#1`) закрылись **сами** статусом 3; в обеих транзакциях ваулт −500 и игрок +500 фантиков (проверено `preTokenBalances`/`postTokenBalances`), ваулт 26 850 → 25 850. Канал `bet_closed` → `statusName: "refunded"` подтверждён.
*Остаток:* политика выбрана как «возврат залога без множителя»; доля ничьих ~33 % означает, что треть ставок не приносит дому ни прибыли, ни убытка (экономика остаётся положительной за счёт множителя 1.3×).
## Средний приоритет — инфраструктура
- [ ] **SEC-10. NATS без аутентификации** — `auth_required` не выставлен, анонимная публикация в шину проходит (проверено на тестовом subject; боевой не трогали). Пока цена условна — терпимо; как только станет денежной, у шины нет прав на запись и любой сможет напечатать свой курс. Фикс: auth + subject-ACL, шлюзу — read-only на `market.price.*`.