Skip to content

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 — значит сломать инвариант: вкладчики становятся заложниками качества каждого создателя рынка — и это не чинится параметрами:

  1. Корреляция ног — структурная дыра adverse selection. W = S·(1−m)/Π p_i честен только для независимых ног. В permissionless-мире создатель рынка может по желанию собрать коррелирующие ноги («X выиграет матч» + «X выиграет карту 2» — один и тот же факт реальности, обёрнутый двумя разными оракулами). Π p_i систематически недооценивает такие комбо, давая атакующему устойчивый +EV против пула. Корреляция между рынками — семантика реального мира, в принципе недетектируемая on-chain. Централизованные букмекеры решают это живыми трейдерами и per-combo-лимитами; у протокола такого слоя нет.
  2. Цены ног берутся с манипулируемых кривых. Execution-price quoting (q#600=A) защищает от разовой манипуляции кривой прямо перед открытием, но тонкая parimutuel-кривая всё равно не честная вероятность. Fixed odds против источника цены, на который атакующий может влиять, означают, что пул платит за чужой контроль над источником.
  3. Чего стоило сделать безопасным один существующий pool-fronted продукт (плечо, F1/#300). Плечо — единственное место, где пул фронтюет средства, и оно породило ровно этот класс сбоя: прибыль позиции превышает пул проигравших, недостача ложится на LP. Это решено — early-exit reward cap плюс deferred outcome-contingent claim дают winners_pool ≥ (1−cap)·losers − fees ≥ 0, т.е. uncovered == 0 by construction, а LP-charge-путь оставлен только как защитный фолбэк за loud-логом нарушения инварианта. Суть в цене этой гарантии: плечо — это ограниченный, обеспеченный залогом займ с liquidation-свипами, и всё равно понадобились отдельный cap, дизайн deferred-claim и постоянно включённый инвариант. У parlay-книги мультипликативные выплаты, нет залога для ликвидации и нет per-leg-границы, чтобы капнуть — той же гарантии не на что опереться.
  4. Сложность консенсуса против одной 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_payout auto-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):

  1. 2 ≤ legs.size() ≤ pm_parlay_max_legs; все market_id различны.
  2. Каждый рынок ноги: status 1 (active), беттинг ещё открыт как минимум с pm_parlay_min_time_left секундами до betting_expiration этой ноги (anti-sniping: parlays ценятся по живой кривой, поэтому поздний steam на почти закрытой ноге — самая дешёвая атака).
  3. Каждый рынок ноги разрешает instant-ставки (allow_instant_bet), не скрыт ниже risk-floor оракула, и глубина его кривой проходит manipulation-гейт (ниже).
  4. Median kill-switch pm_parlay_enabled включён; у пула есть ёмкость (ниже).
  5. 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 op pm_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-якоря):

  1. parlay_fund_used == Σ_open (W_i − S_i) — пересчитываемо проходом по открытым билетам.
  2. pool.free_balance ≥ 0 всегда (правило FIFO-очереди не тронуто; выплаты parlay идут через ту же дисциплину «никогда не ниже нуля» — escrow гарантирует, что средства существуют).
  3. Терминальные состояния билета поглощающие; 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, unwindExecution-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 / underflowW − 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^9validate()-границы на каждый новый параметр (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 после запуска в любом случае, дефолты только сидят самую первую медиану.