Parlay (accumulator) и системные ставки — рассмотрены и ОТВЕРГНУТЫ
Статус: отвергнут как consensus-примитив (решение владельца 2026-08-19, q#603=A). Полный дизайн ниже сохранён как архивная запись почему идея не вписывается в протокол, чтобы следующее предложение «давай добавим parlays» стартовало с аргументированного отказа, а не с нуля.
Почему отвергнут
Ключевой trust-инвариант платформы — liquidity providers и Lazy Pool защищены по принципалу by construction: беттинг zero-sum между бетторами (победители делят пул проигравших), а пул/LP собирают только комиссии и полы. Вкладчику не нужно доверять создателям рынков или оракулам свой принципал. Именно этот инвариант делает децентрализованный prediction market с permissionless созданием рынков и конкурирующими оракулами вообще жизнеспособным.
Настоящий parlay требует контрагента, держащего directional-риск по коэффициентам, зафиксированным на момент ставки. Сделать этим контрагентом Lazy Pool — значит сломать инвариант: вкладчики становятся заложниками качества каждого создателя рынка — и это не чинится параметрами:
- Корреляция ног — структурная дыра adverse selection.
W = S·(1−m)/Π p_iчестен только для независимых ног. В permissionless-мире создатель рынка может по желанию собрать коррелирующие ноги («X выиграет матч» + «X выиграет карту 2» — один и тот же факт реальности, обёрнутый двумя разными оракулами).Π p_iсистематически недооценивает такие комбо, давая атакующему устойчивый +EV против пула. Корреляция между рынками — семантика реального мира, в принципе недетектируемая on-chain. Централизованные букмекеры решают это живыми трейдерами и per-combo-лимитами; у протокола такого слоя нет. - Цены ног берутся с манипулируемых кривых. Execution-price quoting (q#600=A) защищает от разовой манипуляции кривой прямо перед открытием, но тонкая parimutuel-кривая всё равно не честная вероятность. Fixed odds против источника цены, на который атакующий может влиять, означают, что пул платит за чужой контроль над источником.
- Чего стоило сделать безопасным один существующий pool-fronted продукт (плечо, F1/#300). Плечо — единственное место, где пул фронтюет средства, и оно породило ровно этот класс сбоя: прибыль позиции превышает пул проигравших, недостача ложится на LP. Это решено — early-exit reward cap плюс deferred outcome-contingent claim дают
winners_pool ≥ (1−cap)·losers − fees ≥ 0, т.е.uncovered == 0by construction, а LP-charge-путь оставлен только как защитный фолбэк за loud-логом нарушения инварианта. Суть в цене этой гарантии: плечо — это ограниченный, обеспеченный залогом займ с liquidation-свипами, и всё равно понадобились отдельный cap, дизайн deferred-claim и постоянно включённый инвариант. У parlay-книги мультипликативные выплаты, нет залога для ликвидации и нет per-leg-границы, чтобы капнуть — той же гарантии не на что опереться. - Сложность консенсуса против одной UX-фичи. Десять новых median-параметров, новые объекты, новые сеттлмент-пути (void re-pricing, взаимодействия с диспутами, escrow FIFO) — всё это money-path attack surface, который надо аудировать до мейннета.
Parlay-книга работает, когда маркет-мейкер — централизованная, полностью доверенная сторона. Это явно не trust-модель данного протокола.
Что вместо этого
- Купон (одна транзакция, N независимых
pm_place_bet) уже поставляется в клиенте Forecaster — мульти-ставка без контрагента. - Клиентский auto-roll («последовательный parlay») может дать ощущение accumulator'а с нулём консенсус-изменений: клиент ре-стейкает выигрыш ноги на следующую ногу после её резолва. Контрагент = обычные parimutuel-пулы; коэффициенты не фиксируются заранее, что честно при parimutuel-ценообразовании. Позже можно укрепить небольшой операцией «условная ставка после резолва рынка X», если нужны on-chain гарантии исполнения.
- Если когда-нибудь появится полностью доверенный маркет-мейкер, parlay-книга может работать как отдельный opt-in risk-фонд (явно не Lazy Pool), где вкладчики осознанно принимают bookmaker-риск. До этого — вне scope.
Архивный дизайн (до отказа, 2026-08-18)
Всё ниже этой черты документирует дизайн, каким он был до отказа, включая scope-решения q#600/q#601, которые были залочены, пока он ещё был кандидатом. Сохранено только для справки — ничего из этого не является запланированной работой.
Проблема
Купон, поставленный в клиенте Forecaster (одна транзакция, несущая N независимых операций pm_place_bet), — это мульти-ставка, а не parlay: каждая нога сеттлится сама по себе, выигрыши и проигрыши независимы. Настоящий parlay (accumulator/экспресс) — это единый стейк на конъюнкцию N исходов: он платит, только если каждая нога выиграла, а потенциальная выплата перемножает коэффициенты ног. Системная ставка «M из N» — стандартное обобщение: стейк делится по всем C(N,M) M-ногам-под-parlay'ям, так что билет переживает до N−M проигравших ног.
Parimutuel-рынки не имеют фиксированных коэффициентов — финальный коэффициент ноги известен только при закрытии её пула. Поэтому наивный parlay «перемножить финальные parimutuel-коэффициенты» не может финансироваться пулами самих ног: cross-market конъюнкция-выплата не обеспечена проигравшими ни одного отдельного рынка. Parlay нужен явный контрагент и цена, зафиксированная на момент ставки.
Сводка дизайна
- Контрагент: Lazy Pool — тот же inventory-фонд, что уже фронтюет leverage-займы. Parlay — это side bet против пула по ценам кривой; он не трогает кривые или пулы ног.
- Цена фиксируется на момент ставки с живой кривой каждой ноги (CPMM для binary, LMSR-softmax для multi): цена комбо
P = Π p_i, потенциальная выплатаW = S · (1 − pm_parlay_margin) / P, с капом. - All-or-nothing сеттлмент, управляемый обычными оракул-резолвами ног: любая нога проиграла → билет мёртв сразу; void-нога (no-contest) исключается (её
p_iперемножается обратно — стандарт букмекеров); все оставшиеся ноги выиграли → пул платитWавтоматически после сеттла последней ноги. Без claim-операции, в духеpm_payoutauto-payout. - Worst-case escrow: пул лочит
W − Sпри открытии, так что каждый открытый билет полностью обеспечен by construction; стейкSсразу входит вpool.free_balance.
Механика
Открытие: pm_parlay_open
pm_parlay_open {
account,
legs: [ { market_id, side (binary) | outcome_index (multi) }, ... ],
amount, // стейк S, ликвидный VIZ
min_payout, // slippage-гард по W (кривая может сдвинуться между котировкой и включением)
extensions
}Validation / evaluator-гейты (все — loud FC_ASSERT):
2 ≤ legs.size() ≤ pm_parlay_max_legs; всеmarket_idразличны.- Каждый рынок ноги: status 1 (active), беттинг ещё открыт как минимум с
pm_parlay_min_time_leftсекундами доbetting_expirationэтой ноги (anti-sniping: parlays ценятся по живой кривой, поэтому поздний steam на почти закрытой ноге — самая дешёвая атака). - Каждый рынок ноги разрешает instant-ставки (
allow_instant_bet), не скрыт ниже risk-floor оракула, и глубина его кривой проходит manipulation-гейт (ниже). - Median kill-switch
pm_parlay_enabledвключён; у пула есть ёмкость (ниже). S ≥ pm_min_bet; у аккаунта есть ликвидныеS(те же правила финансирования, что уpm_place_bet).
Цена ноги p_i — это execution price пропорционального виртуального размера ноги, а не mid: квотируй кривую для гипотетической instant-ставки S на эту сторону/исход и бери получившуюся среднюю цену. Mid-котировка отдаёт атакующему спред бесплатно; execution pricing заставляет движение тонкой кривой перед открытием parlay сначала оплатить собственный slippage двигателя. Виртуальная котировка не мутирует кривую.
Выплата комбо:
P = Π p_i (0 < p_i < 1, поэтому P ∈ (0,1))
W_raw = S · (1 − pm_parlay_margin) / P
W = min(W_raw, pm_parlay_max_payout, S · pm_parlay_max_multiplier)
FC_ASSERT(W ≥ min_payout) // slippage-гард пользователя
FC_ASSERT(W > S) // parlay, который не может быть прибыльным, — мис-клик, rejectФинансирование при открытии (один сбалансированный шаг, conservation-exact):
account.balance -= S
pool.free_balance += S
pool.parlay_fund_used += (W − S) // worst-case escrow, W − S > 0 по ассерту выше
pool.free_balance -= (W − S)Гейт ёмкости: parlay_fund_used + (W − S) ≤ free-only base × pm_parlay_fund_percent — то же правило free-only базы, которое владелец зафиксировал для плеча (q#566=A): обязательства меряются только против free_balance, никогда против NAV.
Объект
pm_parlay_object {
id, account,
legs: [ { market_id, side, outcome_index, price_ppm, // p_i зафиксирована при открытии, parts-per-million
state } ], // 0 pending | 1 won | 2 lost | 3 void
stake, payout, // S, W (asset)
margin_ppm_at_open,
opened_at,
status, // 0 open | 1 won(paid) | 2 lost | 3 refunded(all-void)
last_settled_leg_count
}Индексы: by_id, by_account, и by_market_leg (market_id → id parlays) чтобы per-market резолв мог найти затронутые билеты без сканирования. Per-market fan-out ограничен pm_parlay_max_open_per_market (cap enforced при открытии через bounded index probe — без счётчика, см. commit-cap прецедент M4 и правило computed-vs-counter).
Сеттлмент
Подключён в тот же per-block process_pm_markets()-проход, что уже финализирует выплаты — ноги parlay реагируют на достижение рынком ноги состояния settled (после dispute-grace), а не на сырой resolve, так что reversal'ы диспутов учитываются автоматически:
- Нога проиграла →
status = 2билета сразу: освободить escrow (parlay_fund_used -= (W − S),free_balance += (W − S)). Стейк уже лежит в пуле — это и есть выручка пула по проигравшим билетам. Эмитить virtual oppm_parlay_lost. - Нога void (no-contest / missed-resolution void) →
state = 3; выплата сжимается:W' = W · p_i(перемножить цену исключённой ноги обратно), клампW' = max(W', S); освободить дельту escrow. Если все ноги void → рефандS(status = 3, пул возвращает стейк, полный escrow освобождён). Эмититьpm_parlay_leg_void. - Нога выиграла →
state = 1; когда последняя pending-нога сеттлится выигрышем: заплатитьpool.free_balance -= W; account.balance += W;освободить escrow-бухгалтерию (parlay_fund_used -= (W − S); лишниеW − Sуже были вырезаны из free при открытии, поэтому выплатаWдаёт free_balance−Sпротив pre-open — ровно убыток пула по выигравшему билету).status = 1, эмититьpm_parlay_won(per-account virtual op для account_history).
Работа per settled market ограничена: затронуто максимум pm_parlay_max_open_per_market билетов, каждый O(legs) ≤ pm_parlay_max_legs. Без unbounded per-block циклов (класс аудита H3/M3).
Инварианты (debug-assert, проверка при импорте снапшота как у TOKEN-якоря):
parlay_fund_used == Σ_open (W_i − S_i)— пересчитываемо проходом по открытым билетам.pool.free_balance ≥ 0всегда (правило FIFO-очереди не тронуто; выплаты parlay идут через ту же дисциплину «никогда не ниже нуля» — escrow гарантирует, что средства существуют).- Терминальные состояния билета поглощающие;
last_settled_leg_countмонотонен.
Системные ставки «M из N»
Одна операция pm_system_open, те же правила ног плюс 2 ≤ M < N ≤ pm_parlay_max_legs и C(N,M) ≤ pm_system_max_combos (например, 256 — держит worst-case работу сеттлмента и escrow-мате- матику тривиально ограниченными). Семантика: стейк S делится на C(N,M) равных sub-стейков, каждый под-parlay ценится/капается ровно как выше, из того же фиксированного набора price_ppm; escrow = Σ по комбо. Хранится как один объект (ноги + M + per-combo derived данные считаются на сеттле, не хранятся). «7 из 8» = M=7, N=8, 8 комбо. Сеттлмент: на сеттле последней ноги посчитать won/void ноги, перечислить комбо арифметически (без рекурсии), заплатить Σ выплат выигравших комбо. Правила refund/void/shrink применяются per combo. Отложено на фазу 2 реализации, но специфицировано сейчас, чтобы layout объекта и параметры не churn'ились (урок snapshot-layout: батч B → redeploy-only-by-snapshot).
Adversarial review (до реализации)
| Класс атаки / сбоя | Вектор здесь | Митигация в этом дизайне |
|---|---|---|
| Манипуляция кривой (главная) | Накачать кривую тонкой ноги, купить parlay по искажённому p_i, unwind | Execution-price quoting (двигатель платит свой slippage), гейт pm_parlay_min_depth на ногу (минимальная ликвидность кривой), pm_parlay_margin house edge, жёсткие капы max_payout/max_multiplier, окно min_time_left |
| Unbounded accumulation (класс #141) | parlay_fund_used растёт, потом вычитается | Escrow освобождается на каждом терминальном переходе, пересчитываемый инвариант 1, кламп в 0 с loud ilog на mismatch |
| Sign-flip / underflow | W − S, W' = W·p_i shrink, рефанды | W > S ассертится при открытии; void-shrink клампится на S; все вычитания клампятся max(x,0) + debug-assert |
| Missing floor/assert | «escrow покрывает выплату by construction» | Явный debug-assert инварианта 1 на каждом maintenance-блоке + перепроверка при импорте снапшота (паттерн якоря) |
| DoS / per-block work | Много билетов на одном рынке; много ног | max_open_per_market (bounded index probe), max_legs, max_combos, сеттлмент O(tickets×legs) ограничен |
| Governance extremes (класс F3) | Медиана ставит margin=0 / multiplier=10^9 | validate()-границы на каждый новый параметр (margin ≤ 20%, multiplier ≤ 10000×, legs ≤ 16, combos ≤ 1024, percent-параметры bp-checked ≤10000) — и каждый параметр вплетён в median loop (урок retention-параметра) |
| Oracle/dispute interplay | Заплатить до сеттла диспута, потом reversal | Ноги реагируют только на settled (post-grace) состояние, тот же cutoff, что у pm_payout-свипа |
| Self-dealing LP | Беттор одновременно вкладчик пула | Спец-путь не нужен: P&L пула социализируется ровно как у плеча; margin + капы ограничивают извлечение |
| Snapshot round-trip | Новые объект/поля теряются при импорте | Full-reflect export; импорт с contains()-гардами; forward-only счётчики получают сиды или пересчитываемы (инвариант 1 пересчитываем — предпочтительно) |
Новые governance-параметры (chain_properties, следующий version bump)
pm_parlay_enabled (kill-switch, default off — прецедент плеча), pm_parlay_margin (bp, default 500 = 5%, граница ≤ 2000), pm_parlay_max_legs (default 8, граница 2..16), pm_parlay_max_multiplier (default 1000×, граница ≤ 10000), pm_parlay_max_payout (VIZ, default 100k), pm_parlay_fund_percent (bp от free пула, default 2000, граница ≤ 5000), pm_parlay_min_depth (VIZ, default 1000), pm_parlay_min_time_left (sec, default 3600), pm_parlay_max_open_per_market (default 1000, граница ≤ 10000), pm_system_max_combos (default 256, граница ≤ 1024).
Все десять должны появиться в: validate() с границами, median-vote loop, get_pm_chain_properties, сериализаторах (C++ ⇄ js ⇄ php ⇄ python lock-step — урок vop/param drift из P1), и в snapshot export/import chain_properties_pm.
Client surface (после того, как нода ляжет)
Экран купона получает переключатель режима: Multi (сегодняшние N независимых ставок) / Экспресс (один pm_parlay_open) / Система M из N (фаза 2). Купон уже собирает ноги ровно в нужной форме; котировка parlay (Π p_i, потенциальная выплата, капы) вычислима client-side из тех же чтений кривой, что использует форма ставки, с min_payout как slippage-гардом. Read API: get_account_parlays, get_market_parlays (newest-first по умолчанию per q#383=A), карточка parlay в activity (табы History/Active).
Лог решений
- q#600=A (2026-08-18): цена ноги = execution price виртуального размера стейка на живой кривой (не mid) — манипулятор кривой первым платит свой slippage.
- q#601=A (2026-08-18): первый раунд = binary-ноги + простой parlay; M-of-N системы и multi (LMSR) ноги — фаза 2. Layout объекта для систем всё равно специфицирован выше, чтобы форма state не churn'илась между раундами.
- Launch-дефолты новых параметров
pm_parlay_*(margin 500 bp, max_payout 100k VIZ, max_multiplier 1000×, max_legs 8, kill-switch default off) остаются как предложено, если владелец не переопределит конкретные значения до реализации; все они median-votable после запуска в любом случае, дефолты только сидят самую первую медиану.